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 — a 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— itsDebug/Info/Warn/Errorare(msg string, args ...any).- go-tool-base's
logger.Logger— same four methods, 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 the module's dependency list to
go/signing+ Cobra. - 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.