Skip to content

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 — its Debug/Info/Warn/Error are (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