Skip to content
Observability

JSON Logging Best Practices: Design Events You Can Investigate

Build a small, consistent event schema that survives collection, indexing, and production incidents.

JSON Logging Best Practices: Design Events You Can Investigate

Key takeaways

  • JSON makes fields explicit, but useful logging also requires stable names, types, units, and event meanings.
  • Use a logger and serializer rather than assembling JSON strings by hand.
  • Correlation identifiers should come from the active request or tracing context.
  • Control sensitive data, event size, and indexing choices before logs reach shared storage.

JSON logging records events as JSON objects so that software can filter and analyze individual fields. Its value comes from a consistent event design: a duration should have a predictable type and unit, an event name should have a stable meaning, and identifiers should connect related work.

Imagine an export worker that occasionally fails while writing a report to object storage. “Export failed” tells an operator where to begin. A well-designed event also identifies the export, deployment, elapsed time, and failure category without exposing the report itself.

Start With an Event You Can Explain

Here is an illustrative application schema, not a required OpenTelemetry wire format. It is expanded for readability:

{
  "timestamp": "2026-05-05T14:31:02.184Z",
  "level": "error",
  "event": "report.export.failed",
  "service": "export-worker",
  "environment": "production",
  "version": "2.8.1",
  "job_id": "job_842",
  "attempts": 3,
  "duration_ms": 1812,
  "error_code": "storage_timeout",
  "message": "Report export failed after retries"
}

The event name identifies what happened. The message explains it to a person. The remaining fields support investigation.

Before adding another field, identify its use. A full report payload is unnecessary if the operational question is whether storage requests timed out.

Keep Names, Types, and Units Consistent

JSON supports numbers, strings, booleans, objects, arrays, and null. Preserve useful types: emit 1812 as a number rather than the string “1812ms.”

Choose one name for each shared concept. Mixing duration_ms, durationMs, and elapsed makes cross-service queries harder. Mixing milliseconds and seconds under the same name is worse because queries can succeed while producing incorrect results.

Define what absence means too. Omitting an unavailable duration differs from recording zero, which appears to describe an instantaneous operation.

Keep identifiers as strings when their numerical appearance is incidental. An order reference with leading zeros is an identifier, not a quantity to aggregate.

Serialize Complete Events

Use a logging library that accepts structured fields and handles escaping. Hand-built JSON strings can break when values contain quotes, backslashes, or line breaks.

For a line-oriented collection pipeline, a common arrangement is one complete JSON object per physical line. Keep pretty-printing in a local viewer rather than sending multiline JSON to a collector expecting individual records.

A serialized stack trace can contain escaped newline characters without becoming several physical events. Confirm how the logger, container runtime, and collector handle record boundaries and size limits.

Avoid serializing the entire event into a second JSON string inside message. That can leave all the useful fields hidden behind another parsing step.

Add Shared Context in One Place

Service name, environment, and release version usually belong in logger configuration or collection enrichment. Request-specific fields belong in request or job context.

This lets business code describe the export failure without repeating deployment metadata at every call site. Define which component owns each field so enrichment does not silently overwrite application values.

Record event time consistently, for example as a UTC timestamp with an explicit offset. Collector arrival time may be later, especially when events are buffered. The OpenTelemetry log data model distinguishes event time from observed time.

Use Real Correlation Context

When a trace is active, attach its trace and span identifiers through the logger’s tracing integration. Do not generate a new trace ID independently for each log entry.

A job ID can connect retries across time, while a trace ID connects work within the tracing model. Both may be useful, but they answer different questions. See the guide to correlation IDs and trace IDs.

For the export worker, an operator might start with job_id, locate the failed attempt, and follow its trace into the storage call.

Preserve Errors Without Duplicating Them

Use the logging library’s supported exception serializer. Some exception properties are not included by a generic object serializer, so attaching an error object does not always preserve its message and stack.

Record a stable application error code for grouping, plus the exception details needed for diagnosis. Keep raw exception messages out of field names and event names.

Choose the boundary responsible for recording the failed operation. If the storage client, export service, and job runner all emit the same terminal error, one failed export can appear to be three incidents.

Intermediate retry details may still be useful at a lower level. The log-level guide explains how to distinguish those events.

Bound What Enters the Log

Use an allowlist of useful context rather than dumping entire requests or configuration objects. Credentials, tokens, signed URLs, and sensitive document content do not belong in routine diagnostic events.

The OWASP logging guidance covers sensitive-data exclusions and handling untrusted event data. Redact known sensitive values before emission; downstream masking can provide another layer.

Also bound large strings and arrays. If truncation is necessary, make it visible rather than leaving an apparently complete value.

High-cardinality values such as job IDs can be essential for investigation. Keep them where needed, but choose indexing and label policies according to the backend. A useful searchable identifier need not become an expensive aggregation dimension.

Treat Important Fields as Contracts

Dashboards and alerts become consumers of the schema. Renaming error_code during a rolling deployment may temporarily split one failure category across two query shapes.

Plan compatibility for important changes and preserve existing consumers while they migrate. Additive fields are usually easier to introduce than renamed fields or changed types.

Finally, inspect the event after ingestion. Confirm that duration remains numeric, the message is readable, correlation fields are searchable, and the pipeline has not split or dropped the record. Structured logging pays off when the structure reaches the system where people actually investigate.

Design a log event around the question someone will need to answer when the operation fails.
Top