Structured Logging in Node.js with Pino
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:
errorneeds attention,warnis unexpected but handled,infois a business event,debugis 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
errso 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.