Difficulty: Intermediate
Logging is the foundation of observability in production applications. When something goes wrong at 3 AM, logs are your primary tool for understanding what happened. Good logging practices go far beyond console.log - they involve structured formats, appropriate log levels, request logging, and routing logs to different destinations (files, services, dashboards).
Node.js provides several console methods beyond console.log: console.error() writes to stderr (important for process managers), console.warn() for warnings, console.table() for tabular data, console.time()/console.timeEnd() for measuring execution time, and console.trace() for stack traces. While these are useful during development, they lack the features needed for production: log levels, file rotation, structured output, and external transport.
Winston is the most popular logging library for Node.js. It supports multiple log levels (error, warn, info, http, verbose, debug, silly), multiple transports (console, file, HTTP, and custom), and configurable formats (JSON, printf, colorized, timestamped). In production, you typically configure Winston to write JSON-formatted logs to files (with rotation) and error logs to a separate file. The JSON format enables log aggregation tools like the ELK stack (Elasticsearch, Logstash, Kibana) or Datadog to parse and search your logs.
Morgan is an HTTP request logging middleware for Express. It logs details about every incoming request: method, URL, status code, response time, and content length. Morgan integrates with Winston by streaming its output to the Winston logger instead of stdout. Common formats include 'combined' (Apache standard), 'dev' (colorized for development), and custom formats with tokens.
Log levels control what gets logged in each environment. In development, you might log everything (debug level). In production, you typically log info and above (info, warn, error). This prevents verbose debug messages from filling up production log files while ensuring you capture important events. Each log entry should include a timestamp, the log level, a clear message, and relevant context (user ID, request ID, error details).
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'myapp-api' },
transports: [
// Error logs to separate file
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// All logs to combined file
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880,
maxFiles: 5
})
]
});
// Add colorized console in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
const metaStr = Object.keys(meta).length > 1 ? JSON.stringify(meta) : '';
return `${timestamp} ${level}: ${message} ${metaStr}`;
})
)
}));
}
// Usage
logger.info('Server started', { port: 3000 });
logger.warn('Slow query detected', { duration: 2500, query: 'SELECT...' });
logger.error('Database connection failed', { host: 'db.example.com' });
console.log('Winston logger configured with', logger.transports.length, 'transports');
The logger is configured with JSON format for machine parsing, separate error and combined log files with rotation (max 5MB, 5 files), and a colorized console for development. The defaultMeta adds service name to every log entry for filtering in log aggregation tools.
const express = require('express');
const morgan = require('morgan');
const winston = require('winston');
const logger = winston.createLogger({
level: 'http',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.Console()]
});
const app = express();
// Stream Morgan logs to Winston
const morganMiddleware = morgan(
':method :url :status :res[content-length] - :response-time ms',
{
stream: {
write: (message) => logger.http(message.trim())
}
}
);
app.use(morganMiddleware);
// Custom request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info('Request completed', {
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: Date.now() - start,
userAgent: req.get('user-agent'),
ip: req.ip
});
});
next();
});
console.log('Morgan + Winston request logging configured');
Morgan handles HTTP request logging using a custom format string. The stream option pipes Morgan's output into Winston's http log level. The custom middleware adds structured JSON logging with duration measurement, useful for performance monitoring.
const winston = require('winston');
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message, ...meta }) =>
`${timestamp} [${level.toUpperCase()}] ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`
)
),
transports: [new winston.transports.Console()]
});
// Pattern 1: Operation logging
async function createUser(data) {
logger.info('Creating user', { email: data.email });
// ... create user logic
logger.info('User created successfully', { email: data.email, userId: 42 });
}
// Pattern 2: Error context logging
async function processPayment(orderId, amount) {
try {
logger.info('Processing payment', { orderId, amount });
throw new Error('Payment gateway timeout');
} catch (error) {
logger.error('Payment failed', {
orderId,
amount,
error: error.message,
stack: error.stack
});
throw error;
}
}
// Pattern 3: Performance logging
async function heavyOperation() {
const start = Date.now();
logger.debug('Starting heavy operation');
// ... operation
const duration = Date.now() - start;
logger.info('Heavy operation completed', { duration });
if (duration > 1000) {
logger.warn('Slow operation detected', { duration, threshold: 1000 });
}
}
console.log('Logging patterns demonstrated');
console.log('Patterns: operation, error context, performance');
Three essential patterns: operation logging (track what the system is doing), error context logging (capture full details when things fail), and performance logging (measure and alert on slow operations). Each includes structured metadata for searchability.
console methods, winston, morgan, log levels, structured logging, transports