Skip to main content

Express Logging Solutions

Introduction

When deploying Express applications to production, proper logging becomes crucial for monitoring application health, troubleshooting issues, and understanding user behavior. Logging provides visibility into what's happening inside your application when it's running in production environments where you don't have direct access to the console.

In this guide, we'll explore various logging solutions for Express applications, from simple built-in options to more advanced configurations using popular logging libraries.

Why Logging Matters in Production

Before diving into implementation details, let's understand why logging is essential:

  1. Troubleshooting: Logs help identify the root cause of errors
  2. Performance Monitoring: Track response times and identify bottlenecks
  3. Security: Detect suspicious activities or unauthorized access attempts
  4. User Behavior: Understand how users interact with your application
  5. Audit Trail: Maintain a record of important operations for compliance

Basic Logging with Console

The simplest form of logging in Express uses JavaScript's built-in console methods:

javascript
app.get('/users', (req, res) => {
console.log(`GET request to /users at ${new Date().toISOString()}`);
// Handle the request...
res.send(users);
});

While this works for development, it has several limitations for production:

  • No log levels (everything is treated with equal importance)
  • No structured format (makes parsing difficult)
  • No persistent storage (logs disappear when the process restarts)
  • Performance concerns (synchronous I/O operations)

Using Morgan for HTTP Request Logging

Morgan is Express's official HTTP request logger middleware, perfect for tracking incoming requests.

Installing Morgan

bash
npm install morgan

Basic Morgan Setup

javascript
const express = require('express');
const morgan = require('morgan');
const app = express();

// Use morgan with the 'combined' format
app.use(morgan('combined'));

app.get('/', (req, res) => {
res.send('Hello World!');
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

Output Example

When a request is made to your application, Morgan will log something like:

::1 - - [10/Nov/2023:12:34:56 +0000] "GET / HTTP/1.1" 200 12 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"

Morgan Predefined Formats

Morgan offers several predefined formats:

  • combined: Standard Apache combined log output
  • common: Standard Apache common log output
  • dev: Colored by response status for development
  • short: Shorter than default, includes response time
  • tiny: The minimal output

For development, the dev format is particularly useful:

javascript
// Development environment
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
} else {
// Production environment
app.use(morgan('combined'));
}

Custom Morgan Log Format

You can create custom log formats:

javascript
// Custom token for request body
morgan.token('body', (req) => JSON.stringify(req.body));

// Custom format
app.use(
morgan(':method :url :status :response-time ms - :body')
);

Advanced Logging with Winston

For more sophisticated logging needs, Winston provides a multi-transport logging library with features like log levels, formatting, and multiple outputs.

Installing Winston

bash
npm install winston

Basic Winston Setup

javascript
const winston = require('winston');

// Create a logger instance
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
// Write all logs to console
new winston.transports.Console(),
// Write all logs error level or below to 'error.log'
new winston.transports.File({ filename: 'error.log', level: 'error' }),
// Write all logs to 'combined.log'
new winston.transports.File({ filename: 'combined.log' }),
],
});

// Example usage
logger.info('Application started');
logger.error('Something went wrong', { error: new Error('Database connection failed') });

Integrating Winston with Express

To use Winston for HTTP request logging (similar to Morgan), you'll need to create custom middleware:

javascript
const express = require('express');
const winston = require('winston');
const app = express();

// Create logger instance
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/app.log' })
]
});

// Create request logging middleware
app.use((req, res, next) => {
const start = Date.now();

// Once the request is processed
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
userAgent: req.get('user-agent')
});
});

next();
});

// Your routes here
app.get('/', (req, res) => {
res.send('Hello World!');
});

app.listen(3000, () => {
logger.info('Server started on port 3000');
});

Winston Log Levels

Winston supports various log levels, in order of increasing priority:

  • error: Something has failed and needs immediate attention
  • warn: Something unexpected happened but doesn't require immediate action
  • info: Important application events (startup, shutdown, configuration)
  • http: HTTP request logging
  • verbose: Detailed information useful for debugging
  • debug: Very detailed debugging information
  • silly: Extremely detailed information

You can configure the minimum level to log:

javascript
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
// Other options...
});

Using the Debug Module

For more selective debugging, the debug module is an excellent lightweight option:

Installing Debug

bash
npm install debug

Basic Debug Usage

javascript
const express = require('express');
const debug = require('debug')('app:server');
const debugRoutes = require('debug')('app:routes');

const app = express();

app.get('/', (req, res) => {
debugRoutes('Processing request to homepage');
res.send('Hello World!');
});

app.listen(3000, () => {
debug('Server started on port 3000');
});

To enable these debug outputs, you would run your application with:

bash
DEBUG=app:* node app.js

This selective approach is perfect for development but less suitable for comprehensive production logging.

Production Logging Best Practices

When setting up logging for production, consider these best practices:

1. Use Different Log Levels Appropriately

javascript
// Example with Winston
if (process.env.NODE_ENV === 'production') {
logger.error('Critical failure', { error }); // Always log errors

if (shouldLogWarning) {
logger.warn('Potential issue detected', { details });
}

// Only log detailed info if enabled by configuration
if (process.env.VERBOSE_LOGGING === 'true') {
logger.info('User action', { userId, action });
logger.debug('Function execution details', { params, result });
}
}

2. Structured Logging with JSON

javascript
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
// Other options...
});

// Results in logs like:
// {"level":"info","message":"User logged in","userId":"123","timestamp":"2023-11-10T12:34:56.789Z"}

3. Separate Logs by Concern

