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:
115
CLAUDE.md
Normal file
115
CLAUDE.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user