Logging & Observability Rules
Structured logging and correlation IDs make Node services debuggable in production without SSH and grep of unstructured strings.
Recipe
Quick-reference recipe card - copy-paste ready.
import pino from "pino";
import { randomUUID } from "node:crypto";
export const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
app.use((req, res, next) => {
const requestId = req.headers["x-request-id"]?.toString() ?? randomUUID();
res.setHeader("X-Request-Id", requestId);
req.log = logger.child({ requestId });
next();
});When to reach for this:
- Every production HTTP service and background worker.
- Cross-service calls need traceable request chains.
- Compliance requires audit trails without PII leakage.
Working Example
// src/logging/logger.ts
import pino from "pino";
export const rootLogger = pino({
level: process.env.LOG_LEVEL ?? "info",
redact: ["req.headers.authorization", "password", "creditCard"],
});
export type AppLogger = pino.Logger;// src/middleware/request-context.ts
import { randomUUID } from "node:crypto";
import type { Request, Response, NextFunction } from "express";
import { rootLogger } from "../logging/logger.js";
declare global {
namespace Express {
interface Request {
log: import("pino").Logger;
requestId: string;
}
}
}
export function requestContext(req: Request, res: Response, next: NextFunction) {
const requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
req.requestId = requestId;
res.setHeader("X-Request-Id", requestId);
req.log = rootLogger.child({ requestId, method: req.method, path: req.path });
req.log.info("request started");
res.on("finish", () => req.log.info({ status: res.statusCode }, "request finished"));
next();
}// src/services/billing.ts
export async function charge(log: AppLogger, customerId: string, cents: number) {
log.info({ customerId, cents }, "charge started");
// ...
log.info({ customerId }, "charge completed");
}What this demonstrates:
- JSON logs via Pino with field redaction.
- Incoming
X-Request-Idrespected; generated if absent. - Child logger carries
requestIdthrough service layer.
Deep Dive
How It Works
- Structured logs are JSON lines ingestable by Loki, CloudWatch, Datadog.
- Correlation ID propagates to outbound HTTP via header on internal clients.
- Log levels:
errorfor failures needing action,warnfor degraded,infofor request lifecycle,debugdev-only. - Metrics (request duration histogram, error rate) complement logs for SLOs.
Required Fields
| Field | Source |
|---|---|
requestId | Header or UUID |
level | Pino default |
msg | Human-readable event |
time | ISO timestamp automatic |
service | name in logger config |
TypeScript Notes
- Type
req.logaugmentation for Express; Fastify uses built-inreq.logwithpinointegration. - Never log full
req.body- log ids and action names only.
Gotchas
- console.log in production - Unstructured, no levels, no redaction. Fix: ESLint
no-consoleinsrc/. - Missing correlation on workers - Queue jobs untraceable. Fix: pass
requestIdin job payload from HTTP handler. - Logging PII - GDPR violations. Fix: redact paths; hash user ids in logs if needed.
- Debug logs always on - Cost and noise in prod. Fix:
LOG_LEVEL=infodefault; debug via dynamic toggle with expiry. - Logging inside tight loops - Log volume spikes. Fix: aggregate or sample; log summary counts.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| OpenTelemetry traces | Full distributed tracing | Tiny single service MVP |
| Winston | Team legacy standard | Greenfield (Pino faster on Node) |
| AsyncLocalStorage context | Implicit requestId without req.log | Simple Express apps (middleware enough) |
FAQs
Is Pino required?
No, but JSON structured logging is required. Pino is the common Node 24 choice; Fastify integrates natively.
How to propagate requestId outbound?
await fetch(url, { headers: { "X-Request-Id": requestId } });What about NestJS?
Use nestjs-pino or OTel; same correlation header rules apply.
Should errors include stack traces?
Yes in error logs server-side; never return stacks to API clients in production.
Log sampling?
Sample debug/info on high-RPS health checks; never sample error logs.
Worker log format?
Same JSON schema with jobId and requestId fields for trace join.
Metrics vs logs?
Metrics for SLO dashboards; logs for incident debugging. Both required for mature ops.
How long retain logs?
Org policy; typically 30-90 days hot, longer cold archive for audit domains.
stdout only?
Yes in Kubernetes - collectors scrape stdout; no file rotation in container.
Health check logging?
Do not log every /health at info in prod; use debug or exclude path in middleware.
Related
- Node Project Rules Checklist - rules 14-15
- Security Rules - PII redaction overlap
- API Rules - error response vs log detail
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.