Morgan
HTTP request logging middleware specifically for Express.js. Specialized in access log generation, providing predefined formats like common, combined, and dev. Often used in combination with other logging libraries (Winston or Pino).
GitHub Overview
expressjs/morgan
HTTP request logger middleware for node.js
Topics
Star History
Library
Morgan
Overview
Morgan is a specialized HTTP request logging middleware designed exclusively for Express.js applications. It focuses on generating comprehensive access logs for web applications, providing predefined formats like common, combined, and dev. Frequently used in combination with other logging libraries such as Winston and Pino, Morgan has established itself as the de facto standard for HTTP monitoring in Express.js applications, delivering essential request tracking and performance monitoring capabilities.
Details
Morgan 2025 edition maintains its indispensable position within the Express.js ecosystem. Operating as a lightweight middleware, it efficiently captures detailed HTTP request information including request methods, URLs, status codes, response times, IP addresses, and user agents. The custom token functionality enables creation of unique log formats, while supporting file output and integration with external logging services. With its minimal overhead design and seamless Express.js integration, Morgan continues to be the preferred choice for HTTP request logging in production Node.js applications.
Key Features
- Predefined Formats: Multiple format options including Apache Common/Combined, dev, short, and tiny
- Custom Tokens: Ability to define custom log fields and data extraction
- Conditional Logging: Selective logging based on request criteria and conditions
- Stream Support: Direct output to files, cloud services, and external systems
- Lightweight Design: Minimal performance overhead with high-speed operation
- Express.js Integration: Seamless middleware integration with Express applications
Pros and Cons
Pros
- Simple setup and immediate usability in Express.js applications
- Rich predefined formats enable rapid deployment and configuration
- Custom token functionality provides flexible log customization options
- Excellent compatibility and integration with major logging libraries
- Minimal performance impact with lightweight, efficient implementation
- Long-term stability and reliability with extensive production usage
Cons
- Limited to Express.js framework, incompatible with other Node.js frameworks
- Basic HTTP logging functionality only, lacks advanced features independently
- Performance constraints under extremely high traffic loads
- Log analysis capabilities require external tools and services
- Missing real-time monitoring and alerting functionality
- Limited complex filtering and conditional logic capabilities
Reference Pages
Usage Examples
Installation and Basic Setup
# Install Morgan
npm install morgan
# Install with Express.js
npm install express morgan
# TypeScript type definitions
npm install --save-dev @types/morgan
# Verify installation
node -p "require('morgan')"
Basic Usage with Predefined Formats
const express = require('express');
const morgan = require('morgan');
const app = express();
// Development format (colored output)
app.use(morgan('dev'));
// GET / 200 51.267 ms - 2
// Apache Combined Log Format (production recommended)
app.use(morgan('combined'));
// 127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET / HTTP/1.1" 200 2326 "-" "Mozilla/5.0..."
// Apache Common Log Format
app.use(morgan('common'));
// 127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET / HTTP/1.1" 200 2326
// Short format
app.use(morgan('short'));
// 127.0.0.1 - GET / HTTP/1.1 200 2326 - 1.234 ms
// Minimal format
app.use(morgan('tiny'));
// GET / 200 2326 - 1.234 ms
// Basic routes
app.get('/', (req, res) => {
res.send('Hello, Morgan Logging!');
});
app.get('/api/users', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Custom Formats and Tokens
const express = require('express');
const morgan = require('morgan');
const app = express();
// Define custom tokens
morgan.token('id', (req) => {
return req.headers['x-request-id'] || 'unknown';
});
morgan.token('host', (req) => {
return req.hostname;
});
morgan.token('body', (req) => {
return JSON.stringify(req.body);
});
morgan.token('query', (req) => {
return JSON.stringify(req.query);
});
// Custom format string
const customFormat = ':method :host :url :status :res[content-length] - :response-time ms [ID: :id]';
app.use(express.json()); // Body parser
app.use(morgan(customFormat));
// More detailed custom format
const detailedFormat = [
':method :url',
'Status: :status',
'Size: :res[content-length]',
'Time: :response-time ms',
'Host: :host',
'User-Agent: :user-agent',
'Query: :query'
].join(' | ');
app.use(morgan(detailedFormat));
// Function-based custom format
morgan.format('detailed', (tokens, req, res) => {
return [
new Date().toISOString(),
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
tokens.res(req, res, 'content-length'), '-',
tokens['response-time'](req, res), 'ms',
'IP:', tokens['remote-addr'](req, res),
'Referrer:', tokens.referrer(req, res)
].join(' ');
});
app.use(morgan('detailed'));
app.get('/custom', (req, res) => {
res.json({ message: 'Custom logging example' });
});
app.listen(3000);
File Output and Log Rotation
const express = require('express');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream'); // npm install rotating-file-stream
const app = express();
// Create log directory
const logDir = path.join(__dirname, 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
// Simple file output
const accessLogStream = fs.createWriteStream(path.join(logDir, 'access.log'), { flags: 'a' });
app.use(morgan('combined', { stream: accessLogStream }));
// Daily rotation
const dailyLogStream = rfs.createStream('access.log', {
interval: '1d', // Rotate daily
path: logDir,
compress: 'gzip' // Compress old logs
});
app.use(morgan('combined', { stream: dailyLogStream }));
// Size-based rotation
const sizeBasedStream = rfs.createStream('access.log', {
size: '10M', // Rotate every 10MB
maxFiles: 5, // Keep maximum 5 files
path: logDir
});
app.use(morgan('combined', { stream: sizeBasedStream }));
// Multiple outputs (console + file)
app.use(morgan('dev')); // Console output
app.use(morgan('combined', { stream: accessLogStream })); // File output
// Conditional logging
app.use(morgan('combined', {
stream: accessLogStream,
skip: (req, res) => {
// Skip successful responses (2xx) from file logging
return res.statusCode < 400;
}
}));
// Error-only log stream
const errorLogStream = fs.createWriteStream(path.join(logDir, 'error.log'), { flags: 'a' });
app.use(morgan('combined', {
stream: errorLogStream,
skip: (req, res) => res.statusCode < 400 // Only 4xx, 5xx errors
}));
app.get('/error', (req, res) => {
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(3000);
Integration with Winston and Pino
const express = require('express');
const morgan = require('morgan');
const winston = require('winston');
const pino = require('pino');
const app = express();
// Winston integration
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
new winston.transports.Console()
]
});
// Winston stream
const winstonStream = {
write: (message) => {
logger.info(message.trim());
}
};
app.use(morgan('combined', { stream: winstonStream }));
// Pino integration
const pinoLogger = pino({
level: 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true
}
}
});
const pinoStream = {
write: (message) => {
pinoLogger.info(message.trim());
}
};
app.use(morgan('combined', { stream: pinoStream }));
// Structured logging integration
morgan.format('json', (tokens, req, res) => {
return JSON.stringify({
timestamp: new Date().toISOString(),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: Number(tokens.status(req, res)),
contentLength: tokens.res(req, res, 'content-length'),
responseTime: Number(tokens['response-time'](req, res)),
remoteAddr: tokens['remote-addr'](req, res),
userAgent: tokens['user-agent'](req, res),
referrer: tokens.referrer(req, res)
});
});
const structuredStream = {
write: (message) => {
const logData = JSON.parse(message);
logger.info('HTTP Request', logData);
}
};
app.use(morgan('json', { stream: structuredStream }));
app.get('/winston', (req, res) => {
res.json({ logger: 'winston' });
});
app.get('/pino', (req, res) => {
res.json({ logger: 'pino' });
});
app.listen(3000);
Advanced Configuration and Filtering
const express = require('express');
const morgan = require('morgan');
const app = express();
// Environment-based configuration
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
// Development: detailed colored output
app.use(morgan('dev'));
} else if (isProduction) {
// Production: structured logs
app.use(morgan('combined'));
}
// Conditional logging
app.use(morgan('combined', {
skip: (req, res) => {
// Skip health check endpoints
if (req.url === '/health') return true;
// Skip static files
if (req.url.match(/\.(css|js|png|jpg|ico)$/)) return true;
// Skip 2xx responses (error-only logging)
if (res.statusCode >= 200 && res.statusCode < 300) return true;
return false;
}
}));
// IP address filtering
app.use(morgan('combined', {
skip: (req) => {
const clientIP = req.ip || req.connection.remoteAddress;
const skipIPs = ['127.0.0.1', '::1', '10.0.0.0']; // Skip internal IPs
return skipIPs.includes(clientIP);
}
}));
// Response time-based conditional logging
app.use(morgan('combined', {
skip: (req, res) => {
const responseTime = parseFloat(res.get('X-Response-Time')) || 0;
return responseTime < 100; // Skip requests faster than 100ms
}
}));
// Custom filter functions
const isAPIRequest = (req) => req.url.startsWith('/api/');
const isStaticRequest = (req) => req.url.match(/\.(css|js|png|jpg|ico|woff|woff2)$/);
// API-only logger
app.use(morgan('detailed', {
skip: (req) => !isAPIRequest(req)
}));
// Static files logger (simplified)
app.use(morgan('tiny', {
skip: (req) => !isStaticRequest(req)
}));
// Security logging (login attempts, etc.)
morgan.format('security', (tokens, req, res) => {
return JSON.stringify({
type: 'security',
timestamp: new Date().toISOString(),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: tokens.status(req, res),
ip: tokens['remote-addr'](req, res),
userAgent: tokens['user-agent'](req, res),
suspicious: res.statusCode === 401 || res.statusCode === 403
});
});
app.use('/auth', morgan('security'));
// Rate limiting logs
app.use(morgan('combined', {
skip: (req, res) => res.statusCode !== 429 // Only Too Many Requests
}));
app.get('/api/data', (req, res) => {
res.json({ data: 'API response' });
});
app.get('/health', (req, res) => {
res.json({ status: 'OK' });
});
app.listen(3000);
Analytics and Metrics Collection
const express = require('express');
const morgan = require('morgan');
const fs = require('fs');
const app = express();
// Metrics collection
let requestCount = 0;
let totalResponseTime = 0;
let errorCount = 0;
morgan.token('metrics', (req, res) => {
requestCount++;
const responseTime = parseFloat(res.get('X-Response-Time')) || 0;
totalResponseTime += responseTime;
if (res.statusCode >= 400) {
errorCount++;
}
return `[Metrics: requests=${requestCount}, avgTime=${(totalResponseTime/requestCount).toFixed(2)}ms, errors=${errorCount}]`;
});
app.use(morgan(':method :url :status :metrics'));
// Performance analysis
morgan.format('performance', (tokens, req, res) => {
const responseTime = parseFloat(tokens['response-time'](req, res));
const contentLength = parseInt(tokens.res(req, res, 'content-length') || '0');
return JSON.stringify({
url: tokens.url(req, res),
method: tokens.method(req, res),
status: parseInt(tokens.status(req, res)),
responseTime: responseTime,
contentLength: contentLength,
throughput: contentLength / responseTime, // bytes per ms
slow: responseTime > 1000, // Mark as slow if > 1 second
timestamp: new Date().toISOString()
});
});
const performanceLogStream = fs.createWriteStream('logs/performance.log', { flags: 'a' });
app.use(morgan('performance', { stream: performanceLogStream }));
// Statistics reporting
setInterval(() => {
const avgResponseTime = requestCount > 0 ? (totalResponseTime / requestCount).toFixed(2) : 0;
const errorRate = requestCount > 0 ? ((errorCount / requestCount) * 100).toFixed(2) : 0;
console.log(`
=== Server Statistics ===
Total Requests: ${requestCount}
Average Response Time: ${avgResponseTime}ms
Error Rate: ${errorRate}%
Total Errors: ${errorCount}
========================
`);
}, 60000); // Every minute
// User agent analysis
const userAgentStats = {};
morgan.token('user-agent-stats', (req) => {
const ua = req.get('User-Agent') || 'Unknown';
const browser = ua.includes('Chrome') ? 'Chrome' :
ua.includes('Firefox') ? 'Firefox' :
ua.includes('Safari') ? 'Safari' : 'Other';
userAgentStats[browser] = (userAgentStats[browser] || 0) + 1;
return `[UA: ${JSON.stringify(userAgentStats)}]`;
});
app.use(morgan(':method :url :user-agent-stats'));
app.get('/stats', (req, res) => {
res.json({
requests: requestCount,
averageResponseTime: (totalResponseTime / requestCount).toFixed(2),
errorRate: ((errorCount / requestCount) * 100).toFixed(2),
userAgents: userAgentStats
});
});
app.listen(3000);