Logging with Pino
Configure Pino through Fastify for structured JSON logs with request context and production-ready performance.
Recipe
Quick-reference recipe card - copy-paste ready.
import Fastify from "fastify";
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? "info",
redact: ["req.headers.authorization", "req.body.password"],
serializers: {
req(req) {
return { method: req.method, url: req.url, id: req.id };
},
},
},
});
app.get("/users", async (req) => {
req.log.info({ action: "list_users" });
return [];
});When to reach for this: Every Fastify service. Pino is the default and the right choice for production JSON logging.
Working Example
import Fastify from "fastify";
const app = Fastify({
logger: {
level: "info",
timestamp: () => `,"time":"${new Date().toISOString()}"`,
redact: {
paths: ["req.headers.authorization", "req.headers.cookie", "body.password"],
censor: "[REDACTED]",
},
},
genReqId: (req) => req.headers["x-request-id"] as string ?? crypto.randomUUID(),
requestIdLogLabel: "requestId",
});
app.addHook("onRequest", async (req) => {
req.log.info({ event: "request_start" });
});
app.addHook("onResponse", async (req, reply) => {
req.log.info({
event: "request_complete",
statusCode: reply.statusCode,
responseTime: reply.elapsedTime,
});
});
app.get("/users/:id", async (req) => {
req.log.info({ userId: req.params.id, action: "get_user" });
return { id: (req.params as { id: string }).id };
});What this demonstrates:
- Structured JSON logs with request ID propagation
- Redaction of sensitive headers and body fields
- Request lifecycle logging via hooks
reply.elapsedTimefor response timing
Deep Dive
How It Works
- Pino writes JSON lines to stdout (one log per line)
req.logis a child logger bound to the request ID- Pino is async: serializes in worker thread (minimal main-thread impact)
- Log aggregators (Datadog, Loki, CloudWatch) parse JSON lines natively
Log Levels
| Level | Use for |
|---|---|
fatal | Process is unusable |
error | Handled errors, failed operations |
warn | Degraded state, retries |
info | Request lifecycle, business events |
debug | Development diagnostics |
trace | Verbose internals |
Development Pretty Print
const app = Fastify({
logger: {
transport: process.env.NODE_ENV === "development"
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
},
});Never use pino-pretty in production. It blocks the main thread.
Gotchas
- Logging full
req.body- PII in log storage. Fix: redact paths; log only necessary fields. - pino-pretty in production - kills throughput and blocks event loop. Fix: JSON to stdout only.
- Missing request ID - cannot correlate logs across services. Fix:
genReqIdfromx-request-idheader. - console.log alongside Pino - unstructured logs mixed with JSON. Fix: ban
console.login lint rules. - Logging inside tight loops - log volume explodes. Fix: sample or aggregate.
- Not logging error context -
log.error(err)withouterrserializer loses stack. Fix:req.log.error({ err }, "message").
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Pino standalone | Express or raw Node app | Already on Fastify (built-in) |
| Winston | Team mandate for Winston transports | Greenfield Fastify (Pino is faster) |
| OpenTelemetry logs | Unified traces + logs + metrics | Simple API needing only request logs |
| morgan (Express) | Express access logs only | Fastify project |
FAQs
Why does Fastify default to Pino?
Pino is the fastest Node JSON logger. It aligns with Fastify's performance focus and produces structured logs without configuration.
How do I add user ID to every log?
In an onRequest hook after auth: req.log = req.log.child({ userId: req.user.id }).
Can I ship logs to Datadog?
Log JSON to stdout; use Datadog agent to collect container logs. Or use pino-datadog-transport (adds latency).
How does this relate to AsyncLocalStorage?
Request ID via genReqId covers HTTP context. For non-HTTP workers, use AsyncLocalStorage. See AsyncLocalStorage for Context.
Should I log request bodies?
Only in development, with redaction. Production: log method, path, status, duration, and request ID.
How do I silence health check logs?
Custom disableRequestLogging per route or filter in onResponse when path is /health.
Does NestJS use Pino?
Via nestjs-pino package. Fastify adapter integrates naturally.
What about correlation with downstream services?
Forward x-request-id in outbound HTTP calls. Log the same ID in both services.
Related
- Logging Basics - structured logging principles
- Request Correlation IDs - distributed tracing
- PII Redaction and Compliance - redaction rules
- Fastify Basics - logger configuration
- Fastify Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.