Structured Logging in Node.js with Pino

A
Author
05 Aug, 2026 1 min read 918 views

console.log("user " + id + " failed") is fine on a laptop. In production you need logs that machines can search and filter: JSON with consistent fields, levels and request IDs.

Basic setup

import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  redact: ['req.headers.authorization', '*.password', '*.cardNumber'],
});

logger.info({ orderId: 'ord_42', total: 1499 }, 'order placed');
logger.error({ err, orderId: 'ord_42' }, 'payment failed');
{"level":30,"time":1760172600000,"pid":81,"hostname":"api-7d9f","orderId":"ord_42","total":1499,"msg":"order placed"}

Request-scoped loggers

import pinoHttp from 'pino-http';
import { randomUUID } from 'node:crypto';

app.use(pinoHttp({
  logger,
  genReqId: (req) => req.headers['x-request-id'] ?? randomUUID(),
}));

app.post('/orders', async (req, res) => {
  req.log.info({ items: req.body.items.length }, 'creating order'); // includes reqId automatically
  // ...
});

Every line logged during a request carries the same reqId, so you can see the whole story of one request in your log search.

Readable output in development

node src/server.js | npx pino-pretty

Logging guidelines

  • Levels: error needs attention, warn is unexpected but handled, info is a business event, debug is for troubleshooting (off in production by default).
  • Message as a constant, details as fields: 'payment failed' with { orderId }, not an interpolated string. This makes grouping and counting possible.
  • Pass errors as err so the stack trace is serialised.
  • Never log secrets or full personal data; use redact.
  • Log to stdout and let the platform ship logs; don't manage log files in the app.
  • Include the trace ID if you use OpenTelemetry, to link logs and traces.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.