Observability Wiki

Logs

Structured Logging Best Practices

Why JSON or key-value logs beat free text, and the conventions that keep structured logs cheap to query and safe to ship.

Last updated

Structured logging means emitting log entries as a well-defined set of fields — typically JSON or key-value pairs — instead of a free-text sentence a human wrote for other humans to read. Instead of "User 4521 failed login from 10.0.0.4 after 3 attempts", a structured log looks like {"event":"login_failed","user_id":4521,"ip":"10.0.0.4","attempts":3}. The information is the same; what changes is whether a machine can reliably parse it without guessing.

That distinction matters more as log volume grows. A free-text message can only be searched by regex or full-text index at query time, which is slow, fragile to wording changes, and expensive to index. A structured field can be filtered, aggregated, and correlated directly, and many log backends can index it far more cheaply than the equivalent raw text.

Why it matters

  • Queryability compounds. Once user_id, status_code, or duration_ms are real fields, you can filter, group by, and aggregate on them directly instead of writing brittle regexes against message text that changes every time a developer rewords a log line.
  • It controls cost, not just convenience. Log backends that index full text (or that must parse text at query time) pay a storage and CPU cost proportional to message complexity. Consistent structured fields let index-light systems like Loki index a small set of labels while leaving the bulk of the content compressed and unindexed, which is a major cost lever at scale.
  • It makes correlation possible. A structured field for a trace ID or request ID is what lets you jump from a trace span or a metric spike directly to the exact log lines for that request — free text makes this brittle or impossible.
  • It survives refactors and rewordings. A dashboard or alert built on event: "payment_failed" keeps working even if the human-readable message text changes; one built on matching a sentence breaks the moment someone edits the wording.

Practical guidance

  • Separate the message from the data. Keep a short, stable human-readable message field for context, and put everything queryable — IDs, durations, status codes, error types — in its own named fields rather than interpolating them into the message string.
  • Standardize field names across services. Agree on a shared convention (trace_id, user_id, duration_ms, http.status_code — the OpenTelemetry semantic conventions are a reasonable default) so the same concept isn’t called uid in one service and userId in another. Without this, cross-service queries and dashboards fall apart.
  • Never log secrets or PII in plain fields. API keys, passwords, tokens, and personal data (emails, full names, national IDs) should be redacted, hashed, or omitted before the log line is emitted — not filtered out later at the storage layer, which is easy to get wrong and leaves the data in transit and in intermediate buffers.
  • Keep the schema stable, or version it deliberately. Adding a field is safe; renaming or repurposing one silently breaks every downstream query, dashboard, and alert built against the old name.
  • Emit logs in a format your pipeline already understands. JSON lines are the practical default because essentially every collector, shipper, and backend (Fluentd, Vector, Elasticsearch, Loki, Splunk) can parse them natively without a fragile custom grok pattern.

Getting structured logging right is largely a one-time investment in convention: agree on field names and a redaction policy early, bake them into a shared logging library, and every service that adopts it becomes cheaper to query and easier to correlate for the lifetime of the system. See the logs hub for how this fits alongside levels, retention, and correlation.

Related tools