Structured Logs with Pino

A structured logger emits one JSON object per event, so a log platform indexes fields instead of guessing at substrings. pino-http 705 is the Express 24,430 binding: it attaches a child logger to each request as req.log, logs a request completed event on finish, and lets you set the id, level and shape.

pino-app.js — pino-http configured for an APIJavaScript
import express from 'express';
import pinoHttp from 'pino-http';
import { randomUUID } from 'node:crypto';
const app = express();
app.use(pinoHttp({
  genReqId: (req, res) => {
    const id = req.headers['x-request-id']?.slice(0, 64) ?? randomUUID();
    res.setHeader('X-Request-Id', id);  return id;
  },
  redact: ['req.headers.authorization', 'req.headers.cookie'],
  customLogLevel: (req, res, err) =>
    err || res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info',
  serializers: { req: (r) => ({ id: r.id, method: r.method, url: r.url }),
    res: (r) => ({ statusCode: r.statusCode }) } }));
app.get('/api/books', (req, res) => {
  req.log.info({ count: 1 }, 'listed books');       // the child logger for this request
  res.json([{ id: 1, title: 'Dune' }]);
});
app.get('/boom', () => { throw new Error('store unreachable'); });

A list, a 404 carrying x-request-id: trace-8f1c, and the throwing route log this, repeated fields elided:

Output of 53
{"level":30,"time":1789667027307,"pid":38212,"hostname":"PHANG","req":{"id":"ec8cc0f7-d17f-454
a-979e-cb1b8def00f7","method":"GET","url":"/api/books"},"count":1,"msg":"listed books"}
{"level":30,...,"res":{"statusCode":200},"responseTime":6,"msg":"request completed"}
{"level":40,...,"req":{"id":"trace-8f1c","url":"/api/books/99"},"res":{"statusCode":404},
 "responseTime":0,"msg":"request completed"}
{"level":50,...,"err":{"type":"Error","message":"store unreachable"},"msg":"request errored"}

Levels are numbers — 30 info, 40 warn, 50 error — because comparing integers is cheaper than comparing strings, and pino 18,227 's design is about cost: pre-compiled serializers and a stream write with no formatting pass. redact replaces the listed paths before anything is written, so a bearer token never reaches disk, and customLogLevel makes the level alertable: a 4xx is the client's mistake, a 5xx is yours.