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. 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, whose Debug/Info/Warn/Error are (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