Skip to main content

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:

  1. Debugging - Logs help identify the source of errors and exceptions
  2. Monitoring - Track application performance and identify bottlenecks
  3. Security - Detect suspicious activities and potential security threats
  4. Auditing - Maintain records of important operations for compliance
  5. 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

bash
npm install morgan

Basic Usage

javascript
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 output
  • dev: Colored by response status for development
  • common: Standard Apache common log format
  • combined: Standard Apache combined log format
  • short: Shorter than default, includes response time

Example with the combined format:

javascript
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.

javascript
// 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

bash
npm install winston

Basic Winston Setup

javascript
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:

javascript
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:

json
{"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:

javascript
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:

javascript
// 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:

javascript
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:

  1. Use appropriate log levels: Debug for development, Info/Warning/Error for production
  2. Implement log rotation: To prevent logs from consuming all disk space
  3. Centralize logs: Use services like ELK Stack (Elasticsearch, Logstash, Kibana) or cloud services
  4. Structure logs as JSON: Makes them machine-readable and easier to parse
  5. Include contextual information: Request IDs, user IDs, timestamps
  6. Set up alerts: For critical errors and abnormal patterns

Example: Setting Up Log Rotation with Winston

javascript
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:

javascript
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:

  1. Check normal request logging
  2. Test error logging
  3. Verify log rotation (if configured)
  4. 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:

  1. Use appropriate tools - Morgan for basic HTTP logging, Winston for advanced logging
  2. Structure your logs - Use consistent formats, preferably JSON
  3. Include context - Request IDs, timestamps, and relevant metadata
  4. Be security-conscious - Avoid logging sensitive information
  5. Plan for scale - Implement log rotation and centralized logging for production
  6. 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

Exercises

  1. Implement a basic Express server with Morgan and test different log formats
  2. Set up Winston with different log levels and multiple transports
  3. Create a middleware that logs performance metrics for slow requests (e.g., those taking longer than 500ms)
  4. Implement a system that logs detailed information for errors but minimal info for successful requests
  5. 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! :)