Set up Winston logger with structured JSON logging, multiple transports, and automated log rotation using winston-daily-rotate-file for production Node.js applications. Configure comprehensive error handling and log management best practices.
Prerequisites
- Node.js 18+ installed
- Basic understanding of Node.js and npm
- Sudo access for systemd service creation
What this solves
Production Node.js applications require robust logging systems that capture structured data, rotate log files automatically, and handle errors gracefully. Winston provides enterprise-grade logging with multiple transports, custom formatting, and integration with log aggregation systems. This tutorial configures Winston with daily log rotation, structured JSON output, and production-ready error handling to prevent disk space issues and enable effective debugging.
Step-by-step installation
Install Node.js and npm
First, ensure Node.js 18+ is installed on your system for modern logging features and performance optimizations.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt update
sudo apt install -y nodejs
Create project directory and initialize npm
Set up a new Node.js project structure with proper directory permissions for log files.
mkdir -p /opt/nodejs-app/logs
cd /opt/nodejs-app
npm init -y
Install Winston and log rotation packages
Install Winston logger with daily rotate file transport for automated log management and compression.
npm install winston winston-daily-rotate-file
npm install --save-dev @types/node
Configure Winston logger with multiple transports
Create a centralized logger configuration with console, file, and rotating file transports for different log levels.
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require('path');
// Define log format for structured logging
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.prettyPrint()
);
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'HH:mm:ss'
}),
winston.format.printf(({ timestamp, level, message, stack }) => {
return ${timestamp} [${level}]: ${stack || message};
})
);
// Daily rotate file transport for general logs
const dailyRotateTransport = new DailyRotateFile({
filename: path.join(__dirname, 'logs', 'application-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: logFormat
});
// Daily rotate file transport for error logs
const errorRotateTransport = new DailyRotateFile({
filename: path.join(__dirname, 'logs', 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'error',
format: logFormat
});
// Create the logger instance
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: {
service: 'nodejs-app',
environment: process.env.NODE_ENV || 'development'
},
transports: [
dailyRotateTransport,
errorRotateTransport,
new winston.transports.File({
filename: path.join(__dirname, 'logs', 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 3
})
],
exceptionHandlers: [
new winston.transports.File({
filename: path.join(__dirname, 'logs', 'exceptions.log')
})
],
rejectionHandlers: [
new winston.transports.File({
filename: path.join(__dirname, 'logs', 'rejections.log')
})
]
});
// Add console transport for development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
// Handle log rotation events
dailyRotateTransport.on('rotate', (oldFilename, newFilename) => {
logger.info('Log file rotated', {
oldFile: oldFilename,
newFile: newFilename
});
});
dailyRotateTransport.on('archive', (zipFilename) => {
logger.info('Log file archived', { archive: zipFilename });
});
// Export logger and helper functions
module.exports = {
logger,
// Request logging middleware for Express
requestLogger: (req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: ${duration}ms,
userAgent: req.get('User-Agent'),
ip: req.ip || req.connection.remoteAddress
});
});
next();
},
// Error logging helper
logError: (error, context = {}) => {
logger.error('Application Error', {
error: error.message,
stack: error.stack,
...context
});
}
};
Create sample application with logging
Implement a sample Express.js application demonstrating structured logging patterns and error handling.
npm install express
const express = require('express');
const { logger, requestLogger, logError } = require('./logger');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(requestLogger);
// Sample routes with different log levels
app.get('/', (req, res) => {
logger.info('Home page accessed', {
timestamp: new Date().toISOString(),
userAgent: req.get('User-Agent')
});
res.json({
message: 'Winston logging demo',
timestamp: new Date().toISOString()
});
});
app.get('/debug', (req, res) => {
logger.debug('Debug endpoint called', {
query: req.query,
headers: req.headers
});
res.json({ level: 'debug', data: req.query });
});
app.get('/warning', (req, res) => {
logger.warn('Warning endpoint accessed', {
message: 'This endpoint is deprecated',
deprecationDate: '2024-12-31'
});
res.json({
warning: 'This endpoint is deprecated',
alternative: '/api/v2/data'
});
});
app.get('/error-demo', (req, res) => {
try {
// Simulate an error
throw new Error('Simulated application error');
} catch (error) {
logError(error, {
endpoint: '/error-demo',
requestId: req.get('X-Request-ID'),
userId: req.get('X-User-ID')
});
res.status(500).json({
error: 'Internal server error',
requestId: Date.now()
});
}
});
// Async error handling example
app.get('/async-error', async (req, res) => {
try {
await new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Async operation failed')), 100);
});
} catch (error) {
logError(error, {
endpoint: '/async-error',
operation: 'database-query'
});
res.status(500).json({ error: 'Database operation failed' });
}
});
// Global error handler
app.use((error, req, res, next) => {
logError(error, {
url: req.url,
method: req.method,
body: req.body
});
res.status(500).json({
error: 'Internal server error',
timestamp: new Date().toISOString()
});
});
// Graceful shutdown handling
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
logger.info('SIGINT received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});
const server = app.listen(PORT, () => {
logger.info('Server started', {
port: PORT,
environment: process.env.NODE_ENV || 'development',
nodeVersion: process.version
});
});
module.exports = app;
Configure environment variables
Create environment configuration for different deployment environments with appropriate log levels.
NODE_ENV=production
LOG_LEVEL=info
PORT=3000
NODE_ENV=development
LOG_LEVEL=debug
PORT=3000
Set up proper file permissions
Configure secure file permissions for the application and log directories to prevent unauthorized access.
sudo useradd --system --no-create-home --shell /bin/false nodejs-app
sudo chown -R nodejs-app:nodejs-app /opt/nodejs-app
sudo chmod 755 /opt/nodejs-app
sudo chmod 755 /opt/nodejs-app/logs
sudo chmod 644 /opt/nodejs-app/.js /opt/nodejs-app/.env
Create systemd service for production
Set up a systemd service to manage the Node.js application with proper logging and automatic restarts.
[Unit]
Description=Node.js Application with Winston Logging
After=network.target
Wants=network.target
[Service]
Type=simple
User=nodejs-app
Group=nodejs-app
WorkingDirectory=/opt/nodejs-app
EnvironmentFile=/opt/nodejs-app/.env
ExecStart=/usr/bin/node app.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodejs-app
Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/nodejs-app/logs
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable nodejs-app
sudo systemctl start nodejs-app
Configure log rotation cleanup script
Create a maintenance script to clean up old compressed logs and monitor disk usage.
#!/bin/bash
Log maintenance script for Winston rotating logs
LOG_DIR="/opt/nodejs-app/logs"
MAX_DISK_USAGE=80
OLD_ARCHIVE_DAYS=90
Check disk usage
DISK_USAGE=$(df $LOG_DIR | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $DISK_USAGE -gt $MAX_DISK_USAGE ]; then
echo "$(date): Disk usage $DISK_USAGE% exceeds threshold, cleaning old archives"
# Remove archives older than 90 days
find $LOG_DIR -name "*.gz" -type f -mtime +$OLD_ARCHIVE_DAYS -delete
# Log cleanup action
echo "$(date): Cleaned archives older than $OLD_ARCHIVE_DAYS days" >> $LOG_DIR/maintenance.log
fi
Compress logs older than 7 days that aren't compressed
find $LOG_DIR -name ".log" -type f -mtime +7 ! -name "combined.log" ! -name "$(date +%Y-%m-%d)*" -exec gzip {} \;
echo "$(date): Log maintenance completed" >> $LOG_DIR/maintenance.log
sudo chmod +x /opt/nodejs-app/scripts/log-maintenance.sh
sudo chown nodejs-app:nodejs-app /opt/nodejs-app/scripts/log-maintenance.sh
Set up automated log maintenance
Configure a systemd timer to run log maintenance automatically without using cron.
[Unit]
Description=Node.js Application Log Maintenance
[Service]
Type=oneshot
User=nodejs-app
Group=nodejs-app
ExecStart=/opt/nodejs-app/scripts/log-maintenance.sh
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run Node.js log maintenance daily
Requires=nodejs-log-maintenance.service
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable nodejs-log-maintenance.timer
sudo systemctl start nodejs-log-maintenance.timer
Verify your setup
Test the logging system and verify log rotation is working correctly.
# Check service status
sudo systemctl status nodejs-app
Test different log endpoints
curl http://localhost:3000/
curl http://localhost:3000/debug?test=value
curl http://localhost:3000/warning
curl http://localhost:3000/error-demo
Verify log files are created
ls -la /opt/nodejs-app/logs/
Check log content
sudo tail -f /opt/nodejs-app/logs/application-$(date +%Y-%m-%d).log
Verify log rotation timer
sudo systemctl list-timers nodejs-log-maintenance.timer
Test log maintenance script
sudo -u nodejs-app /opt/nodejs-app/scripts/log-maintenance.sh
Production logging best practices
Configure advanced logging features for production environments with monitoring integration.
Add structured metadata and correlation IDs
Enhance logging with request correlation IDs and structured metadata for better traceability.
const { v4: uuidv4 } = require('uuid');
const { logger } = require('../logger');
const correlationMiddleware = (req, res, next) => {
// Generate or extract correlation ID
const correlationId = req.headers['x-correlation-id'] || uuidv4();
// Add to request and response headers
req.correlationId = correlationId;
res.setHeader('X-Correlation-ID', correlationId);
// Create child logger with correlation ID
req.logger = logger.child({
correlationId,
requestId: uuidv4(),
userId: req.headers['x-user-id'] || 'anonymous'
});
next();
};
module.exports = correlationMiddleware;
Configure log aggregation transport
Set up Winston transport for sending logs to centralized logging systems like ELK stack.
npm install winston-elasticsearch
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const elasticsearchTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
auth: {
username: process.env.ELASTICSEARCH_USERNAME || 'elastic',
password: process.env.ELASTICSEARCH_PASSWORD || 'changeme'
}
},
index: 'nodejs-app-logs',
indexTemplate: {
name: 'nodejs-app-logs',
body: {
index_patterns: ['nodejs-app-logs-*'],
settings: {
number_of_shards: 1,
number_of_replicas: 1,
'index.lifecycle.name': 'nodejs-app-logs-policy',
'index.lifecycle.rollover_alias': 'nodejs-app-logs'
},
mappings: {
properties: {
'@timestamp': { type: 'date' },
level: { type: 'keyword' },
message: { type: 'text' },
correlationId: { type: 'keyword' },
service: { type: 'keyword' },
environment: { type: 'keyword' }
}
}
}
}
});
module.exports = elasticsearchTransport;
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Log files not created | Permission denied on logs directory | sudo chown nodejs-app:nodejs-app /opt/nodejs-app/logs |
| Disk space filling up | Log rotation not working | Check maxFiles and maxSize settings, verify maintenance timer |
| Logs missing timestamp | Format configuration error | Verify Winston timestamp format in logger configuration |
| Console logs in production | NODE_ENV not set correctly | Set NODE_ENV=production in environment file |
| Log rotation files corrupted | Process writing during rotation | Use zippedArchive: true and ensure atomic writes |
| Memory leaks with large logs | Missing stream cleanup | Configure maxsize limits and implement log streaming |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Production-quality Node.js Winston logging setup script
# Supports Ubuntu, Debian, AlmaLinux, Rocky Linux, CentOS, RHEL, Oracle Linux, Fedora, Amazon Linux
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
APP_NAME="${1:-nodejs-app}"
APP_USER="${2:-nodeapp}"
APP_DIR="/opt/${APP_NAME}"
LOG_DIR="${APP_DIR}/logs"
SERVICE_NAME="${APP_NAME}"
# Usage message
usage() {
echo "Usage: $0 [app-name] [app-user]"
echo " app-name: Application name (default: nodejs-app)"
echo " app-user: Application user (default: nodeapp)"
exit 1
}
# Validate arguments
if [[ $# -gt 2 ]]; then
usage
fi
# Cleanup function for rollback
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop "${SERVICE_NAME}" 2>/dev/null || true
systemctl disable "${SERVICE_NAME}" 2>/dev/null || true
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
userdel -r "${APP_USER}" 2>/dev/null || true
rm -rf "${APP_DIR}"
echo -e "${YELLOW}Cleanup completed${NC}"
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Auto-detect distribution
echo -e "${BLUE}[1/10] Detecting distribution...${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y --refresh"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
else
echo -e "${RED}/etc/os-release not found${NC}"
exit 1
fi
# Update package cache
echo -e "${BLUE}[2/10] Updating package cache...${NC}"
$PKG_UPDATE
# Install Node.js 20.x
echo -e "${BLUE}[3/10] Installing Node.js 20.x...${NC}"
if ! command -v node &> /dev/null || [[ $(node -v | cut -d. -f1 | sed 's/v//') -lt 18 ]]; then
case "$PKG_MGR" in
"apt")
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
$PKG_INSTALL nodejs
;;
"dnf"|"yum")
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
$PKG_INSTALL nodejs npm
;;
esac
else
echo -e "${GREEN}Node.js $(node -v) already installed${NC}"
fi
# Create application user
echo -e "${BLUE}[4/10] Creating application user...${NC}"
if ! id "${APP_USER}" &>/dev/null; then
useradd -r -s /bin/false -d "${APP_DIR}" "${APP_USER}"
echo -e "${GREEN}User ${APP_USER} created${NC}"
else
echo -e "${YELLOW}User ${APP_USER} already exists${NC}"
fi
# Create application directory structure
echo -e "${BLUE}[5/10] Creating application directories...${NC}"
mkdir -p "${APP_DIR}/logs"
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}"
chmod 755 "${APP_DIR}"
chmod 755 "${LOG_DIR}"
# Initialize npm project
echo -e "${BLUE}[6/10] Initializing npm project...${NC}"
cd "${APP_DIR}"
if [[ ! -f package.json ]]; then
sudo -u "${APP_USER}" npm init -y
fi
# Install Winston packages
echo -e "${BLUE}[7/10] Installing Winston packages...${NC}"
sudo -u "${APP_USER}" npm install winston winston-daily-rotate-file
sudo -u "${APP_USER}" npm install --save-dev @types/node
# Create Winston logger configuration
echo -e "${BLUE}[8/10] Creating logger configuration...${NC}"
cat > "${APP_DIR}/logger.js" << 'EOF'
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require('path');
// Define log format for structured logging
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.prettyPrint()
);
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'HH:mm:ss'
}),
winston.format.printf(({ timestamp, level, message, stack }) => {
return `${timestamp} [${level}]: ${stack || message}`;
})
);
// Daily rotate file transport for general logs
const dailyRotateTransport = new DailyRotateFile({
filename: path.join(__dirname, 'logs', 'application-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: logFormat
});
// Daily rotate file transport for error logs
const errorRotateTransport = new DailyRotateFile({
filename: path.join(__dirname, 'logs', 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'error',
format: logFormat
});
// Create the logger instance
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: {
service: process.env.SERVICE_NAME || 'nodejs-app',
environment: process.env.NODE_ENV || 'development'
},
transports: [
dailyRotateTransport,
errorRotateTransport,
new winston.transports.File({
filename: path.join(__dirname, 'logs', 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 3
})
],
exceptionHandlers: [
new winston.transports.File({
filename: path.join(__dirname, 'logs', 'exceptions.log')
})
],
rejectionHandlers: [
new winston.transports.File({
filename: path.join(__dirname, 'logs', 'rejections.log')
})
]
});
// Add console transport for development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
// Handle log rotation events
dailyRotateTransport.on('rotate', (oldFilename, newFilename) => {
logger.info('Log file rotated', {
oldFile: oldFilename,
newFile: newFilename
});
});
dailyRotateTransport.on('archive', (zipFilename) => {
logger.info('Log file archived', { archive: zipFilename });
});
module.exports = logger;
EOF
# Create sample application
echo -e "${BLUE}[9/10] Creating sample application...${NC}"
cat > "${APP_DIR}/app.js" << 'EOF'
const logger = require('./logger');
// Sample application demonstrating logging
logger.info('Application starting', {
version: '1.0.0',
nodeVersion: process.version
});
// Simulate different log levels
setInterval(() => {
logger.info('Heartbeat', { timestamp: new Date().toISOString() });
}, 30000);
// Error handling
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at Promise', { reason, promise });
});
logger.info('Application initialized successfully');
// Keep the application running
process.stdin.resume();
EOF
# Set proper ownership and permissions
chown -R "${APP_USER}:${APP_USER}" "${APP_DIR}"
chmod 644 "${APP_DIR}/logger.js"
chmod 644 "${APP_DIR}/app.js"
chmod 644 "${APP_DIR}/package.json"
# Create systemd service
echo -e "${BLUE}[10/10] Creating systemd service...${NC}"
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
[Unit]
Description=${APP_NAME} Node.js Application with Winston Logging
After=network.target
[Service]
Type=simple
User=${APP_USER}
WorkingDirectory=${APP_DIR}
ExecStart=/usr/bin/node app.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=SERVICE_NAME=${APP_NAME}
Environment=LOG_LEVEL=info
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=${LOG_DIR}
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
echo -e "${GREEN}Installation completed successfully!${NC}"
echo
echo -e "${BLUE}Application Details:${NC}"
echo " App Directory: ${APP_DIR}"
echo " Log Directory: ${LOG_DIR}"
echo " Service Name: ${SERVICE_NAME}"
echo " App User: ${APP_USER}"
echo
echo -e "${BLUE}Management Commands:${NC}"
echo " Start service: systemctl start ${SERVICE_NAME}"
echo " Stop service: systemctl stop ${SERVICE_NAME}"
echo " View logs: journalctl -u ${SERVICE_NAME} -f"
echo " View app logs: tail -f ${LOG_DIR}/application-$(date +%Y-%m-%d).log"
echo " View error logs: tail -f ${LOG_DIR}/error-$(date +%Y-%m-%d).log"
# Verification
echo -e "${BLUE}Verification:${NC}"
if command -v node &> /dev/null; then
echo -e "${GREEN}✓ Node.js $(node -v) installed${NC}"
else
echo -e "${RED}✗ Node.js installation failed${NC}"
fi
if [[ -f "${APP_DIR}/logger.js" ]]; then
echo -e "${GREEN}✓ Logger configuration created${NC}"
else
echo -e "${RED}✗ Logger configuration missing${NC}"
fi
if systemctl is-enabled "${SERVICE_NAME}" &>/dev/null; then
echo -e "${GREEN}✓ Service enabled${NC}"
else
echo -e "${RED}✗ Service not enabled${NC}"
fi
trap - ERR
echo -e "${GREEN}Setup completed! Start the service with: systemctl start ${SERVICE_NAME}${NC}"
Review the script before running. Execute with: bash install.sh