javascript
const errorLogger = winston.createLogger({
level: 'error',
transports: [new winston.transports.File({ filename: 'logs/error.log' })],
// Other options...
});

const accessLogger = winston.createLogger({
level: 'info',
transports: [new winston.transports.File({ filename: 'logs/access.log' })],
// Other options...
});

// Then use them accordingly
errorLogger.error('Application error', { error });
accessLogger.info('User accessed protected resource', { userId, resource });

4. Include Contextual Information

javascript
app.use((req, res, next) => {
// Add request ID to each request
req.requestId = crypto.randomUUID();

// Continue with request processing
next();
});

// Later in your code
logger.info('Processing payment', {
requestId: req.requestId,
userId: req.user?.id,
amount: payment.amount,
currency: payment.currency
});

5. Handle Asynchronous Errors Properly

javascript
app.get('/async-operation', async (req, res, next) => {
try {
const result = await someAsyncOperation();
logger.info('Async operation completed', { result });
res.json(result);
} catch (error) {
logger.error('Async operation failed', {
error: {
message: error.message,
stack: error.stack,
code: error.code
}
});
next(error);
}
});

Rotating Log Files

For production applications, log rotation is essential to prevent log files from growing indefinitely:

bash
npm install winston-daily-rotate-file
javascript
const { createLogger, format, transports } = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

const logger = createLogger({
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
})
]
});

Centralized Logging Solutions

For applications running across multiple servers, consider sending your logs to a centralized logging service:

javascript
// Example with Winston + Loggly
const { Loggly } = require('winston-loggly-bulk');

logger.add(new Loggly({
token: "your-loggly-token",
subdomain: "your-subdomain",
tags: ["Express", "Production"],
json: true
}));

Other popular centralized logging options include:

  • ELK Stack (Elasticsearch, Logstash, Kibana)
  • Graylog
  • Papertrail
  • Datadog

Error Tracking Integration

For production applications, integrate your logging with error tracking services:

javascript
// Example with Sentry
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://[email protected]/0',
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());

// Your routes here

// The error handler must be before any other error middleware and after all controllers
app.use(Sentry.Handlers.errorHandler());

// Regular error handlers
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err });
res.status(500).send('Something broke!');
});

Complete Example: Comprehensive Logging Setup

Here's a complete example combining several logging approaches:

javascript
const express = require('express');
const winston = require('winston');
const morgan = require('morgan');
const { createLogger, format, transports } = winston;
const { combine, timestamp, json, printf } = format;
const DailyRotateFile = require('winston-daily-rotate-file');

// Environment configuration
const isProduction = process.env.NODE_ENV === 'production';
const logLevel = isProduction ? 'info' : 'debug';

// Create custom format
const customFormat = printf(({ level, message, timestamp, ...metadata }) => {
return JSON.stringify({
level,
message,
timestamp,
...metadata
});
});

// Create the logger
const logger = createLogger({
level: logLevel,
format: combine(
timestamp(),
customFormat
),
transports: [
new transports.Console(),
new DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
}),
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d'
})
]
});

// Create Express app
const app = express();

// Create custom Morgan token for response time
morgan.token('response-time-ms', (req, res) => {
return res.responseTime ? `${res.responseTime}ms` : 'unknown';
});

// Stream Morgan logs to Winston
app.use(morgan(
':method :url :status :response-time-ms - :res[content-length] bytes',
{
stream: {
write: (message) => logger.http(message.trim())
}
}
));

// Add responseTime to res object
app.use((req, res, next) => {
// Add unique request ID
req.id = require('crypto').randomUUID();

// Track response time
const start = Date.now();
res.on('finish', () => {
res.responseTime = Date.now() - start;
});

next();
});

// Setup routes
app.get('/', (req, res) => {
logger.debug('Processing request to homepage', { requestId: req.id });
res.send('Hello, world!');
});

app.get('/error', (req, res, next) => {
try {
throw new Error('Example error');
} catch (error) {
logger.error('Error in /error route', {
requestId: req.id,
error: {
message: error.message,
stack: isProduction ? undefined : error.stack
}
});
next(error);
}
});

// Global error handler
app.use((err, req, res, next) => {
logger.error('Unhandled application error', {
requestId: req.id,
path: req.path,
error: {
message: err.message,
stack: isProduction ? undefined : err.stack
}
});

res.status(500).json({
error: isProduction ? 'Internal server error' : err.message,
requestId: req.id
});
});

// Start server
const port = process.env.PORT || 3000;
app.listen(port, () => {
logger.info(`Server started on port ${port}`, {
nodeEnv: process.env.NODE_ENV || 'development'
});
});

Summary

Effective logging is an essential part of deploying Express applications to production. By implementing proper logging solutions, you gain visibility into your application's behavior, making it easier to monitor, debug, and maintain.

Key takeaways:

  • Use Morgan for basic HTTP request logging
  • Implement Winston for advanced, structured logging with multiple outputs
  • Configure log levels appropriately for different environments
  • Implement log rotation to manage file sizes
  • Consider centralized logging for distributed applications
  • Include contextual information in your logs

Remember that good logging practices are a balance between collecting enough information to be useful while avoiding excessive logging that can impact performance or create privacy concerns.

Additional Resources

Exercises

  1. Configure Morgan to log requests to a file instead of the console
  2. Create a custom Winston logger with different log levels for errors and informational messages
  3. Implement request ID tracking across all your logs
  4. Set up log rotation using winston-daily-rotate-file
  5. Create a custom middleware that logs the performance of all database queries


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)