ADR-023: Observability & Wide-Events Logging
Decision
All services emit wide, structured JSON events — one log line per meaningful operation, drawn from a standardized field registry. The msg field is always static and grep-stable; dynamic values go in top-level fields or data, never interpolated into msg.
Key rules
- Emit one log entry per meaningful operation, and attach all context to that single wide event.
- Keep
msgstatic and grep-stable:"request completed", never"request completed for user 123". - Make
eventdot-namespaced and machine-readable:"http.request_completed","billing.payment_failed". - Always include the Tier 1 fields:
timestamp,level,msg,event,service,version,env. - Never log sensitive data. The canonical never-log list (PII, secrets, payloads over 2 KB) lives in ADR-025.
- Put stack traces in
data.stack, never as a top-level field. - Use the standard implementations: pino (TypeScript), zerolog (Go), structlog (Python).
Code patterns
log.info({ event: 'billing.subscription_created', data: { plan: 'pro' } }, 'subscription created')log.error({ event: 'billing.payment_failed', error: err.message, data: { stack: err.stack } }, 'payment failed')Why
Wide events keep one operation on one line, so context is queryable without correlating across entries. A static msg stays greppable while dynamic detail lives in structured fields. The never-log list is centralized in ADR-025 (rather than duplicated here) so it stays single-source-of-truth across services.
Applies when
You are adding logging to a new service, setting up pino, zerolog, or structlog, or reviewing a log statement for correctness.
Related
- ADR-025 — secrets management; canonical never-log list.
- ADR-024 — engineering standards; logging checklist.
- ADR-027 — default tech stack; observability defaults.