Logging in JavaScript Applications: Best Practices for Node.js
Logging is not just about writing messages to the console. In modern Node.js applications, it is a critical tool for debugging, monitoring, and auditing. Properly configured logging allows you to quickly find errors, analyze user behavior, and ensure service stability. In this article, we will cover best practices for logging in JavaScript (Node.js), from choosing a library to setting up centralized log collection.
1. Choosing a Logging Library
The built-in console.log is only good for development. For production environments, specialized libraries are needed that provide logging levels, formatting, file rotation, and integration with external systems.
Let's look at three popular options:
- Winston — the most popular library. Flexible, with support for multiple transports (files, console, HTTP, databases). Ideal for complex systems.
- Pino — an ultra-fast logger. Recommended for high-load applications where performance is critical.
- Bunyan — generates logs in JSON format. Convenient for machine processing.
Winston setup example:
const winston = require('winston');
const logger = winston.createLogger({ level: 'info', format: winston.format.json(), defaultMeta: { service: 'user-service' }, transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ],});
if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), }));}
logger.info('Service started');logger.error('Database connection error', { db: 'main' });2. Using Logging Levels
Logging levels help filter messages by importance. The standard hierarchy (from least to most critical):
- debug — detailed debugging information (development only).
- info — standard events (request processed, user authenticated).
- warn — potential issues (low memory, slow request).
- error — critical errors (module crash, exception).
Rule: In production, use the info level or higher. debug should be disabled to avoid filling up disks and reducing performance.
Example of dynamic level switching:
const logger = require('./logger');
if (process.env.LOG_LEVEL) { logger.level = process.env.LOG_LEVEL;}
logger.debug('This message will not appear in production');logger.info('User logged in');logger.error('Failed to save data');3. Structured Logs (JSON)
Text logs are difficult to parse. The modern practice is to use structured logs in JSON format. This allows easy integration with analysis systems (ELK, Splunk, Grafana Loki).
What should be in each log:
- timestamp — exact event time (ISO 8601).
- level — logging level.
- message — human-readable description.
- context — additional data (request ID, username, error code).
Example with Pino:
const pino = require('pino');
const logger = pino({ level: 'info', timestamp: pino.stdTimeFunctions.isoTime, formatters: { level(label) { return