Logging Basics
8 examples to get you started with Logging for Node.js backends - 6 basic and 2 intermediate.
Prerequisites
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.
Basic Examples
1. Structured JSON Instead of String Concatenation
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",...}- First argument is a context object; second is a short human message.
- String templates like
`User ${id} logged in`lose searchable fields. level30 is info in Pino's numeric mapping.
Related: pino - child loggers and redaction
2. Log Levels Mean Something
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
3. Log Errors with the err Key
Pino's standard serializer captures stack traces.
try {
await chargeCard(orderId);
} catch (err) {
logger.error({ err, orderId }, "charge failed");
}{ err }triggerspino.stdSerializers.err- stack and type preserved.logger.error(err)without object wrapper loses field structure.- Map to HTTP 5xx after logging - log first, then respond generically.
Related: Error Response Standards - client vs log detail
4. Ban console.log in Application Code
Unstructured lines break JSON-only log pipelines.
// eslint no-console: error
import pino from "pino";
const log = pino();
log.info({ event: "worker_started" });console.logbypasses level filters and redaction.- ESLint
no-consoleinsrc/with exception for CLI scripts. - Tests can use
pino.destination({ sync: true })for capture.
Related: Logging with Pino on Fastify - framework integration
5. Include Request Metadata on Every HTTP Log
Method, path, status, and duration power SLO dashboards.
logger.info({
event: "request_complete",
method: "GET",
path: "/users",
statusCode: 200,
durationMs: 42,
requestId: "req_abc",
});- Consistent field names across services (
requestId, notreqIdin one app andridin another). - Duration from
reply.elapsedTime(Fastify) or middleware timing (Express). - Health checks can be sampled or excluded to reduce noise.
Related: Request Correlation IDs -
x-request-id
6. One Logger Instance Per Process
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 },
});basefields appear on every line - service name for multi-tenant log indexes.- Child loggers add per-request fields without reconfiguring the root.
- Pass
loggerto workers via import, not global mutation.
Related: pino - configuration options
Intermediate Examples
7. Child Logger for Request Context
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 tenantId- Create child in HTTP middleware or Fastify
onRequesthook. - After auth,
req.log = req.log.child({ userId: user.id }). - Child loggers are cheap - prefer over manual spread on every call.
Related: AsyncLocalStorage for Context - non-HTTP workers
8. Redact Sensitive Paths
Keep passwords and tokens out of log storage.
const logger = pino({
redact: {
paths: ["req.headers.authorization", "req.body.password", "email"],
censor: "[REDACTED]",
},
});- Redaction runs at serialize time - safer than hoping developers remember.
- List paths in a security review checklist.
- GDPR and PCI often require email/phone redaction or hashing in logs.
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.