structured logging

why

a log line is evidence queried at 3 a.m. under pressure, when nobody can re-derive its meaning. stable event and field names make each line machine-filterable, countable, and joinable across services in seconds; prose recovers the same facts only through manual text archaeology, and drifting field names (wid vs workflowId) silently split one fact into unmergeable buckets.

rule

every log line is a structured event: a stable event name, domain-word fields, and values a machine can filter and a human can read. logging is designed with stable names like any public symbol — the same vocabulary as the domain glossary.

event shape

fieldrule
eventone stable domain phrase: workflow.step.retry style. use glossary words.
leveldebug for development noise, info for notable events, warn for recoverable, error for attention.
correlation idpresent on every line inside one operation, from execution context.
domain fieldsthe durable nouns involved: workflowId, stepId, resourceId.
no secretsfingerprints only, per secrets and keys.

use a structured logger (consola with context objects, or the platform's equivalent) — never console.log string interpolation. one logging module owns the event vocabulary; components and services call it with domain fields, not free text.

what to log

log boundary crossings and decisions, not implementation steps:

  • request in/out with status at the route boundary.
  • durable state transitions: workflow started, step retried, handoff recorded.
  • external provider calls with latency and outcome.
  • authorization denials, rate limit verdicts.
  • typed error diagnostics with their stable code.

no per-item logging inside loops unless each item is a genuine boundary crossing; log the summary with counts.

do not

  • log free-form strings where a structured event exists.
  • invent a second field vocabulary (wid vs workflowId): one glossary, one log field set.
  • log entire request/response/error objects — choose typed fields.
  • put secrets, raw tokens, or full payloads in any line.
  • use console.log/console.error directly anywhere.

verification

grep authored source for console.log\|console.error\|console.info — expect zero outside the logging module. pick three events from different owners and confirm each carries the correlation id and glossary field names. query the log store (or grep local output) for one workflow id and confirm the full path of its operation appears as separate lines sharing the correlation id.

good and bad

bad

console.log(`retrying step ${step.name} for w=${wid} attempt=${n}`);

free-text interpolation with an invented field name (wid): unqueryable, outside the glossary, missing the correlation id. every line needs its own parser, so counting retries across operations is impossible, and wid silently splits one workflow's history from the lines that used workflowId.

good

log.warn({
event: "workflow.step.retry",
workflowId,
stepId,
attempt,
correlationId,
});

a stable event name with glossary fields — one filter finds every retry across every operation. counts, rates, and timelines come from grouping one event name instead of parsing prose; the correlation id joins the line to the rest of its operation instantly.

search pages

go to any page