Morgan Advanced Patterns (80-20)#
Advanced logging patterns and production setups.
Log Rotation with rotating-file-stream#
npm install rotating-file-stream
const morgan = require('morgan')
const rfs = require('rotating-file-stream')
const path = require('path')
// Create rotating stream
const accessLogStream = rfs.createStream('access.log', {
interval: '1d', // Rotate daily
size: '10M', // Rotate when file reaches 10MB
path: path.join(__dirname, 'logs'),
compress: 'gzip', // Compress rotated files
maxFiles: 30, // Keep 30 days of logs
})
app.use(morgan('combined', { stream: accessLogStream }))
JSON Logging for Production#
const morgan = require('morgan')
// Custom JSON format
morgan.token('json', (req, res) => {
return JSON.stringify({
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
status: res.statusCode,
responseTime: parseFloat(res.getHeader('X-Response-Time')),
userAgent: req.get('user-agent'),
ip: req.ip,
userId: req.user?.id,
})
})
app.use(morgan(':json'))
Output:
{"timestamp":"2025-11-26T15:30:15.123Z","method":"GET","url":"/api/users","status":200,"responseTime":12.34,"userAgent":"Mozilla/5.0...","ip":"127.0.0.1","userId":"user123"}
Structured Logging with Winston#
npm install winston morgan
const morgan = require('morgan')
const winston = require('winston')
// Create Winston logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
})
// Morgan + Winston stream
const stream = {
write: (message) => {
logger.info(message.trim())
},
}
app.use(morgan('combined', { stream }))
Request ID Tracking#
const { v4: uuidv4 } = require('uuid')
const morgan = require('morgan')
// Add request ID middleware
app.use((req, res, next) => {
req.id = uuidv4()
res.setHeader('X-Request-ID', req.id)
next()
})
// Custom token for request ID
morgan.token('request-id', (req) => req.id)
// Use in format
app.use(morgan(':request-id :method :url :status :response-time ms'))
Output:
a1b2c3d4-e5f6-7890-abcd-ef1234567890 GET /api/users 200 12.34 ms
Performance Monitoring#
morgan.token('total-time', (req, res) => {
if (!req._startTime) return '-'
const diff = process.hrtime(req._startTime)
return (diff[0] * 1e3 + diff[1] * 1e-6).toFixed(2)
})
app.use((req, res, next) => {
req._startTime = process.hrtime()
next()
})
app.use(morgan(':method :url :status :total-time ms'))
Skip Health Checks & Static Files#
const skipPaths = ['/health', '/metrics', '/favicon.ico']
app.use(morgan('combined', {
skip: (req, res) =>
skipPaths.some(path => req.url.startsWith(path)) ||
req.url.startsWith('/static') ||
res.statusCode < 400 // Only log errors
}))
Log Request/Response Body#
morgan.token('req-body', (req) => {
// Only log for specific routes
if (req.url.startsWith('/api/')) {
const sanitized = { ...req.body }
delete sanitized.password
delete sanitized.token
return JSON.stringify(sanitized)
}
return '-'
})
morgan.token('res-body', (req, res) => {
return res.locals.body ? JSON.stringify(res.locals.body) : '-'
})
// Middleware to capture response
app.use((req, res, next) => {
const originalSend = res.send
res.send = function(data) {
res.locals.body = data
return originalSend.call(this, data)
}
next()
})
app.use(morgan(':method :url :status - Req: :req-body Res: :res-body'))
Environment-Based Setup#
const morgan = require('morgan')
const rfs = require('rotating-file-stream')
const path = require('path')
function setupLogging(app) {
if (process.env.NODE_ENV === 'production') {
// Production: JSON logs to rotating files
const accessLog = rfs.createStream('access.log', {
interval: '1d',
path: path.join(__dirname, '../logs'),
compress: 'gzip',
})
const errorLog = rfs.createStream('error.log', {
interval: '1d',
path: path.join(__dirname, '../logs'),
compress: 'gzip',
})
morgan.token('json', (req, res) => {
return JSON.stringify({
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
status: res.statusCode,
responseTime: parseFloat(res.getHeader('X-Response-Time')),
ip: req.ip,
userAgent: req.get('user-agent'),
})
})
// All requests
app.use(morgan(':json', {
stream: accessLog,
skip: (req, res) => res.statusCode >= 400
}))
// Errors only
app.use(morgan(':json', {
stream: errorLog,
skip: (req, res) => res.statusCode < 400
}))
} else if (process.env.NODE_ENV === 'development') {
// Development: colored console output
app.use(morgan('dev'))
} else {
// Test: silent
app.use(morgan('dev', {
skip: () => true
}))
}
}
module.exports = { setupLogging }
Custom Format Examples#
API-specific Format#
app.use(morgan(':method :url :status :res[content-length] - :response-time ms - :remote-addr'))
Detailed Debug Format#
const debugFormat = [
':date[iso]',
':method',
':url',
':status',
':response-time ms',
'- User: :user-id',
'- IP: :remote-addr',
'- UA: :user-agent',
].join(' ')
app.use(morgan(debugFormat))
Log to Database#
const morgan = require('morgan')
const stream = {
write: async (message) => {
const parts = message.trim().split(' ')
await db.logs.create({
timestamp: new Date(),
method: parts[0],
url: parts[1],
status: parseInt(parts[2]),
responseTime: parseFloat(parts[3]),
})
},
}
app.use(morgan(':method :url :status :response-time', { stream }))
Complete Production Example#
import express from 'express'
import morgan from 'morgan'
import rfs from 'rotating-file-stream'
import { v4 as uuidv4 } from 'uuid'
import path from 'path'
const app = express()
// Request ID
app.use((req, res, next) => {
req.id = uuidv4()
res.setHeader('X-Request-ID', req.id)
next()
})
// Rotating file stream
const logStream = rfs.createStream('access.log', {
interval: '1d',
size: '10M',
path: path.join(__dirname, '../logs'),
compress: 'gzip',
maxFiles: 30,
})
// Custom tokens
morgan.token('request-id', (req: any) => req.id)
morgan.token('user-id', (req: any) => req.user?.id || 'anonymous')
// Production logging
if (process.env.NODE_ENV === 'production') {
morgan.token('json-log', (req: any, res) => {
return JSON.stringify({
timestamp: new Date().toISOString(),
requestId: req.id,
method: req.method,
url: req.url,
status: res.statusCode,
responseTime: parseFloat(res.getHeader('X-Response-Time') || '0'),
userId: req.user?.id,
ip: req.ip,
userAgent: req.get('user-agent'),
})
})
app.use(morgan(':json-log', {
stream: logStream,
skip: (req, res) => req.url === '/health'
}))
} else {
// Development
app.use(morgan('dev'))
}
app.listen(3000)
Best Practices
- Use structured (JSON) logging in production
- Rotate log files to prevent disk space issues
- Skip logging health checks and static assets
- Never log sensitive data (passwords, tokens)
- Include request IDs for tracing
- Separate error logs from access logs