Warming up the neural circuits...
By the end of this chapter you will:
When production breaks, logs are usually all you have. If they're unstructured
console.logstrings, you'll spend an hour grepping. If they're structured JSON with request IDs, you'll find the bug in 30 seconds.
Treat every log line as evidence for a future investigation. The question is always "what happened to this one user at this one moment?"
To answer that, every log line needs:
Unstructured strings can't answer that. Structured JSON can.
console.log flushes synchronously and prints arbitrary strings. Pino writes structured JSON and is ~10× faster.
npm install pino pino-httpimport pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Pretty print in dev only
transport: process.env.NODE_ENV
Two things to note:
redact — automatically strips sensitive fields. Always configure this.Use it:
logger.info({ userId: 42 }, 'logged in');
logger.warn({ userId: 42, reason: 'wrong password' }, 'failed login');
logger.error({ err }, 'The first argument is structured context. The second is the human message. Don't put values inside the message string (logger.info('user 42 logged in')) — you can't filter on them.
import pinoHttp from 'pino-http';
import { logger } from './lib/logger';
app.use(pinoHttp({
logger,
customLogLevel: (req, res, err) => {
if
Now every request automatically logs at completion with method, URL, status, latency.
A request ID is a UUID generated at the edge of your service. It travels through every log line and every outbound call. When you debug, you grep the ID and see every line for that one request.
import crypto from 'node:crypto';
export function requestId(req, res, next) {
req.id = req.headers['x-request-id'] || crypto.randomUUID()
Forward it on outbound calls too:
fetch(url, { headers: { 'x-request-id': req.id } });Now a single ID traces a request from the , through your service, to your downstream services. This is the single biggest debuggability win you can give yourself.
| Level | Use for | Production behavior |
|---|---|---|
trace | Function entry/exit | Never enabled |
debug | Development noise | Off in prod |
info | Normal events (login, request done) | On |
warn | Degraded but functional (retry, 429) | On + maybe alert |
error | Operation failed (500, exception) | On + page on-call |
fatal | Process about to die | Restart |
The rule of thumb: info is for expected events. error is for unexpected events. If a 4xx is normal (user typed wrong password), it's info, not error. If your alert system fires on every error, every wrong-password attempt wakes the on-call.
redact in Pino catches the obvious. Be thoughtful about the .
For tricky bugs, logs are step 1. Step 2: the Node inspector.
node --inspect-brk src/server.tsOpen Chrome → chrome://inspect → "inspect" your Node process. You get breakpoints, watch expressions, call stacks. The same UI you'd use for frontend JS.
VS Code has a built-in debugger:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug server",
"program"
Press F5. Set breakpoints. Step through.
L5 covers the three pillars:
You don't need all three on day one. But your logs need a request ID now so day-one logs are still useful when you add traces in year two.
| Mistake | Why it's wrong | What to do |
|---|---|---|
console.log everywhere | Unstructured, slow, can't filter | Pino |
| Logging secrets | Compliance nightmare | redact config |
error log for user errors (e.g. 400) | Wakes up the on-call | Use warn or info |
| No request ID | Can't correlate | Generate at the edge |
| Logging full request body | PII; huge logs | Log path + method + status |
Production. Ship logs to a remote service (Datadog, Logtail, ELK). Local files are a stopgap.
Performance. Pino is fast but logging is I/O. Don't put logger.debug({ entireDbResult }) in a hot loop.
Security. Configure redact paths before the first log line. Once a secret hits disk, it's logged forever.
console.log in your project with Pino.redact for req.headers.authorization, *.password, *.token.Use Pino for structured JSON logs. Add a request ID at the edge and propagate it through every log line and outbound call. Use levels meaningfully: info for expected, warn for degraded, error for unexpected. Configure redact so secrets never reach disk. When logs aren't enough, use the inspector or VS Code's debugger.
console.log.info ≠ error. Don't page on user errors.redact before the first log.console.log in production?400 Bad Request get?redact do in Pino config?