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/
116 lines
4.1 KiB
Markdown
116 lines
4.1 KiB
Markdown
# logz
|
|
|
|
Structured logger backed by log/slog with request-context enrichment and duck-typed error integration.
|
|
|
|
## Purpose
|
|
|
|
`logz` provides a stable `Logger` interface and a `New()` constructor. It wraps
|
|
`log/slog` (stdlib) so no external logging library is required. It owns the request
|
|
correlation ID context key and extra-field context key, providing helpers to attach
|
|
and retrieve them. When an error passed to `Logger.Error` implements `ErrorCode()`
|
|
or `ErrorContext()`, those fields are automatically appended to the log record —
|
|
without importing `xerrors`.
|
|
|
|
## Tier & Dependencies
|
|
|
|
**Tier:** 1
|
|
**Imports:** `context`, `errors`, `log/slog`, `os` (stdlib only)
|
|
**Must NOT import:** `xerrors`, `rbac`, `launcher`, or any other micro-lib module.
|
|
The xerrors bridge is achieved via private duck-typed interfaces (`errorWithCode`,
|
|
`errorWithContext`) — no import is needed.
|
|
|
|
## Key Design Decisions
|
|
|
|
- Wraps `log/slog` exclusively; no external logging library dependency. See
|
|
`docs/adr/ADR-001-slog-stdlib-backend.md`.
|
|
- `logz` owns `ctxRequestIDKey{}` and `ctxExtraFieldsKey{}`. Any package that needs
|
|
to attach a request ID to logs imports only `logz`. See
|
|
`docs/adr/ADR-002-requestid-context-ownership.md`.
|
|
- `Logger` is an exported interface; `New` returns it, not the concrete `*slogLogger`.
|
|
`With` returns `Logger`, not `*slogLogger`. See
|
|
`docs/adr/ADR-003-exported-logger-interface.md`.
|
|
- `Error(msg string, err error, args ...any)` treats `err` as a first-class
|
|
parameter, enabling automatic enrichment from duck-typed error interfaces.
|
|
|
|
## Patterns
|
|
|
|
**Creating a logger:**
|
|
|
|
```go
|
|
logger := logz.New(logz.Options{
|
|
Level: slog.LevelDebug,
|
|
JSON: true,
|
|
StaticArgs: []any{"service", "api", "env", "production"},
|
|
})
|
|
```
|
|
|
|
**Basic logging:**
|
|
|
|
```go
|
|
logger.Info("server started", "port", 8080)
|
|
logger.Warn("retrying", "attempt", 3)
|
|
logger.Error("request failed", err, "path", "/users")
|
|
// If err is *xerrors.Err, error_code and context fields are added automatically
|
|
```
|
|
|
|
**Child loggers:**
|
|
|
|
```go
|
|
reqLogger := logger.With("trace_id", traceID)
|
|
reqLogger.Info("handling request")
|
|
```
|
|
|
|
**Request context enrichment:**
|
|
|
|
```go
|
|
// In middleware:
|
|
ctx = logz.WithRequestID(ctx, requestID)
|
|
ctx = logz.WithField(ctx, "user_id", userID)
|
|
ctx = logz.WithFields(ctx, map[string]any{"tenant": tenantID, "region": "eu"})
|
|
|
|
// In handler (picks up request_id and extra fields automatically):
|
|
reqLogger := logger.WithContext(ctx)
|
|
reqLogger.Info("processing order")
|
|
```
|
|
|
|
**Retrieving the request ID (e.g. for response headers):**
|
|
|
|
```go
|
|
id := logz.GetRequestID(ctx)
|
|
```
|
|
|
|
**Local Logger interface in a library (ADR-001 pattern):**
|
|
|
|
```go
|
|
// In your library — does NOT import logz
|
|
type Logger interface {
|
|
Info(msg string, args ...any)
|
|
Warn(msg string, args ...any)
|
|
Error(msg string, err error, args ...any)
|
|
}
|
|
// logz.Logger satisfies this interface; pass logz.New(...) from the app layer
|
|
```
|
|
|
|
## What to Avoid
|
|
|
|
- Do not import `xerrors` from `logz`. The duck-type bridge (`errorWithCode`,
|
|
`errorWithContext`) keeps the two packages decoupled.
|
|
- Do not return `*slogLogger` from any exported function. The concrete type must
|
|
stay unexported so the interface contract is the only public surface.
|
|
- Do not write log output to `os.Stderr` or arbitrary `io.Writer`s. Output always
|
|
goes to `os.Stdout`; routing is the responsibility of the process supervisor.
|
|
- Do not use `slog.Attr` or `slog.Group` in the `Logger` interface. Keep the
|
|
variadic `key, value` convention for simplicity.
|
|
- Do not call `WithContext(nil)` — the method handles `nil` safely (returns the
|
|
same logger), but `WithRequestID` and `WithField` do not accept nil contexts.
|
|
- Do not add new methods to the `Logger` interface without a version bump. Any
|
|
addition is a breaking change for all callers that replicate the interface locally.
|
|
|
|
## Testing Notes
|
|
|
|
- `compliance_test.go` asserts at compile time that `New(Options{})` satisfies
|
|
`logz.Logger`, enforcing the full method set at every build.
|
|
- `logz_test.go` covers level filtering, JSON vs text mode, `With` chaining,
|
|
`WithContext` enrichment, and automatic error enrichment via duck-typed interfaces.
|
|
- No external test dependencies — run with plain `go test`.
|