Express Logging Strategy
Implementing a proper logging strategy is essential for any Express application. Good logging helps you debug issues, monitor application health, track user activity, and gather valuable insights about how your application is being used. In this guide, we'll explore how to set up and configure logging in your Express applications, following industry best practices.
Why Logging Matters
Before diving into implementation, let's understand why logging is crucial:
- Debugging - Logs help identify the source of errors and exceptions
- Monitoring - Track application performance and identify bottlenecks
- Security - Detect suspicious activities and potential security threats
- Auditing - Maintain records of important operations for compliance
- Analytics - Gather insights about application usage and user behavior
Basic Logging with Morgan
The most popular logging middleware for Express is Morgan. It's simple to set up and provides a good starting point for HTTP request logging.
Installation
npm install morgan
Basic Usage
const express = require('express');
const morgan = require('morgan');
const app = express();
// Use morgan with predefined format
app.use(morgan('dev'));
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
When you run this application and make requests, you'll see log output like:
GET / 200 5.311 ms - 11
This indicates:
- HTTP method (
GET
) - Path (
/
) - Status code (
200
) - Response time (
5.311 ms
) - Response size (
11 bytes
)
Morgan Predefined Formats
Morgan comes with several predefined formats:
tiny
: The minimal outputdev
: Colored by response status for developmentcommon
: Standard Apache common log formatcombined
: Standard Apache combined log formatshort
: Shorter than default, includes response time
Example with the combined
format:
app.use(morgan('combined'));
Output:
127.0.0.1 - - [01/Jan/2023:12:34:56 +0000] "GET / HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
Creating Custom Log Formats
Morgan allows you to create custom log formats to capture exactly what you need.
// Custom token for request body
morgan.token('body', (req) => JSON.stringify(req.body));
// Custom format
app.use(morgan(':method :url :status :res[content-length] - :response-time ms - :body'));
Output:
POST /users 201 42 - 10.291 ms - {"name":"John","email":"[email protected]"}
Advanced Logging with Winston
For production applications, you'll want more robust logging capabilities. Winston is a versatile logging library that offers multiple transport options and log levels.
Installation
npm install winston
Basic Winston Setup
const winston = require('winston');
// Create a logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
// Console transport
new winston.transports.Console(),
// File transport for errors
new winston.transports.File({ filename: 'error.log', level: 'error' }),
// File transport for all logs
new winston.transports.File({ filename: 'combined.log' })
]
});
Integrating Winston with Express
You can integrate Winston with Express using middleware:
const express = require('express');
const winston = require('winston');
const app = express();
// Create logger (same as above)
const logger = winston.createLogger({
// configuration from previous example
});
// Middleware to log all requests
app.use((req, res, next) => {
const start = Date.now();
// Once the request is processed
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
userAgent: req.headers['user-agent']
});
});
next();
});
// Example route that logs
app.get('/', (req, res) => {
logger.info('Homepage was accessed');
res.send('Hello World');
});
// Error handling route
app.get('/error', (req, res, next) => {
try {
// Simulate an error
throw new Error('Something went wrong');
} catch (error) {
logger.error('Error occurred', { error: error.message, stack: error.stack });
next(error);
}
});
app.listen(3000, () => {
logger.info('Server started on port 3000');
});
The above code will create log entries like:
{"level":"info","message":"Server started on port 3000","timestamp":"2023-01-01T12:34:56.789Z"}
{"level":"info","message":{"method":"GET","url":"/","status":200,"duration":"5ms","userAgent":"Mozilla/5.0"},"timestamp":"2023-01-01T12:35:01.234Z"}
{"level":"info","message":"Homepage was accessed","timestamp":"2023-01-01T12:35:01.237Z"}
Request ID Tracking
For distributed systems, tracking requests across services is important. You can add a unique request ID to each incoming request:
const { v4: uuidv4 } = require('uuid');
// Add request ID middleware
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// Update your logging middleware to include the request ID
app.use((req, res, next) => {
// Log with request ID
logger.info({
requestId: req.id,
method: req.method,
url: req.url
});
next();
});
Error Logging
Proper error logging is crucial for troubleshooting. Here's how to set up global error handling:
// Error handling middleware (must be the last middleware)
app.use((err, req, res, next) => {
logger.error({
requestId: req.id,
message: 'Unhandled error',
error: err.message,
stack: err.stack,
method: req.method,
url: req.url
});
res.status(500).send('Internal Server Error');
});
Logging Sensitive Data
Be cautious about what you log. Never log sensitive information like:
- Passwords
- API keys
- Credit card numbers
- Personal identifiable information (PII)
Here's how to sanitize request data before logging:
const sanitizeRequest = (req) => {
const sanitized = {
method: req.method,
url: req.url,
headers: { ...req.headers }
};
// Remove sensitive headers
if (sanitized.headers.authorization) {
sanitized.headers.authorization = '[REDACTED]';
}
// Clone and sanitize body if exists
if (req.body) {
sanitized.body = { ...req.body };
if (sanitized.body.password) {
sanitized.body.password = '[REDACTED]';
}
if (sanitized.body.creditCard) {
sanitized.body.creditCard = '[REDACTED]';
}
}
return sanitized;
};
// Use in your logging middleware
app.use((req, res, next) => {
logger.info({
request: sanitizeRequest(req)
});
next();
});
Production Logging Best Practices
For production environments, consider these best practices:
- Use appropriate log levels: Debug for development, Info/Warning/Error for production
- Implement log rotation: To prevent logs from consuming all disk space
- Centralize logs: Use services like ELK Stack (Elasticsearch, Logstash, Kibana) or cloud services
- Structure logs as JSON: Makes them machine-readable and easier to parse
- Include contextual information: Request IDs, user IDs, timestamps
- Set up alerts: For critical errors and abnormal patterns
Example: Setting Up Log Rotation with Winston
const { createLogger, format, transports } = require('winston');
require('winston-daily-rotate-file');
const logger = createLogger({
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new transports.Console(),
new transports.DailyRotateFile({
filename: 'application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d'
})
]
});
Complete Example: A Production-Ready Logging Setup
Here's a complete example that brings everything together:
const express = require('express');
const { createLogger, format, transports } = require('winston');
const { v4: uuidv4 } = require('uuid');
require('winston-daily-rotate-file');
// Create Express app
const app = express();
app.use(express.json());
// Environment configuration
const isProduction = process.env.NODE_ENV === 'production';
// Create Winston logger
const logger = createLogger({
level: isProduction ? 'info' : 'debug',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'user-service' },
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message, ...meta }) => {
return `${timestamp} ${level}: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
})
)
})
]
});
// In production, add file rotation
if (isProduction) {
logger.add(new transports.DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '10m',
maxFiles: '14d'
}));
logger.add(new transports.DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '10m',
maxFiles: '30d'
}));
}
// Request ID middleware
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// Data sanitizer for logging
const sanitizeData = (data) => {
if (!data) return data;
const sanitized = { ...data };
const sensitiveFields = ['password', 'token', 'creditCard', 'ssn'];
sensitiveFields.forEach(field => {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
});
return sanitized;
};
// Request logger middleware
app.use((req, res, next) => {
const startTime = Date.now();
// Log request
logger.debug(`Request received: ${req.method} ${req.url}`, {
requestId: req.id,
method: req.method,
url: req.url,
query: req.query,
body: sanitizeData(req.body),
headers: {
'user-agent': req.headers['user-agent'],
'content-type': req.headers['content-type']
}
});
// Log response when finished
res.on('finish', () => {
const duration = Date.now() - startTime;
const level = res.statusCode >= 400 ? 'warn' : 'info';
logger[level](`Request completed: ${req.method} ${req.url} ${res.statusCode}`, {
requestId: req.id,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`
});
});
next();
});
// Example routes
app.get('/', (req, res) => {
logger.info('Processing home request', { requestId: req.id });
res.send('Hello World');
});
app.post('/users', (req, res) => {
logger.info('Creating new user', { requestId: req.id, user: sanitizeData(req.body) });
// User creation logic here
res.status(201).json({ message: 'User created' });
});
app.get('/error', (req, res, next) => {
next(new Error('This is a test error'));
});
// Error handling middleware
app.use((err, req, res, next) => {
logger.error('Unhandled error', {
requestId: req.id,
error: err.message,
stack: err.stack,
method: req.method,
url: req.url
});
res.status(500).json({
error: 'Internal Server Error',
requestId: req.id
});
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server started on port ${PORT}`);
});
Testing Your Logging
It's important to verify your logging works correctly:
- Check normal request logging
- Test error logging
- Verify log rotation (if configured)
- Review log format and content
Summary
Implementing a robust logging strategy is essential for any Express application, especially as it grows in complexity. Good logging provides visibility into your application's behavior, helps with debugging, and supports monitoring and maintenance.
Key points to remember:
- Use appropriate tools - Morgan for basic HTTP logging, Winston for advanced logging
- Structure your logs - Use consistent formats, preferably JSON
- Include context - Request IDs, timestamps, and relevant metadata
- Be security-conscious - Avoid logging sensitive information
- Plan for scale - Implement log rotation and centralized logging for production
- Use appropriate log levels - To control verbosity based on environment
By following these practices, you'll build a logging system that provides valuable insights while ensuring your application remains maintainable and secure.
Additional Resources
- Morgan Documentation
- Winston Documentation
- ELK Stack for Centralized Logging
- 12-Factor App: Logs
- OWASP Logging Cheat Sheet
Exercises
- Implement a basic Express server with Morgan and test different log formats
- Set up Winston with different log levels and multiple transports
- Create a middleware that logs performance metrics for slow requests (e.g., those taking longer than 500ms)
- Implement a system that logs detailed information for errors but minimal info for successful requests
- Create a log analyzer that can parse your logs and generate simple reports
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)