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.2 KiB
ADR-002: RequestID Context Ownership
Status: Accepted Date: 2026-03-18
Context
Global ADR-003 establishes that context helpers must live with their data owners.
The request correlation ID (request_id) is a logging concern — it is used
exclusively to enrich log records. Therefore its context key and helpers belong in
logz, not in an HTTP module or a generic ctx package.
If the key were defined in an HTTP middleware package, any non-HTTP component that wanted to attach a correlation ID to logs would need to import an HTTP package. If the key were defined in a separate context utility package, that package would become an implicit dependency of both the HTTP layer and the logging layer with no clear owner.
Decision
Two unexported context key types are defined in context.go:
ctxRequestIDKey struct{}— key for the correlation ID string.ctxExtraFieldsKey struct{}— key for amap[string]anyof arbitrary extra log fields.
Four exported helpers manage these keys:
WithRequestID(ctx, id) context.Context— stores the request ID.GetRequestID(ctx) string— retrieves it, returning""when absent or whenctxis nil.WithField(ctx, key, value) context.Context— adds one key-value pair to the extra fields map.WithFields(ctx, fields) context.Context— merges multiple pairs; does not overwrite unrelated existing fields.
Logger.WithContext(ctx) Logger reads both keys and returns a child logger with the
found values pre-attached as attributes. The method returns the same logger unchanged
when neither key is present, avoiding an unnecessary allocation.
Consequences
- Any package — HTTP middleware, gRPC interceptor, background worker — can attach a
request ID to the context by importing only
logz. No HTTP package is needed. WithFieldsmerges into a new map rather than mutating the existing one, so middleware stages can add fields without affecting the context seen by upstream handlers.- The unexported key types prevent key collisions: no external package can construct
or compare
ctxRequestIDKey{}directly. GetRequestIDis provided for diagnostic use (e.g. adding the request ID to an error response header) without requiring the caller to call through theLoggerAPI.