The Logger seam¶
Every constructor in this module takes one argument: a signingcli.Logger.
This page explains what that interface is, why it is defined here rather than
imported, and why callers never need an adapter.
The interface¶
// Logger is the minimal logging surface the commands need. Both a
// *slog.Logger and GTB's logger.Logger satisfy it structurally, so callers
// pass their logger directly — no adapter.
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}
Four methods, each with the (msg string, args ...any) shape that log/slog
popularised. That is the whole contract.
Why the module defines its own interface¶
The builders need to log. An OpenPGP sign run emits a structured INFO line
carrying the backend, key id and resulting fingerprint, so operators can confirm
the signing identity without a follow-up gpg --verify. But how the host wants to
log is the host's business.
If signing-cli imported go-tool-base's logger.Logger concretely, it would
depend on go-tool-base, re-introducing exactly the cycle the module exists to
avoid (see Why a separate module). If it imported
*slog.Logger concretely, it would force every host onto log/slog, even one
with its own logging stack.
Defining a narrow interface locally sidesteps both. This is the standard Go "accept interfaces" move, applied at a module boundary: the consumer of the dependency (this module) declares the minimal surface it needs, and any type that provides it qualifies.
Why no adapter is needed¶
Go interfaces are satisfied structurally: a type implements an interface if
it has the right methods, with no implements declaration required. Both loggers
the fleet uses already have exactly these four methods with exactly this
signature:
*slog.Logger, whoseDebug/Info/Warn/Errorare(msg string, args ...any).- go-tool-base's
logger.Logger, with the same four methods in the same shape.
So both satisfy signingcli.Logger automatically. A caller writes:
// slog host
log := slog.Default()
root.AddCommand(signingcli.NewCmdSign(log))
// go-tool-base host — p.GetLogger() returns a logger.Logger
setup.Wrap("", signingcli.NewCmdSign(p.GetLogger()))
No wrapper type, no func(msg string, args ...any) shims, no import of the
other side's logger. The seam is invisible at the call site, which is the point.
The upshot¶
This one small interface is what lets signing-cli sit below both hosts without
importing either:
- It keeps go-tool-base, and every other logging stack, out of the module's dependency list.
- It lets
*slog.Logger, go-tool-base's logger, or any future logger with the four canonical methods drop straight in. - It means the command builders are usable from any Go CLI, not just ones built on a particular framework.
See also¶
- Why a separate module: the cycle this seam helps avoid.
- Attach the commands to a CLI: passing your logger in practice.