👍🎉 First off, thanks for taking the time to contribute! 🎉👍
The following is a set of guidelines for contributing to the dutctl project, which are maintained by Blindspot Software GmbH on GitHub. These are mostly guidelines, not rules. Use your best judgment.
If you discover a security issue, please bring it to our attention right away! Please refer to our Security Policy for information about reporting vulnerabilities.
Read our Code of Conduct to keep contributions approachable and respectable
Use the table of contents icon on the top right corner of this document to get to a specific section of this guide quickly.
The dutctl project follows a governance model that balances the leadership of Blindspot Software with community input. For details about how decisions are made and how you can participate, please read our Governance document.
- Ensure the bug was not already reported by searching on GitHub under Issues.
- If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
- Use the bug report template.
- Open a feature request issue on the issue tracker.
- Use the feature request template.
Unsure where to begin contributing to dutctl? You can start by looking through these issues:
- Beginner friendly issues - issues which should only require a few lines of code.
- Help wanted issues - issues which should be a bit more involved than beginner-friendly issues.
- Fork and Pull Request: All changes are made through pull requests.
- Issue First: For significant changes, opening an issue for discussion before implementation is recommended.
- Test: If you've added code that should be tested, add tests.
- Documentation: New features should include appropriate documentation.
- Consider Draft PRs: This way you can ensure the CI passes, before asking for review.
- 🚀 Issue the pull request!
- Install Go: https://go.dev/learn/ , see go.mod file for the minimal required version
- Clone the repository:
git clone https://github.com/BlindspotSoftware/dutctl.git - Install dependencies:
go mod download - Set up (optional) development tools:
- Install golangci-lint (see Code Quality section below)
- Set up commit hooks for conventional commits (see Conventional Commits section below)
The Developer Certificate of Origin (DCO) is a lightweight way for contributors to certify that they wrote or otherwise have the right to submit the code they are contributing to the .
By adding a Signed-off-by line to commit messages, you adhere to these requirements:
This is my commit message
Signed-off-by: Random J Developer <random@developer.example.org>
Tip
Use the '-s' flag on git commits to append this automatically.
This project uses Conventional Commits for its commit message format.
Commit messages should follow this pattern:
<type>(<optional scope>): <description>
[optional body]
[optional footer(s)]
Where type is one of the following:
- build: Tooling, etc.
- chore: Housekeeping, dependency management, go.mod etc.
- ci: Continuous integration, workflows, etc.
- docs: Readme, doc comments
- feat: Source code changes introducing new functionality
- fix: Bug fixes, no new functionality
- refactor: Source code changes without changing behavior
- revert: Revert a commit
- test: Add tests, increase coverage, which were not committed initially with a fix or feat commit
If you want to enable commitlint locally, check out https://commitlint.js.org/guides/local-setup.html
Tip
You can skip the husky part and just use GitHub hooks out of the box:
Rename .git/hooks/commit-msg.sample into .git/hooks/commit-msg and put in
#!/bin/sh
npx commitlint --edit
This change will not be propagated to the remote repo.
You can bypass commitlint locally like so: git commit --no-verify -m"commitlint won't like"
- Write tests for new features and bug fixes
- Run tests locally before submitting PRs:
go test ./... - Aim for reasonable test coverage of new code
- Follow the standard Go Code Review Comments guidelines (The exception proves the rule ...)
- Follow the Effective Go principles
- Document all exported symbols with proper Go doc comments
To automatically check for most of these styles and practices the CI runs golangci-lint to run a collection of linters. The rules and settings will be adapted as the project grows. Contributions are welcome here, too.
For a faster development cycle, you can integrate golangci-lint into your local setup. The current version and configuration is pinned in .golangci.yml.
We recommend setting up your editor to run golangci-lint automatically. Most popular Go IDEs support this:
- VS Code: Use the Go extension which supports golangci-lint
- GoLand: Install the Golangci-lint plugin
- Vim/Neovim: Configure with ALE or similar linting engines
The dutctl client keeps diagnostic logging separate from command output:
- stdout carries results and agent/module output (the
output.Formatter). Never log to stdout. - stderr carries client diagnostics via the standard
log/slogpackage.
Use only two levels:
slog.Debug— internal trace; hidden unless the user passes--log debug.slog.Warn— non-fatal anomalies. By default (--log warn) warnings are collected and printed as a short summary when the command finishes, so they never interrupt streaming output.
Other slog entry points (slog.Info, slog.Error, slog.Log, the *Context variants) are rejected by forbidigo. The handler still maps any level by severity (>= Warn → warn, else debug), but write Debug/Warn in code.
Errors that should stop the command are returned, not logged — they bubble up to a single exit point and are rendered through the formatter (format-aware, on stderr). There is intentionally no error log level.
The handler and the --log flag (debug|warn|none, default warn) live in cmds/dutctl/clilog.go.
dutagent (and dutserver) are service daemons, so they log differently from the client: structured records via internal/log (built on log/slog), to stderr, at the full set of levels. The base logger is installed in start() (slog.SetDefault); the -log flag sets the level (debug|info|warn|error, default debug) and -log-json switches the text handler for a JSON one. Human/TTY output is 2006/01/02 15:04:05 LEVEL [scope] message key=value (color only on a terminal); JSON emits scope as an attribute. Never log to stdout.
Obtain the logger from the context. Code retrieves it with log.FromContext(ctx). At a component boundary, the caller sets the scope and any shared attributes before handing control on, so each component logs only its own concern:
ctx = log.WithScope(ctx, "session")— set the component scope (a flat label; a new scope replaces the old one).ctx = log.With(ctx, "device", dev)— add structured attributes that descend with the context.
Module Init/Deinit and Run all receive a context carrying the module-scoped logger — a good place to log applied defaults from missing config, and the external hosts/tools/devices the module talks to. Where there genuinely is no context — process bootstrap and lifecycle — log through the slog package directly (slog.Info/slog.Error/…); where even that is unavailable (the Session methods, Locker, chanio), use a logger frozen into the struct at construction (see session.log, Locker). Scopes in dutagent: agent (bootstrap/lifecycle, the default), rpc, session, locker, module. Scopes in dutserver: server (bootstrap/lifecycle, the default), rpc, relay (with directional relay downstream/relay upstream for the two forwarders of the relayed Run stream), and registry (the device→agent map).
Module logging is for the admin/operator (the person who wrote the config and wired the DUTs), not a re-narration of what the module already prints to the client. Worth logging: external interactions with their effective parameters (tool + command, host:port, device + settings), and defaults a module applies for missing config. Init/Deinit should not log lifecycle markers — the framework already logs those.
Three logging regions, enforced by forbidigo (see the "slog logging buckets" block in .golangci.yml) — you don't have to decide the idiom, the linter guides you:
- Client (
cmds/dutctland its output rendering):slog.Debug/slog.Warnonly — its two-channel output model.slog.Info/Error/… are rejected. - Agent/server setup (process bootstrap, module
Init/Deinitlifecycle summaries, the frozen-logger objects): no request context, so log through theslogpackage directly (slog.Info,slog.Error, …). - Agent/server request path (RPC handlers, FSM, broker/workers, relay, modules): obtain the logger from the request context —
log.FromContext(ctx).X— never bareslog.
The default (any file not explicitly listed as a client or setup file in the exclusions) is the strictest request-path rule, so new code is nudged toward log.FromContext(ctx) automatically.
Levels:
Error— the agent failed at something it was asked to do; an operator likely must act.Warn— a handled or tolerated anomaly (e.g. a malformed client message, an admin force-unlock).Info— normal operational milestones; a few lines per request (request received/finished, running a module, listening).Debug— internal tracing (per-message traffic, worker start/stop, module init/deinit).
An error returned up the call stack is logged once where it becomes terminal (the RPC handler), not at every hop. Module bodies should not log lifecycle markers ("Run called"); the framework logs the transition and sets the scope.
- Use Markdown for documentation
- Keep README.md and other documentation up to date with code changes
- Use clear, concise language
- Include examples where appropriate
Thank you for contributing to dutctl! Your efforts help make this project better for everyone. ❤️