javascript 86 lines · 3 tabs

Request-Scoped Correlation ID Middleware and Child Logger in Express

Shared by codesnips Aug 2026
3 tabs
const pino = require('pino');
const { AsyncLocalStorage } = require('async_hooks');

const als = new AsyncLocalStorage();

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: {
    paths: ['req.headers.authorization', 'req.headers.cookie', '*.password'],
    censor: '[redacted]'
  }
});

function getStore() {
  return als.getStore() || {};
}

function getRequestId() {
  return getStore().requestId;
}

module.exports = { logger, als, getStore, getRequestId };
3 files · javascript Explain with highlit

This snippet shows how to thread a correlation ID through an Express request lifecycle and expose a per-request child logger, so every log line and downstream call can be tied back to a single inbound request. The core idea is request-scoped context: rather than passing an ID as an argument to every function, it is stored once at the edge and made ambient for the duration of the request.

In logger.js, a base pino logger is configured with a redact list so secrets never leak into logs. It also exports a small AsyncLocalStorage instance and a getStore helper. AsyncLocalStorage is Node's mechanism for carrying data across await boundaries and callbacks without an explicit parameter — it is the backbone that lets deeply nested code recover the current correlation ID.

In correlationId.js, the middleware reads an incoming x-request-id (or x-correlation-id) header and falls back to randomUUID() when the client did not supply one. Reusing an inbound ID is important for distributed tracing: an upstream gateway or caller may already have assigned one, and honoring it stitches the traces together. The chosen ID is written back onto the response via res.setHeader so clients and proxies can observe it, attached to req.id, and used to build req.log, a logger.child({ requestId }). Wrapping the rest of the chain in als.run(...) binds the same value into async context, so any module that calls getStore() sees the correct ID even without access to req.

The middleware also logs request completion on the res finish event, capturing status code and duration measured from process.hrtime.bigint(), which avoids clock-skew issues that Date.now() can introduce.

In app.js, the middleware is registered before routes so req.log is available everywhere. The example route uses req.log.info for a request-tagged line and calls a service function that pulls the ID from getStore() — demonstrating both access styles. The error handler reaches for req.log too, guaranteeing failures are logged with the same correlation ID as the request that caused them.

The main trade-off is a small amount of AsyncLocalStorage overhead and the discipline of registering the middleware early; in return, logs become trivially groupable by request, which is invaluable when debugging production incidents across many concurrent requests.


Related snips

Share this code

Here's the card — post it anywhere.

Request-Scoped Correlation ID Middleware and Child Logger in Express — share card
Link copied