feat(logz): initial stable release v0.9.0
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/
This commit is contained in:
39
docs/adr/ADR-001-slog-stdlib-backend.md
Normal file
39
docs/adr/ADR-001-slog-stdlib-backend.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# ADR-001: log/slog as the Logging Backend
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-18
|
||||
|
||||
## Context
|
||||
|
||||
Structured logging is a cross-cutting concern required by nearly every module in
|
||||
the ecosystem. External logging libraries (zerolog, zap, logrus) add transitive
|
||||
dependencies, pin dependency versions, and require every module that accepts a
|
||||
logger to either import the concrete library or define an adapter interface. Go 1.21
|
||||
shipped `log/slog` as a stdlib structured logging API, providing JSON and text
|
||||
handlers, level filtering, and attribute chaining with no external dependencies.
|
||||
|
||||
## Decision
|
||||
|
||||
`logz` wraps `log/slog` exclusively. The concrete type `slogLogger` holds a
|
||||
`*slog.Logger`. `New(opts Options) Logger` constructs either a JSON handler
|
||||
(`slog.NewJSONHandler`) or a text handler (`slog.NewTextHandler`) backed by
|
||||
`os.Stdout`, controlled by `Options.JSON`.
|
||||
|
||||
`Options` exposes:
|
||||
- `Level slog.Level` — minimum log level (zero value = `slog.LevelInfo`).
|
||||
- `JSON bool` — JSON vs text output.
|
||||
- `StaticArgs []any` — key-value pairs attached to every record via `slog.Logger.With`.
|
||||
|
||||
The `slog` dependency is internal to the `logz` package. Consumers depend only on
|
||||
the `logz.Logger` interface and are not required to import `log/slog` at all.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Zero external dependencies: `logz` stays at Tier 1 (stdlib only).
|
||||
- The `slog` structured attribute system (`slog.String`, `slog.Int`, etc.) is
|
||||
available internally but is not exposed through the `Logger` interface — callers
|
||||
pass plain `key, value` pairs, which `slog` handles via its `any` variadic.
|
||||
- Output always goes to `os.Stdout`. Log routing (to files, remote sinks) is the
|
||||
responsibility of the process supervisor or log collector, not this package.
|
||||
- If a future Go version modifies the `slog` API, only `logz` needs to be updated —
|
||||
all consumers remain unaffected.
|
||||
47
docs/adr/ADR-002-requestid-context-ownership.md
Normal file
47
docs/adr/ADR-002-requestid-context-ownership.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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 a `map[string]any` of 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 when `ctx` is 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.
|
||||
- `WithFields` merges 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.
|
||||
- `GetRequestID` is provided for diagnostic use (e.g. adding the request ID to an
|
||||
error response header) without requiring the caller to call through the `Logger` API.
|
||||
55
docs/adr/ADR-003-exported-logger-interface.md
Normal file
55
docs/adr/ADR-003-exported-logger-interface.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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:
|
||||
|
||||
```go
|
||||
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 `Logger` interface without
|
||||
importing `logz`. They are decoupled from the logging backend.
|
||||
- `launcher`, which imports `logz` directly (it is the bootstrap layer), uses
|
||||
`logz.Logger` as the concrete type in its `New` signature.
|
||||
- Adding methods to `logz.Logger` is 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 with `slog`'s
|
||||
`Error(msg, args...)`. Adapters must account for this signature difference.
|
||||
Reference in New Issue
Block a user