Skip to content

Logging in Node.js Applications

In production, you can't attach a debugger, logs are often your only window into what actually happened. This lesson covers structured logging and the tools that make production logs actually useful.

Why console.log Isn't Enough in Production

console.log() works fine for local development, but production systems need more: consistent structure (so logs can be searched and filtered), severity levels (so you can distinguish routine info from urgent errors), and often JSON output (so log aggregation tools like Datadog or CloudWatch can parse fields automatically).

Dedicated logging libraries like pino (extremely fast, JSON-first) and winston (highly configurable, supports multiple "transports") solve these problems without you having to build structured logging by hand.

import pino from 'pino';

const logger = pino();

logger.info({ userId: 42 }, 'User logged in');
logger.warn({ attempts: 5 }, 'Repeated failed login attempts');
logger.error({ err: new Error('DB timeout') }, 'Failed to fetch user');

Each log call outputs structured JSON with a severity level, timestamp, and any extra fields you pass, ready to be searched and filtered by a log aggregation system.

Standard Log Levels

fatal  - the application is about to crash
error  - something failed and needs attention
warn   - unexpected but recoverable situation
info   - normal, notable application events
debug  - detailed information useful during development
  • Log levels let you filter output by severity in different environments (verbose in dev, quieter in prod).
  • error/fatal logs should generally trigger alerts; info/debug logs usually shouldn't.
  • Structured (JSON) logs are far easier to search and filter than freeform text messages.
  • Avoid logging sensitive data (passwords, full credit card numbers, tokens) at any log level.

Logging Cheatsheet

Common logging library choices and their strengths.

Library Strengths
Pino Extremely fast, JSON-first, low overhead
Winston Highly configurable, multiple transports/formats
Morgan HTTP request logging middleware for Express
console Fine for small scripts and local development
Log aggregators Datadog, CloudWatch, ELK, consume structured logs

Structured vs Unstructured Logs

An unstructured log message like "User 42 logged in from 1.2.3.4" is readable by a human but hard for tooling to search reliably. A structured log, logger.info({ userId: 42, ip: '1.2.3.4' }, 'User logged in'), keeps the same information as searchable, filterable fields.

Logging Every Request

Middleware like Morgan (or a custom logger) records every incoming request automatically, method, path, status code, and response time, giving you a complete, consistent record of traffic without adding logging calls to every individual route.

import morgan from 'morgan';

app.use(morgan('combined')); // logs every request in a standard format

In production, pair request logging with a structured logger like Pino so both request-level and application-level logs share the same searchable format.

Common Mistakes

  • Logging sensitive data like passwords, tokens, or full payment details, even temporarily during debugging.
  • Using only unstructured string messages, making production logs difficult to search or filter reliably.
  • Not distinguishing log levels, treating every message as equally important (or unimportant).
  • Relying entirely on console.log() in production instead of a proper logging library with levels and structure.

Key Takeaways

  • Production logging needs structure and severity levels beyond what plain console.log() provides.
  • Libraries like Pino and Winston provide fast, structured, JSON-first logging.
  • Standard log levels (fatal, error, warn, info, debug) let you filter noise appropriately per environment.
  • Never log sensitive data like passwords or tokens, at any log level.

Pro Tip

Adopt a structured logging library from the start of any project meant for production, even a small one. Retrofitting structured logs onto thousands of scattered console.log() calls later is far more tedious than starting with logger.info({...}, 'message') from day one.