Logging with Morgan and Winston
Logging records what your application does at runtime — which requests arrive, what errors occur, how long operations take, and what the system state is when something goes wrong. Without logs, debugging production issues means guessing. With structured logs, you can trace every problem back to its root cause in minutes. This topic covers two complementary logging tools: Morgan for HTTP request logging and Winston for application-level logging.
Why Logging Matters
Without logs (production error report):
"Users are complaining that login doesn't work"
→ You have no idea what's happening or when it started
With logs (production error report):
[2024-03-15 14:22:11] ERROR: MongoServerError: connection timed out
at POST /api/auth/login
userId: undefined, ip: 203.0.113.42
Duration: 30042ms
→ You immediately know: database connection dropped at 14:22
Two Layers of Logging
┌───────────────────────────────────────────────────────────────┐ │ Express Logging Layers │ ├─────────────────────┬─────────────────────────────────────────┤ │ Morgan │ Logs every HTTP request automatically │ │ (HTTP layer) │ Method, URL, status, size, response time│ ├─────────────────────┼─────────────────────────────────────────┤ │ Winston │ Logs application events you choose │ │ (App layer) │ Errors, warnings, info, debug messages │ └─────────────────────┴─────────────────────────────────────────┘
Morgan: HTTP Request Logging
Install and Basic Setup
npm install morgan
const morgan = require('morgan');
// 'dev' format: colored output, great for development
app.use(morgan('dev'));
// Output: GET /api/users 200 45.234 ms - 512
Morgan Format Options
┌──────────────────────────────────────────────────────────────────┐ │ Morgan Format Options │ ├────────────────┬─────────────────────────────────────────────────┤ │ 'dev' │ Colored, concise — for development terminal │ │ 'tiny' │ Minimal — method, URL, status, size │ │ 'short' │ Shorter than 'combined', includes res time │ │ 'combined' │ Apache standard format — best for production │ │ 'common' │ Apache common log format │ └────────────────┴─────────────────────────────────────────────────┘
// Use 'combined' in production (includes IP, user-agent, referrer)
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
} else {
app.use(morgan('dev'));
}
Write Morgan Logs to a File
const fs = require('fs');
const path = require('path');
// Create a write stream (append mode)
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'logs', 'access.log'),
{ flags: 'a' }
);
// Write all HTTP logs to file
app.use(morgan('combined', { stream: accessLogStream }));
Custom Morgan Tokens
// Log the user ID alongside each request
morgan.token('user-id', (req) => {
return req.user ? req.user._id.toString() : 'anonymous';
});
app.use(morgan(':method :url :status :response-time ms — user: :user-id'));
// Output: POST /api/orders 201 32.4 ms — user: 64a1b2c3d4e5f6
Winston: Application Logging
Winston handles structured logging for events your application generates: errors, business events, warnings, and debug information. Unlike Morgan (which logs HTTP only), Winston logs anything you tell it to.
Install Winston
npm install winston
Create a Logger Configuration
// utils/logger.js
const winston = require('winston');
const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
// Write error-level logs to error.log
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error'
}),
// Write all logs to combined.log
new winston.transports.File({
filename: path.join('logs', 'combined.log')
})
]
});
// In development, also print colorized output to terminal
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
module.exports = logger;
Winston Log Levels
┌──────────────────────────────────────────────────────────────────┐ │ Winston Log Levels (highest → lowest priority) │ ├───────────────┬──────────────────────────────────────────────────┤ │ error (0) │ Application errors that need immediate attention │ │ warn (1) │ Something unexpected but app keeps running │ │ info (2) │ Normal operational events (user logged in, etc.) │ │ http (3) │ HTTP request details │ │ verbose (4) │ Detailed operational data │ │ debug (5) │ Debugging info for development │ │ silly (6) │ Everything — very verbose │ └───────────────┴──────────────────────────────────────────────────┘ Setting level: 'info' logs everything from error through info. Setting level: 'debug' logs everything.
Using the Logger in Routes
const logger = require('./utils/logger');
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password))) {
logger.warn('Failed login attempt', {
email,
ip: req.ip,
userAgent: req.headers['user-agent']
});
return res.status(401).json({ error: 'Invalid credentials' });
}
logger.info('User logged in', {
userId: user._id,
email: user.email,
ip: req.ip
});
const token = generateToken(user._id);
res.json({ token });
} catch (err) {
logger.error('Login error', {
error: err.message,
stack: err.stack,
ip: req.ip
});
res.status(500).json({ error: 'Login failed' });
}
});
Integrate Winston with Express Error Handler
const logger = require('./utils/logger');
// Global error handler with Winston logging
app.use((err, req, res, next) => {
logger.error('Unhandled error', {
message: err.message,
stack: err.stack,
method: req.method,
url: req.originalUrl,
ip: req.ip,
userId: req.user?._id
});
res.status(err.statusCode || 500).json({
success: false,
message: err.isOperational ? err.message : 'Something went wrong'
});
});
Route Morgan Through Winston
For a fully unified logging system, pipe Morgan's output through Winston instead of printing directly:
const morganMiddleware = morgan('combined', {
stream: {
write: (message) => logger.http(message.trim())
}
});
app.use(morganMiddleware);
// Now HTTP access logs appear in combined.log alongside app logs
Log File Structure
my-express-app/ ├── logs/ │ ├── access.log ← Morgan HTTP logs │ ├── error.log ← Winston error-level only │ └── combined.log ← Winston all levels ├── app.js └── package.json
Add the logs folder to .gitignore:
node_modules/ .env logs/
Log Rotation
Log files grow indefinitely without rotation. Use winston-daily-rotate-file to automatically create new files daily and delete old ones:
npm install winston-daily-rotate-file
const DailyRotateFile = require('winston-daily-rotate-file');
const transport = new DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxFiles: '14d', // Keep logs for 14 days, then delete
maxSize: '20m' // Rotate when file reaches 20MB
});
logger.add(transport);
What to Log and What Not to Log
┌─────────────────────────────────────────────────────────────────┐ │ Logging Best Practices │ ├─────────────────────────────┬───────────────────────────────────┤ │ LOG │ DO NOT LOG │ ├─────────────────────────────┼───────────────────────────────────┤ │ Error messages and stacks │ Passwords (never, ever) │ │ Failed login attempts + IP │ Credit card or payment data │ │ User IDs (not names) │ Session tokens or JWT values │ │ HTTP method and URL │ Personal health information │ │ Response status codes │ Full request bodies with secrets │ │ Database error details │ Government ID numbers │ │ Request duration │ Encryption keys or secrets │ └─────────────────────────────┴───────────────────────────────────┘
Summary
Use Morgan to automatically log every HTTP request — method, URL, status code, and response time — with a single app.use(morgan('dev')) call. Use the 'combined' format in production to include IP addresses and user agents for security auditing. Use Winston for structured application logging: create a logger with file transports for errors and combined logs, and use logger.info(), logger.warn(), and logger.error() throughout your routes and error handlers. Pipe Morgan through Winston to unify both logging systems into the same output files. Add log rotation with winston-daily-rotate-file to prevent log files from growing indefinitely. Never log passwords, tokens, or sensitive personal data.
