Try the JSON Formatter

Structured JSON Logging: How to Debug Production API Errors and Search Logs at Scale

Structured JSON logs are queryable across millions of events in milliseconds; unstructured string logs require brittle grep. Here's why JSON logging matters, the OpenTelemetry log schema standard, structured logging libraries in Python/Node/Go, and how formatting API error responses reveals everything needed to debug a production incident.

By sadiqbd Β· June 16, 2026

Share:
Structured JSON Logging: How to Debug Production API Errors and Search Logs at Scale

JSON logs are machine-readable by design β€” but most developers treat them as unstructured text and miss most of the value

Structured logging β€” writing log output as JSON objects rather than formatted strings β€” transforms debugging from grep-and-hope into queryable analytics. A log line like {"level":"error","timestamp":"2024-11-15T14:23:01Z","service":"payments","trace_id":"a3f9b2","user_id":"usr_4821","amount":299.99,"error":"card_declined","code":"insufficient_funds"} contains everything needed to correlate this event with a specific user, transaction, and trace β€” and it's queryable without any string parsing.


Why structured logging beats string formatting

Unstructured log:

2024-11-15 14:23:01 ERROR Payment failed for user 4821: card declined (insufficient_funds), amount: $299.99

Structured JSON log:

{
  "level": "error",
  "timestamp": "2024-11-15T14:23:01.382Z",
  "service": "payments",
  "environment": "production",
  "trace_id": "a3f9b2c1d4e5",
  "span_id": "f8e7d6c5",
  "user_id": "usr_4821",
  "amount": 299.99,
  "currency": "GBP",
  "payment_method": "card",
  "error_code": "insufficient_funds",
  "error_category": "card_declined",
  "message": "Payment failed"
}

Querying unstructured: grep "user 4821" app.log | grep "card declined" β€” brittle, depends on exact string format, breaks if the log message template changes.

Querying structured (Elasticsearch, Datadog, Loki):

user_id:usr_4821 AND error_category:card_declined AND amount:>200

Find all large declined payments for a specific user, with exact field matching, in milliseconds across millions of log events.


JSON log schemas and standard fields

A consistent schema across all services enables cross-service correlation. The OpenTelemetry log data model (increasingly the standard for cloud-native logging) recommends these base fields:

{
  "Timestamp": "2024-11-15T14:23:01.382Z",  // ISO 8601
  "SeverityText": "ERROR",                   // TRACE, DEBUG, INFO, WARN, ERROR, FATAL
  "SeverityNumber": 17,                      // Numeric severity (1=TRACE...24=FATAL)
  "Body": "Payment failed",                  // Human-readable message
  "Resource": {                              // Service-level context
    "service.name": "payments",
    "service.version": "2.4.1",
    "deployment.environment": "production"
  },
  "Attributes": {                            // Event-specific context
    "trace_id": "a3f9b2c1d4e5",
    "user_id": "usr_4821",
    "error.code": "insufficient_funds"
  }
}

Standard severity levels and when to use them:

  • TRACE: very fine-grained, typically only in development (entering/exiting functions)
  • DEBUG: diagnostic information useful during debugging; usually disabled in production
  • INFO: normal operational events (request received, payment processed successfully)
  • WARN: unexpected but handled conditions that might need attention
  • ERROR: failures that need investigation (payment declined, downstream timeout)
  • FATAL: severe errors that cause the application to abort

Structured logging in different languages

Python (structlog):

import structlog

log = structlog.get_logger()

# Bind context that persists for this request
request_log = log.bind(
    trace_id="a3f9b2c1d4e5",
    user_id="usr_4821",
    service="payments"
)

# Log events β€” all include bound context
request_log.info("payment_initiated", amount=299.99, currency="GBP")
request_log.error("payment_failed", error_code="insufficient_funds")

Output:

{"level": "info", "timestamp": "2024-11-15T14:23:01.100Z", "trace_id": "a3f9b2c1d4e5", "user_id": "usr_4821", "service": "payments", "amount": 299.99, "currency": "GBP", "event": "payment_initiated"}
{"level": "error", "timestamp": "2024-11-15T14:23:01.382Z", "trace_id": "a3f9b2c1d4e5", "user_id": "usr_4821", "service": "payments", "error_code": "insufficient_funds", "event": "payment_failed"}

Node.js (pino):

const pino = require('pino');
const log = pino();

const requestLog = log.child({
  traceId: 'a3f9b2c1d4e5',
  userId: 'usr_4821',
  service: 'payments'
});

requestLog.info({ amount: 299.99, currency: 'GBP' }, 'payment_initiated');
requestLog.error({ errorCode: 'insufficient_funds' }, 'payment_failed');

Go (zerolog):

log.Error().
    Str("trace_id", "a3f9b2c1d4e5").
    Str("user_id", "usr_4821").
    Float64("amount", 299.99).
    Str("error_code", "insufficient_funds").
    Msg("payment_failed")

Debugging production API errors with JSON

When an API returns an error, the response body is often JSON. Formatting it immediately reveals the structure:

Raw error response:

{"error":{"code":"RATE_LIMIT_EXCEEDED","message":"Too many requests","details":{"limit":100,"window":"1m","reset_at":"2024-11-15T14:24:00Z","retry_after":47},"request_id":"req_9x8y7z"}}

Formatted:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests",
    "details": {
      "limit": 100,
      "window": "1m",
      "reset_at": "2024-11-15T14:24:00Z",
      "retry_after": 47
    },
    "request_id": "req_9x8y7z"
  }
}

Key fields become immediately visible: you must wait 47 seconds before retrying, the rate limit window is 1 minute at 100 requests, and the request_id can be provided to the API vendor's support.


Log aggregation and querying

Elasticsearch + Kibana (ELK stack): ingest JSON logs, query with Kibana Query Language (KQL) or Elasticsearch DSL. Dominant in self-hosted environments.

Grafana Loki: log aggregation optimised for Kubernetes environments. Queries with LogQL, similar to PromQL. More cost-effective than Elasticsearch for log storage.

Datadog / New Relic / Splunk: commercial SaaS log aggregation with JSON parsing, dashboards, alerting.

The JSON advantage in all of these: parsers don't need to extract fields from strings; every JSON key becomes a queryable attribute automatically. String log lines require regex extraction rules; JSON logs index everything by default.


How to use the JSON Formatter on sadiqbd.com

  1. Paste raw API error responses β€” format instantly to identify the error structure
  2. Format log output β€” copy a minified JSON log line and format for readability during debugging
  3. Validate JSON structure β€” catch missing commas, unclosed brackets before the log schema goes to production
  4. Minify JSON β€” compact JSON before storing in a field or embedding in another system

Frequently Asked Questions

Should I format JSON logs with pretty-printing in production? No β€” pretty-printed JSON (with newlines and indentation) breaks single-line-per-log-event conventions that log shippers depend on. Log aggregators parse one JSON object per line; multi-line pretty-printed JSON requires special configuration. Always write minified (single-line) JSON to log output in production. Pretty-print only for human review (copy into the formatter).

How do I redact sensitive fields from JSON logs? Use a log processor middleware or hook that replaces sensitive values before they're written:

  • Passwords, tokens, API keys: replace with [REDACTED]
  • Credit card numbers: mask all but last 4 digits
  • IP addresses: in some jurisdictions (GDPR), truncate the last octet Structlog and Pino both support processor pipelines for this purpose.

Is the JSON Formatter free? Yes β€” completely free, no sign-up required.

Try the JSON Formatter free at sadiqbd.com β€” format, validate, and minify any JSON instantly.

Share:
Try the related tool:
Open JSON Formatter

More JSON Formatter articles