Standalone net/http middleware for panic recovery, CORS, request ID injection, and request logging. What's included: - Recover(): panic -> 500, captures debug.Stack, no logger required - CORS(origins): OPTIONS 204 preflight, origin allowlist, package-wide method/header constants - RequestID(generator): injects ID via logz.WithRequestID, sets X-Request-ID response header - RequestLogger(logger): logs method/path/status/latency/request_id; Error for 5xx, Info otherwise - Logger interface: Info, Error, With — duck-typed; satisfied by logz.Logger - StatusRecorder: exported http.ResponseWriter wrapper that captures written status code - Direct logz import for context helpers (documented exception to ADR-001) Tested-via: todo-api POC integration Reviewed-against: docs/adr/
2.4 KiB
ADR-002: Request ID Injected via logz Context Helpers
Status: Accepted Date: 2026-03-18
Context
Each HTTP request should carry a unique identifier that appears in log records,
error responses, and the X-Request-ID response header, so that a single request
can be traced across log lines.
There are two sub-problems:
- Generation and storage: the middleware must generate an ID and make it available to downstream code via the request context.
- Retrieval for logging:
RequestLoggermust be able to read the ID from the context to include it in log records.
The naive approach — store the ID under a locally-defined context key — breaks
interoperability with logz.Logger.WithContext, which enriches log records with
values stored under logz's own unexported key. If a different key is used, logz
cannot find the ID, and it does not appear automatically in structured log output.
Decision
The RequestID middleware calls logz.WithRequestID(ctx, id) to store the
generated ID in context. RequestLogger calls logz.GetRequestID(r.Context()) to
retrieve it.
Both functions use the same unexported key inside the logz package, guaranteeing
that the value stored by RequestID is the same value retrieved by RequestLogger
and, more importantly, by any logz.Logger downstream that calls WithContext.
The ID is also written to the X-Request-ID response header at the middleware
level, so clients can correlate responses to requests without parsing log files.
Consequences
- Any
logz.Loggerin the request chain that calls.WithContext(ctx)automatically inherits the request ID as a structured log field — no manual plumbing required. - The generator function is injected by the caller (
RequestID(generator func() string)), keeping the ID format flexible (UUID, ULID, or any string). - If the same request ID is sent in the
X-Request-IDrequest header, it is ignored byRequestID— the middleware always generates a fresh ID. Preserving inbound IDs is a separate concern that should be handled explicitly if required. - This design requires
httpmwto importlogzdirectly (see ADR-001 for the justification of that exception). - Global ADR-003 (context helpers live with data owners) is directly applied here:
logzowns the request ID context key because the request ID is a logging concern, not an auth or business concern.