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/
51 lines
2.4 KiB
Markdown
51 lines
2.4 KiB
Markdown
# 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:
|
|
|
|
1. **Generation and storage**: the middleware must generate an ID and make it
|
|
available to downstream code via the request context.
|
|
2. **Retrieval for logging**: `RequestLogger` must 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.Logger` in 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-ID` request header, it is ignored
|
|
by `RequestID` — the middleware always generates a fresh ID. Preserving inbound
|
|
IDs is a separate concern that should be handled explicitly if required.
|
|
- This design requires `httpmw` to import `logz` directly (see ADR-001 for the
|
|
justification of that exception).
|
|
- Global ADR-003 (context helpers live with data owners) is directly applied here:
|
|
`logz` owns the request ID context key because the request ID is a logging
|
|
concern, not an auth or business concern.
|