Structured logger backed by log/slog with request-context enrichment, extra-field context helpers, and duck-typed automatic error enrichment. What's included: - `Logger` interface with Debug / Info / Warn / Error / With / WithContext; `New(Options)` constructor writing to os.Stdout - `WithRequestID` / `GetRequestID` and `WithField` / `WithFields` context helpers — package owns both context keys - Automatic error_code and context-field enrichment in Logger.Error via duck-typed errorWithCode / errorWithContext interfaces (no xerrors import) Tested-via: todo-api POC integration Reviewed-against: docs/adr/
2.3 KiB
ADR-003: Exported Logger Interface
Status: Accepted Date: 2026-03-18
Context
Global ADR-001 establishes that app-facing modules define a local Logger interface
satisfied by logz.Logger so that libraries do not import logz directly. For this
duck-typing pattern to work, logz must export its Logger interface with a stable
method set that other packages can replicate locally.
If Logger were unexported, or if New returned *slogLogger (the concrete type),
consumers would need to import logz just to name the type in a parameter or field
declaration, coupling every module to the logging library.
Decision
Logger is an exported interface in the logz package:
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, err error, args ...any)
With(args ...any) Logger
WithContext(ctx context.Context) Logger
}
New(opts Options) Logger returns the interface, not *slogLogger. The concrete
type is unexported. With returns Logger (interface), not *slogLogger — this
is enforced by the return type in the interface definition and by the compliance test.
Modules that accept a logger define a local interface with the subset of methods they
use (typically Info, Warn, Error). logz.Logger satisfies any such local
interface because it is a superset.
The Error signature deliberately differs from slog's: Error(msg string, err error, args ...any). The error is a first-class parameter rather than a key-value
attribute, enabling automatic enrichment from duck-typed interfaces (ErrorCode,
ErrorContext) without any extra caller code.
Consequences
- Tier 1+ modules can accept a logger via a local
Loggerinterface without importinglogz. They are decoupled from the logging backend. launcher, which importslogzdirectly (it is the bootstrap layer), useslogz.Loggeras the concrete type in itsNewsignature.- Adding methods to
logz.Loggeris a breaking change for all callers that replicate the interface locally. Any new method must be evaluated carefully and accompanied by a version bump. - The
Error(msg, err, args...)convention is not interchangeable withslog'sError(msg, args...). Adapters must account for this signature difference.