ADR-026: Standardize Structured Logging on log/slog
Date: 2026-07-05
Status
Section titled “Status”Accepted (supersedes ADR-001 §3; amends ADR-013 §2)
Context
Section titled “Context”ADR-001 chose zerolog, but the codebase drifted: cmd/api/main.go, internal/config, and all handlers use stdlib log/slog; only internal/middleware/metrics.go uses zerolog; and 13 call sites (auth handlers, auth middleware, profile handler) still use unstructured log.Printf. Three logging APIs in one codebase means inconsistent output formats, broken log aggregation queries, and two dependencies doing one job. Issue #16 tracks the inconsistency.
When ADR-001 was written (2025-04), log/slog was “newer and less mature”. That is no longer true: slog has been the stdlib structured-logging standard since Go 1.21, the ecosystem (chi middleware, OTel bridges) supports it, and the codebase already voted with its feet.
Decision
Section titled “Decision”Standardize all logging on log/slog (stdlib):
- Migrate
internal/middleware/metrics.gooff zerolog; removegithub.com/rs/zerologfromgo.mod. - Replace all
log.Printfcall sites with leveledslogcalls carrying structured fields. - One logger setup in
main.go: JSON handler whenENV=production, text handler otherwise; level from env (LOG_LEVEL, defaultinfo);slog.SetDefaultremains the wiring mechanism. - The log-level semantics, required context fields (
request_id,user_id,error,duration_ms), and scrubbing rules in ADR-013 are unchanged — only the library changes.
Rationale for slog over migrating everything to zerolog:
- Zero dependency weight — aligns with ADR-000 budgets and the starter’s minimal-dependency ethos; zerolog is the only thing
go.modkeeps it for. - The codebase is already ~95% slog — this direction is a small refactor of one middleware file plus the
log.Printfstragglers; the reverse is a full-codebase migration. - Performance is not the bottleneck: zerolog wins microbenchmarks, but logging is nowhere near the P95 < 100ms budget’s critical path, and slog’s JSON handler allocation profile is more than adequate at this request volume.
Implementation lands in the consistency phase (issue #16) as a table-driven-tested refactor per ADR-023.
Consequences
Section titled “Consequences”- One log format across the app; aggregation queries and the ADR-013 context standards actually hold.
go.modloses a dependency; binary size budget gets marginal headroom.metrics.goneeds a small refactor; ADR-013’s zerolog code samples become illustrative rather than literal (amendment note added there).- Anyone forking the template who prefers zerolog/zap swaps one
main.gosetup block rather than untangling three APIs.
Alternatives Considered
Section titled “Alternatives Considered”- Standardize on zerolog (enforce ADR-001 as written). Rejected: migrates ~30 files instead of one, keeps a dependency stdlib now covers, and fights the codebase’s existing direction.
- Keep both, document a boundary. Rejected: there is no principled boundary; “middleware logs differently than handlers” is drift with a permission slip.
- zap. Rejected: same dependency-weight argument as zerolog with a heavier API.
References
Section titled “References”- ADR-001 §3 (superseded), ADR-013 §2 (amended), ADR-000 (budgets), ADR-023 (test-first refactor)
- Issue #16 — zerolog vs slog inconsistency
- Go blog: Structured Logging with slog
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1:
github.com/rs/zerologis absent fromgo.mod. - TC-2: No
log.Printf/log.Print/log.Printlncall sites incmd/orinternal/— logging goes throughlog/slog.
- TC-1:
- Checks:
- TC-1, TC-2 →
adr026-slog-onlyinscripts/adrcheck(status: block, promoted 2026-09-30)
- TC-1, TC-2 →
- Not machine-checkable: Structured-field completeness (
request_id,user_id,error,duration_mson relevant entries) and scrubbing rules — semantic, per ADR-013. - Graduation log:
- 2026-09-30 — promoted warn → block (ADR-033 §4): clean since 2026-07-12 (80 days, zero false positives; the rule asks for 7).
checks/enforcement.config.jsoncarriesgraduated: 2026-09-30. Demotion back to warn is allowed with the same trail.
- 2026-09-30 — promoted warn → block (ADR-033 §4): clean since 2026-07-12 (80 days, zero false positives; the rule asks for 7).