Structured Logging Done Right
A log line like Payment failed for user is easy for a human to read once, and nearly useless to search, filter, or aggregate across millions of log lines. Structured logging fixes that by emitting logs as data, not prose.
Plain text vs. structured
# Plain text
Payment failed for user 42, card declined, order #8817
# Structured (JSON)
{"level": "error", "event": "payment_failed", "userId": 42, "orderId": 8817, "reason": "card_declined", "timestamp": "2026-07-08T14:32:01Z"}
The structured version is trivial to query ("show me every payment_failed event where reason = card_declined in the last hour"), the plain-text version requires fragile regex or full-text search across free-form sentences that might not even be phrased consistently between developers.
Log levels
Every log line should carry a level, indicating urgency and who needs to see it:
| Level | Meaning |
|---|---|
debug | Detailed diagnostic info, useful in development, usually off in production |
info | Normal operational events (a request completed, a job started) |
warn | Something unexpected, but not (yet) a failure |
error | An operation failed |
Being disciplined about levels means production can run at info and above by default, keeping log volume (and cost) manageable, while debug is available to flip on temporarily when actively investigating something.
Correlation IDs
A single user action often triggers work across multiple services, correlating all their logs together requires a shared identifier attached to every log line involved in handling that one request:
{"event": "request_received", "correlationId": "req-9f8a", "path": "/checkout"}
{"event": "payment_charged", "correlationId": "req-9f8a", "amount": 4999}
{"event": "order_created", "correlationId": "req-9f8a", "orderId": 8817}
Generate a correlationId (or reuse the incoming one if the request came from another internal service) at the very start of handling a request, and pass it along to every downstream call and every log line for that request.
TIP
If you can't answer "show me every log line related to this one specific user's specific failed request" in under a minute, your logging isn't structured enough yet, that query is the entire point of doing this.