Logging with Pino
Configure Pino through Fastify for structured JSON logs with request context and production-ready performance.
Search across all documentation pages
Configure Pino through Fastify for structured JSON logs with request context and production-ready performance.
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.
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:
reply.elapsedTime for response timingreq.log is a child logger bound to the request ID| 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 |
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.
req.body - PII in log storage. Fix: redact paths; log only necessary fields.genReqId from x-request-id header.console.log in lint rules.log.error(err) without err serializer loses stack. Fix: req.log.error({ err }, "message").| 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 |
Pino is the fastest Node JSON logger. It aligns with Fastify's performance focus and produces structured logs without configuration.
In an onRequest hook after auth: req.log = req.log.child({ userId: req.user.id }).
Log JSON to stdout; use Datadog agent to collect container logs. Or use pino-datadog-transport (adds latency).
Request ID via genReqId covers HTTP context. For non-HTTP workers, use AsyncLocalStorage. See AsyncLocalStorage for Context.
Only in development, with redaction. Production: log method, path, status, duration, and request ID.
Custom disableRequestLogging per route or filter in onResponse when path is /health.
Via nestjs-pino package. Fastify adapter integrates naturally.
Forward x-request-id in outbound HTTP calls. Log the same ID in both services.
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.
Reviewed by Chris St. John·Last updated Jul 18, 2026