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:
- Troubleshooting: Logs help identify the root cause of errors
- Performance Monitoring: Track response times and identify bottlenecks
- Security: Detect suspicious activities or unauthorized access attempts
- User Behavior: Understand how users interact with your application
- 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:
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
npm install morgan
Basic Morgan Setup
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 outputcommon
: Standard Apache common log outputdev
: Colored by response status for developmentshort
: Shorter than default, includes response timetiny
: The minimal output
For development, the dev
format is particularly useful:
// 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:
// 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
npm install winston
Basic Winston Setup
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:
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 attentionwarn
: Something unexpected happened but doesn't require immediate actioninfo
: Important application events (startup, shutdown, configuration)http
: HTTP request loggingverbose
: Detailed information useful for debuggingdebug
: Very detailed debugging informationsilly
: Extremely detailed information
You can configure the minimum level to log:
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
npm install debug
Basic Debug Usage
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:
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
// 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
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
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
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
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:
npm install winston-daily-rotate-file
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:
// 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:
// 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:
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
- Morgan Documentation
- Winston Documentation
- Debug Module Documentation
- Node.js Logging Best Practices
Exercises
- Configure Morgan to log requests to a file instead of the console
- Create a custom Winston logger with different log levels for errors and informational messages
- Implement request ID tracking across all your logs
- Set up log rotation using winston-daily-rotate-file
- 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! :)