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.
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
- 1Choose a structured logger
- 2Emit JSON logs with fields
- 3Generate a correlation ID per request
- 4Propagate the ID across calls
- 5Attach trace IDs to logs