Observability

JSON logging best practices

Guidance for emitting JSON logs that are reliable, searchable, and safe with examples using the Instagram Insights API

JSON logging best practices

JSON logging gives each event a machine-readable shape.

Instead of embedding everything in a sentence:

ERROR payments Payment failed order=ord_42 attempts=3

the application can emit:

{
  "timestamp": "2026-05-05T14:30:45Z",
  "level": "error",
  "service": "payments-api",
  "orderId": "ord_42",
  "attempts": 3,
  "message": "Payment failed"
}

The second form lets a log pipeline treat attempts as a number, orderId as a field, and level as severity without parsing the message first.

The format works best when field names stay consistent and payloads remain controlled.

Start With a Small Shared Schema

A service needs a few fields on most entries:

timestamp
level
service
message

Request-oriented services will often add correlation fields such as:

traceId
spanId
requestId

Domain-specific fields belong on the events that need them rather than every log line.

For example:

{
  "timestamp": "2026-05-05T14:30:45Z",
  "level": "info",
  "service": "orders-api",
  "requestId": "req_8f3a2b",
  "orderId": "ord_901",
  "statusCode": 201,
  "durationMs": 84,
  "message": "Order created"
}

Keep the shared schema small and stable so queries work across services.

Keep Values Typed

JSON can represent strings, numbers, booleans, arrays, objects, and null values. Use those types instead of encoding everything as strings.

Prefer:

{
  "statusCode": 429,
  "retry": true,
  "retryAfterSeconds": 60
}

over:

{
  "statusCode": "429",
  "retry": "true",
  "retryAfterSeconds": "60"
}

Typed values make numeric aggregation and boolean filtering predictable.

Use nested objects only when their structure is stable and useful to downstream queries. A field that expands into a large or unstable object can create indexing problems even though the JSON is valid.

Use Consistent Timestamps and Severity

Choose one timestamp representation across services. RFC 3339 UTC timestamps are a practical choice:

2026-05-05T14:30:45Z

Avoid mixing local timestamps, ambiguous offsets, and several date layouts in the same log store.

Severity names also need consistency. If one service emits warning, another WARN, and another warn, queries become harder unless the collection pipeline normalizes them.

OpenTelemetry addresses this by retaining source severity text while also defining a normalized numeric severity range.

Put Context in Fields, Not in the Message

A useful message is short and stable:

Payment failed after all retries

Put changing details in fields:

{
  "orderId": "ord_456",
  "provider": "stripe",
  "attempts": 3,
  "errorCode": "provider_timeout",
  "message": "Payment failed after all retries"
}

This gives humans a readable event while letting machines filter on provider, group by errorCode, or calculate distributions of attempts.

It also avoids dashboards breaking because a variable identifier changed the message string.

Correlation Fields Should Match the Tracing System

A request ID and a trace ID may both help connect events, but they are not interchangeable by definition.

If the application participates in distributed tracing, log the trace and span identifiers supplied by that tracing system.

Conceptually:

traceId → whole distributed request
spanId  → particular operation within that trace

An application-specific requestId can still be useful for support or API correlation.

Propagate the identifiers supplied by the tracing and request infrastructure across service boundaries.

Centralize Enrichment

Fields such as environment, service version, region, or deployment ID are often known by middleware, the runtime, or the log collector.

Add them in one common place instead of requiring every business function to repeat them.

application event

logger / middleware

common service + request fields

collector

deployment / host metadata

Central enrichment reduces boilerplate and prevents one endpoint from using different field names from another.

It also makes schema changes easier because shared metadata can be updated in one layer.

Keep Sensitive Data Out of Log Events

Logs frequently outlive requests and are accessible to more systems and people than the original payload.

Do not log credentials, authorization headers, session tokens, private keys, or complete sensitive request bodies.

Redaction is strongest when sensitive values never enter the log event. Pipeline-side masking can provide another layer, but it should not be the only protection for known secrets.

Also consider indirect leakage. URLs, exception messages, query strings, and user-supplied text can contain personal or secret information.

Control Cardinality and Payload Size

A field can be technically useful and still be expensive to index.

Values such as full URLs, arbitrary user-agent strings, generated IDs, or unbounded user input can have very high cardinality. Logging systems may charge more or perform worse when such fields are indexed aggressively.

Large request and response bodies create a related problem.

Instead of attaching a megabyte payload to an event, record the fields needed for diagnosis and, when appropriate, a reference to data stored somewhere designed for large objects.

Sampling can reduce high-volume diagnostic traffic, but sample intentionally. Rare errors and security events may need different retention from repetitive debug events.

Exceptions Need Structured Error Fields

Avoid reducing an exception to only:

{
  "error": "connection failed"
}

When the logging library supports it, preserve the exception object or standard error fields so the pipeline can retain the type, message, and stack trace appropriately.

OpenTelemetry’s current exception conventions define fields such as exception.type, exception.message, and exception.stacktrace.

Do not record the same exception at every stack layer. Duplicate error events distort counts and increase cost without adding evidence.

Schema Changes Need Compatibility

Once dashboards and alerts depend on a field, renaming it becomes a data-contract change.

Suppose a fleet moves from:

durationMs

to:

duration_ms

During a rolling deployment, both forms may exist at once. Queries written for only one name will miss part of the traffic.

For important shared schemas, plan migrations explicitly. A schema version can help when consumers need to distinguish incompatible shapes, but versioning every minor addition is unnecessary.

The main rule is to know which fields have become contracts for downstream consumers.

Validate the Events You Depend On

Logging failures are easy to miss because the application may continue running.

Add tests or staging checks for important events:

Does the event contain service and severity?
Is durationMs numeric?
Is traceId populated when a trace exists?
Are forbidden secret fields absent?

This is especially useful for logs that drive alerts, compliance records, billing analysis, or operational dashboards.

Validation does not have to mean validating every debug line against a large schema at runtime. Test the contracts that matter.

A Production Entry Should Be Useful Without Being Huge

A useful error event might look like:

{
  "timestamp": "2026-05-05T14:31:02Z",
  "level": "error",
  "service": "payments-api",
  "environment": "production",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "spanId": "00f067aa0ba902b7",
  "orderId": "ord_456",
  "provider": "stripe",
  "attempts": 3,
  "errorCode": "provider_timeout",
  "durationMs": 1812,
  "message": "Payment failed after all retries"
}

Every field has a plausible operational use. There is no full request body, raw token, giant exception dump in an arbitrary field, or prose that needs regex parsing.

That is the standard worth aiming for: enough structure to search and correlate the event, enough message text to understand it quickly, and no data that the logging system does not need.

Top