jsonloggingobservabilitybackend

Structured JSON Logging: Best Practices

·8 min read·APIs

Why JSON logs win

A line like User 91 failed login from 1.2.3.4 is readable but useless at scale — you cannot reliably filter, aggregate, or alert on free text. Structured logging emits each event as a JSON object so log platforms (Datadog, Elastic, Loki, CloudWatch) can index every field and let you query like a database.

json
{"level":"warn","msg":"login_failed","user_id":"usr_91","ip":"1.2.3.4","ts":"2026-05-28T09:00:00Z"}

Now "show all login_failed events for usr_91 in the last hour" is a one-line query.

The fields every log line should have

FieldPurpose
tsISO 8601 timestamp (UTC)
leveldebug, info, warn, error
msgA short, stable event name — not a sentence
serviceWhich service emitted it
trace_idCorrelate logs across services for one request

Use a stable event name in msg (order_created, not Order #4 was created) and put the variable data in separate fields. Stable names are what you build dashboards and alerts on.

Use a logging library, not console

Mature libraries produce JSON, manage levels, and add context efficiently:

javascript
import pino from "pino";
const log = pino();

log.info({ user_id: "usr_91", plan: "pro" }, "subscription_started");
// {"level":30,"time":...,"user_id":"usr_91","plan":"pro","msg":"subscription_started"}
python
import structlog
log = structlog.get_logger()

log.info("payment_captured", amount=4995, currency="INR")

Context propagation

Attach request-scoped context once and have it appear on every log line for that request:

javascript
const reqLog = log.child({ trace_id: req.id, route: req.path });
reqLog.info("handler_start");
reqLog.error({ err: e.message }, "handler_failed");

A shared trace_id lets you reconstruct a single request's journey across multiple services — the foundation of distributed tracing.

What not to log

  • Secrets and credentials — passwords, tokens, API keys. Redact them.
  • Full PII — mask emails and card numbers; log a hashed or partial value if you must correlate.
  • Huge payloads — log an ID and a size, not the entire request body.

Best practices checklist

  • One JSON object per line (newline-delimited) so log shippers parse each event independently.
  • UTC ISO 8601 timestamps.
  • Stable, lowercase, snake_case event names in msg.
  • Consistent field names across services (user_id everywhere, not userId here and uid there).
  • Log levels used consistently; reserve error for things that need attention.
  • Sample or rate-limit noisy debug logs in production.

Frequently asked questions

Marginally, but the queryability is worth it. Good libraries serialize efficiently and asynchronously.

One object per line (NDJSON). Log shippers read line by line; an array would require buffering the whole file.

Pipe them through a pretty-printer during development, or use your library's "pretty" transport. In production, keep them compact for the log platform.

Try JSON Formatter

Paste a JSON log line to format and inspect its fields.