NodeJSLogging in Node.js

Logging in Node.js

Logs are how you find out what your application actually did in production — the only window you have when you can't attach a debugger to a live server at 3am. Good logging turns "the site is down and I have no idea why" into "request abc123 failed because the payment gateway timed out after 30s." But logging is easy to do badly: console.log everywhere, no levels, no structure, secrets leaked into plaintext. This page covers why you need a real logger, log levels, the structured-JSON approach, and the foundational rules — before the next pages dive into Winston and Pino.

Why not just console.log?
`console.log` is fine for scripts — but it's the wrong tool for a production server
`console.log` has real problems at scale. It has **no log levels** (you can't filter debug noise out in production), **no structure** (plain strings are hard to search and parse), **no timestamps or context** by default, and it writes **synchronously to stdout** in some cases — under heavy load that blocking I/O can become a bottleneck. It also can't route different severities to different destinations. A dedicated logger (Winston, Pino) solves all of these: levels, structured output, async writes, multiple transports, and redaction of sensitive fields. Use `console.log` for throwaway debugging; use a real logger for anything that runs in production.
Log levels — the severity scale

Level

When to use it

Example

fatal

App is unusable, about to crash

Cannot connect to the database on startup

error

A request/operation failed; needs attention

Unhandled exception, payment charge failed

warn

Unexpected but recoverable; watch this

Deprecated API used, retry succeeded on 2nd attempt

info

Normal significant events

Server started, user signed up, order placed

debug

Detailed diagnostic info for developers

Query parameters, cache hit/miss, branch taken

trace

Extremely verbose step-by-step flow

Every function entry/exit

Set the level by environment — `info` in production, `debug` locally
Log levels are ordered by severity, and a logger emits everything at or above its configured level. Set `LOG_LEVEL=debug` locally to see everything, and `info` (or `warn`) in production to keep volume — and cost — down while still capturing what matters. The key benefit: you can change verbosity *without changing code*, just an environment variable. Reserve `error` for things a human should investigate; if everything is logged as `error`, nothing is. Resist logging at `info` inside hot loops — high-volume logs are expensive to store and search.
Plain text vs structured JSON

JS
// ❌ Unstructured string — hard to search, parse, or aggregate:
logger.info('User 42 placed order 9981 for $120 from 10.0.0.5')

// ✅ Structured — every field is queryable in your log platform:
logger.info({
  event: 'order_placed',
  userId: 42,
  orderId: 9981,
  amount: 120,
  currency: 'USD',
  ip: '10.0.0.5',
}, 'order placed')
{"level":"info","time":"2026-06-21T09:14:02.511Z","event":"order_placed",
 "userId":42,"orderId":9981,"amount":120,"currency":"USD","ip":"10.0.0.5",
 "msg":"order placed"}
Structured logs are machine-readable — you can query `userId:42 AND event:order_placed`
Production logs are consumed by machines, not humans reading a terminal. When each log line is a JSON object with named fields, your log platform (Elastic, Datadog, Loki, CloudWatch) can index and query them: "show all `error` logs for `userId:42` in the last hour", "count `order_placed` events per minute", "alert when `payment_failed` exceeds 10/min". A free-text string can't do this — you'd be grepping. [Structured Logging](/nodejs/structured-logging) covers the patterns; for now, the rule is: **log objects, not interpolated strings.**
Never log secrets or PII

JS
// ❌ Catastrophic — passwords, tokens, and card numbers in plaintext logs:
logger.info({ body: req.body }, 'login attempt')   // logs the password!

// ✅ Redact sensitive fields (Pino has built-in redaction):
const logger = pino({
  redact: ['req.headers.authorization', 'body.password', 'body.creditCard', '*.ssn'],
})
logger.info({ body: req.body }, 'login attempt')   // password → [Redacted]
Logs are a data-breach vector — redact passwords, tokens, card numbers, and PII before writing
Logs are often stored for months, shipped to third-party platforms, and readable by anyone with ops access — so anything you log is effectively exposed. Logging a request body that contains a password, an `Authorization` header, a credit-card number, or personal data (emails, SSNs, addresses) is a serious compliance and security failure. Configure **redaction** so sensitive keys are stripped or masked automatically, and never log entire request/response bodies blindly. Treat log statements with the same scrutiny as the [secrets management](/nodejs/secrets-management) you'd apply elsewhere.
Where logs should go
  • Write to stdout/stderr — in containers and cloud platforms, the runtime captures stdout and ships it; don't write to local files yourself.

  • Let the platform handle rotation & shipping — Docker, Kubernetes, systemd, or a sidecar collects and forwards logs.

  • Centralize — aggregate all instances' logs into one searchable platform (ELK, Loki, Datadog, CloudWatch) so you can correlate across services.

  • Don't log to a file in serverless/containers — the disk is ephemeral and writing to it competes with the app.

  • Separate app logs from access logs — HTTP access logging is Morgan's job; app logic logging is Winston/Pino's.

The 12-Factor rule: treat logs as an event stream to stdout, not files you manage
The [Twelve-Factor App](https://12factor.net/logs) principle is that an app should **not** concern itself with routing or storing its output — it writes an unbuffered stream of events to `stdout`, and the execution environment captures, routes, and archives it. This decouples your code from the logging infrastructure: the same app runs unchanged whether logs go to a terminal in dev, a file in a VM, or a cloud aggregator in production. Logging libraries support file transports, but in modern containerized deployments, stdout is almost always the right target.
Choosing a logger

Library

Strength

Pick it when…

Pino

Extremely fast, JSON-first, low overhead

Performance matters; you want structured logs by default

Winston

Flexible, many transports, mature ecosystem

You need to route logs to many destinations/formats

Morgan

HTTP request logging middleware for Express

You want concise access logs for incoming requests

Next
Start with the most flexible, widely-used logger: [Winston](/nodejs/winston).