Skip to main content

How to Add Structured Logging with Correlation IDs

Switch a service to structured JSON logging and propagate a per-request correlation ID across services, optionally enriched with OpenTelemetry trace IDs so logs and traces can be joined.

Difficulty
Beginner
Duration
35 minutes
Steps
5

What and why

Plain text logs are hard to search and aggregate. Structured logs emit each entry as JSON with consistent fields, so log systems can filter and group them. A correlation ID (a unique value per request) ties together every log line produced while handling that request, even across services.

Prerequisites

  • A running service.
  • Familiarity with your language's logging.
  • A request flow that may cross services.

Steps

1. Choose a structured logger

Use a JSON-first logger: pino (Node.js), zap or slog (Go), structlog (Python), or Logback with a JSON encoder (Java).

npm install pino pino-http

2. Emit JSON logs with fields

const pino = require('pino');
const log = pino();
log.info({ userId: 42, action: 'checkout' }, 'order placed');

This produces a single JSON object per line with level, time, msg, and your fields.

3. Generate a correlation ID per request

Add middleware that creates an ID if the incoming request lacks one:

const { randomUUID } = require('crypto');
app.use((req, res, next) => {
  req.correlationId = req.headers['x-correlation-id'] || randomUUID();
  res.setHeader('x-correlation-id', req.correlationId);
  req.log = log.child({ correlationId: req.correlationId });
  next();
});

Use req.log everywhere so the ID is on every line automatically.

4. Propagate the ID across calls

When calling downstream services, forward the header:

await fetch(url, { headers: { 'x-correlation-id': req.correlationId } });

The downstream service reuses the ID instead of generating a new one, so logs across services share the same value.

5. Attach trace IDs to logs

If you use OpenTelemetry, add the active trace and span IDs as fields so you can jump from a log line to its trace:

const { trace } = require('@opentelemetry/api');
const span = trace.getActiveSpan();
const ctx = span?.spanContext();
req.log = log.child({ traceId: ctx?.traceId, spanId: ctx?.spanId, correlationId: req.correlationId });

Verification

  • Log lines are valid JSON with consistent fields.
  • All logs from one request share a correlation ID.
  • A downstream service's logs carry the same correlation ID.

Next Steps

Ship logs to a central store (Loki, Elasticsearch, or a cloud log service), build saved searches on correlationId, and avoid logging secrets or PII by adding redaction rules to the logger.

Prerequisites

  • A running service
  • Basic logging knowledge
  • An HTTP request flow

Steps

  • 1
    Choose a structured logger
  • 2
    Emit JSON logs with fields
  • 3
    Generate a correlation ID per request
  • 4
    Propagate the ID across calls
  • 5
    Attach trace IDs to logs