Skip to content

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