Writing Structured Logging and Tracing with Winston, Morgan, and Pino
Master production-grade logging and request tracing in Node.js by combining structured JSON logging with Winston, high-performance Pino, and HTTP request logging via Morgan.
In enterprise Node.js backend architectures, relying on standard `console.log()` statements for debugging and monitoring is insufficient. As applications scale into distributed microservices or high-throughput REST APIs, unformatted string logs become impossible to parse, filter, or ingest efficiently into observability platforms like Elasticsearch, Datadog, or Grafana Loki.
Production-grade observability requires **structured logging**—emitting logs as structured JSON objects containing timestamps, log levels, request correlation IDs, error stacks, and context metadata. This comprehensive guide explores how to configure Winston for versatile multi-transport logging, Pino for lightning-fast JSON serialization, and Morgan for HTTP access logging.
Why Structured JSON Logs Matter
Traditional text logs require complex Regular Expressions (Regex) to parse in log aggregators. Structured JSON logs solve this by organizing every log entry into key-value pairs, enabling instant querying, filtering, and aggregation.
• Correlation IDs: Propagating unique request identifiers across downstream services allows engineers to trace a single user HTTP request through authentication middleware, database queries, and background worker queues.
• Log Severities: Strict separation of log levels (`error`, `warn`, `info`, `http`, `debug`) ensures that alerting systems trigger instantly on application failures without being drowned out by verbose debug statements.
Configuring Winston with Custom Transports and Formats
Winston is one of the most flexible logging libraries for Node.js, supporting multiple transports (console, files, HTTP endpoints) and custom log formats.
npm install winston
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}),
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
export default logger;
Maximizing Throughput with Pino
For high-performance Node.js APIs where logging overhead must be kept to an absolute minimum, **Pino** is the industry benchmark. Pino is up to five times faster than Winston because it minimizes synchronous file I/O overhead and serializes JSON objects natively without intermediate transformations.
npm install pino pino-http
import express from 'express';
import pino from 'pino';
import pinoHttp from 'pino-http';
const logger = pino({
transport: {
target: 'pino-pretty',
options: { colorize: true },
},
});
const app = express();
// Attach automatic request logging and correlation IDs via pino-http
app.use(pinoHttp({ logger }));
app.get('/api/health', (req, res) => {
req.log.info('Health check endpoint accessed');
res.status(200).json({ status: 'OK' });
});
app.listen(4000, () => {
logger.info('Server started on port 4000');
});
Integrating Morgan with Winston or Pino
When using Winston for application logging, developers frequently pair it with **Morgan** to capture incoming HTTP request metrics (method, URL, status code, response time) and route them through Winston's transport stream.
import express from 'express';
import morgan from 'morgan';
import logger from './logger';
const app = express();
// Stream Morgan logs into Winston
const stream = {
write: (message: string) => logger.http(message.trim()),
};
app.use(morgan(':method :url :status :res[content-length] - :response-time ms', { stream }));
app.get('/', (req, res) => {
res.send('Hello World');
});
Summary
Implementing structured logging and request tracing with Winston, Morgan, and Pino transforms opaque backend processes into transparent, observable, and easily debuggable systems.
By standardizing log formats into JSON, propagating correlation IDs across requests, and selecting the right logger for your performance profile, engineering teams can maintain rock-solid reliability in production.