Logging

Structured JSON Beats `console.log`

Logging

Use pino-http for structured request logs. Per-request loggers, request IDs, log levels.

3 min read Level 2/5 #express#logging#pino
What you'll learn
  • Replace morgan with pino-http
  • Attach a request ID
  • Pick the right log level

console.log is fine for dev. Production wants structured JSON logs — grep-able, ingestable into log services.

Install Pino

npm install pino pino-http

Wire In

import pino from "pino";
import pinoHttp from "pino-http";

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  // pretty in dev only
  transport: process.env.NODE_ENV !== "production"
    ? { target: "pino-pretty" }
    : undefined,
});

app.use(pinoHttp({ logger }));

Every request gets a JSON log line with method, URL, status, duration. In dev: human-readable colors.

Per-Request Logger

pinoHttp attaches req.log to each request. Add request-specific context:

app.get("/users/:id", (req, res) => {
  req.log.info({ userId: req.params.id }, "fetching user");

  // ... handler
});

That log line includes the request ID, so you can trace one user’s flow across many log entries.

Request IDs

For tracing requests across systems:

import { randomUUID } from "node:crypto";

app.use((req, res, next) => {
  req.id = req.headers["x-request-id"] ?? randomUUID();
  res.setHeader("x-request-id", req.id);
  next();
});

app.use(pinoHttp({
  logger,
  genReqId: (req) => req.id,
}));

Now logs from your Express app and any downstream service share the ID — easy to trace a request across the whole system.

Log Levels

LevelUse for
traceVery fine-grained, almost-never
debugUseful when chasing a bug
infoStandard — server up, user signed up
warnRecoverable issues, deprecations
errorFailed requests, exceptions caught
fatalAbout to crash

In production: info and above. Lower levels are noise.

Don’t Log Secrets

The fastest way to leak a token / password / API key. Common patterns to filter:

  • req.body.password
  • req.headers.authorization
  • Stack traces that may include request data

Pino has redaction built in:

const logger = pino({
  redact: {
    paths: [
      "req.headers.authorization",
      "req.body.password",
      "*.creditCard",
    ],
    censor: "[REDACTED]",
  },
});

Ship Logs Off-Box

Don’t write to files on the server. Log to stdout — the platform (container runtime, systemd) collects it.

Ship to a service for retention + search:

  • Logtail / BetterStack
  • Datadog
  • Axiom
  • Self-hosted Loki / Elasticsearch
Monitoring →