Node.js cookbook · Beginner

Structured logging with request IDs in Node.js

Produce fast JSON logs where every line from a request carries the same request ID.

Approach

Use pino with pino-http to log requests and attach a child logger (req.log) containing the request ID. Store the ID in AsyncLocalStorage so code deep in services can log with context without passing req around. Redact secrets.

Solution

import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import pino from "pino";
import { pinoHttp } from "pino-http";

export const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  redact: ["req.headers.authorization", "req.headers.cookie", "*.password"],
});
const context = new AsyncLocalStorage<{ requestId: string }>();

app.use(pinoHttp({
  logger,
  genReqId: (req, res) => {
    const incoming = req.headers["x-request-id"];
    const id = typeof incoming === "string" && /^[\w-]{8,64}$/.test(incoming) ? incoming : randomUUID();
    res.setHeader("X-Request-ID", id);
    return id;
  },
  autoLogging: { ignore: (req) => req.url === "/healthz" },
}));
app.use((req, _res, next) => context.run({ requestId: String(req.id) }, next));

// Anywhere in the call stack:
export function log() {
  const store = context.getStore();
  return store ? logger.child({ requestId: store.requestId }) : logger;
}
// log().info({ orderId }, "order placed");

Pitfalls

  • console.log with string concatenation in production.
  • Synchronous, pretty logging in production (use pino-pretty only locally).
  • Logging tokens and cookies.

Variations

  • OpenTelemetry's pino instrumentation adds trace IDs automatically.

Further reading: pino API

Reference: https://getpino.io/#/docs/api

More Node.js recipes