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
| field | rule |
|---|---|
| event | one stable domain phrase: workflow.step.retry style. use glossary words. |
| level | debug for development noise, info for notable events, warn for recoverable, error for attention. |
| correlation id | present on every line inside one operation, from execution context. |
| domain fields | the durable nouns involved: workflowId, stepId, resourceId. |
| no secrets | fingerprints 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 (
widvsworkflowId): 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.errordirectly 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.