Logging Basics
8 examples to get you started with Logging for Node.js backends - 6 basic and 2 intermediate.
Search across all documentation pages
8 examples to get you started with Logging for Node.js backends - 6 basic and 2 intermediate.
npm install pino
npm install -D pino-pretty typescript@5.6 tsxProduction services on Node 24.18.0 should emit structured JSON to stdout - aggregators (Datadog, Loki, CloudWatch) parse one object per line.
Machines parse fields; humans grep level and requestId.
import pino from "pino";
const logger = pino({ level: "info" });
logger.info({ userId: "u_123", action: "login_success" }, "user logged in");
// {"level":30,"userId":"u_123","action":"login_success","msg":"user logged in",...}`User ${id} logged in` lose searchable fields.level 30 is info in Pino's numeric mapping.Related: pino - child loggers and redaction
Pick the level that matches operator response, not developer mood.
logger.fatal({ err }, "database unreachable at boot");
logger.error({ err, orderId }, "payment capture failed");
logger.warn({ retryCount: 3 }, "downstream timeout, retrying");
logger.info({ requestId }, "request completed");
logger.debug({ query }, "sql executed");error - needs investigation or user impact.warn - degraded but recovering (retries, fallbacks).info - normal business and request lifecycle events.debug - development only or sampled in production.Related: Logging Best Practices - prod log level policy
err KeyPino's standard serializer captures stack traces.
try {
await chargeCard(orderId);
} catch (err) {
logger.error({ err, orderId }, "charge failed");
}{ err } triggers pino.stdSerializers.err - stack and type preserved.logger.error(err) without object wrapper loses field structure.Related: Error Response Standards - client vs log detail
console.log in Application CodeUnstructured lines break JSON-only log pipelines.
// eslint no-console: error
import pino from "pino";
const log = pino();
log.info({ event: "worker_started" });console.log bypasses level filters and redaction.no-console in src/ with exception for CLI scripts.pino.destination({ sync: true }) for capture.Related: Logging with Pino on Fastify - framework integration
Method, path, status, and duration power SLO dashboards.
logger.info({
event: "request_complete",
method: "GET",
path: "/users",
statusCode: 200,
durationMs: 42,
requestId: "req_abc",
});requestId, not reqId in one app and rid in another).reply.elapsedTime (Fastify) or middleware timing (Express).Related: Request Correlation IDs -
x-request-id
Create at module scope; avoid pino() per request.
// src/logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: { service: "orders-api", version: process.env.APP_VERSION },
});base fields appear on every line - service name for multi-tenant log indexes.logger to workers via import, not global mutation.Related: pino - configuration options
Bind requestId once; all downstream logs inherit it.
const child = logger.child({ requestId: "req_abc", tenantId: "t_1" });
child.info({ action: "fetch_order" });
child.info({ action: "send_receipt" });
// both lines include requestId and tenantIdonRequest hook.req.log = req.log.child({ userId: user.id }).Related: AsyncLocalStorage for Context - non-HTTP workers
Keep passwords and tokens out of log storage.
const logger = pino({
redact: {
paths: ["req.headers.authorization", "req.body.password", "email"],
censor: "[REDACTED]",
},
});Related: PII Redaction & Compliance - retention policies
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 16, 2026