Morgan and Pino are both excellent Node.js logging libraries, but they tackle request logging with fundamentally different philosophies, and understanding this difference is key to picking the right tool for your Express app.
Here’s an Express app using Morgan to log requests:
const express = require('express');
const morgan = require('morgan');
const app = express();
// Use Morgan for request logging
app.use(morgan('dev')); // 'dev' is a pre-defined format
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
When you send a request to http://localhost:3000/, Morgan will output something like this to your console:
GET / 200 4.594 ms - 12
Morgan’s strength lies in its simplicity and its direct integration with the HTTP request/response cycle. It’s designed to be used as Express middleware, meaning it intercepts requests as they come in and logs them as they go out. The output is concise and human-readable by default, focusing on key metrics like the HTTP method, URL, status code, and response time. It’s ideal for development environments or for basic access logging where you need a quick overview of traffic.
Pino, on the other hand, is built for performance and structured logging. While it can log HTTP requests, it’s not designed to be a direct drop-in middleware replacement for Morgan. Instead, you typically integrate Pino with a request logging library or a custom middleware that uses Pino internally.
Here’s how you might use Pino with a custom middleware for request logging in Express:
const express = require('express');
const pino = require('pino');
const logger = pino({ level: 'info' }); // Configure Pino logger
const app = express();
// Custom middleware to log requests using Pino
app.use((req, res, next) => {
const start = Date.now();
const requestLogger = logger.child({
reqId: Math.random().toString(36).substring(2, 15), // Assign a unique ID to the request
method: req.method,
url: req.url,
});
requestLogger.info('Request started');
res.on('finish', () => {
const duration = Date.now() - start;
requestLogger.info({
res: { statusCode: res.statusCode },
duration: `${duration}ms`
}, 'Request finished');
});
next();
});
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
logger.info('Server listening on port 3000');
});
When a request comes in, this custom middleware logs a "Request started" message with a unique reqId. When the response is finished, it logs a "Request finished" message, including the status code and duration. The output, by default, is JSON:
{"level":30,"time":1678886400000,"pid":12345,"hostname":"my-host","reqId":"a1b2c3d4e5f6","method":"GET","url":"/","msg":"Request started"}
{"level":30,"time":1678886400010,"pid":12345,"hostname":"my-host","reqId":"a1b2c3d4e5f6","method":"GET","url":"/","res":{"statusCode":200},"duration":"10ms","msg":"Request finished"}
Pino’s JSON output is machine-readable and designed for high throughput. It uses a technique called "extreme optimization" to achieve logging speeds that are orders of magnitude faster than many other libraries, making it suitable for high-traffic applications where logging overhead needs to be minimized. The ability to child loggers allows you to pass down context (like reqId, method, url) to all log messages associated with a specific request, ensuring that you can easily filter and correlate logs later.
The core difference is how they are used. Morgan is middleware for logging requests. Pino is a high-performance logger that can be used to log requests, often via a separate middleware layer that orchestrates the logging. If you need simple, human-readable request logs in development, Morgan is your go-to. If you need fast, structured, and scalable logging for production, you’ll likely use Pino, potentially with a custom middleware or another library that integrates Pino for request logging.
The real power of Pino for request logging comes when you start to leverage its structured logging capabilities for more advanced analysis. Instead of just seeing a status code, you can log the entire request and response payload (carefully, of course, to avoid sensitive data) or specific business-level events that occur during the request lifecycle. This allows for deep debugging and performance profiling that goes far beyond what a simple access log can provide.
The next step is often integrating a distributed tracing system with your Pino logs, allowing you to track requests across multiple services.