Configure Node.js application logging with Winston and log rotation for production

Intermediate 25 min Apr 07, 2026 45 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs npm

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
Never use chmod 777. It gives every user on the system full access to your files and logs. Instead, use specific user ownership and minimal permissions for security.

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

SymptomCauseFix
Log files not createdPermission denied on logs directorysudo chown nodejs-app:nodejs-app /opt/nodejs-app/logs
Disk space filling upLog rotation not workingCheck maxFiles and maxSize settings, verify maintenance timer
Logs missing timestampFormat configuration errorVerify Winston timestamp format in logger configuration
Console logs in productionNODE_ENV not set correctlySet NODE_ENV=production in environment file
Log rotation files corruptedProcess writing during rotationUse zippedArchive: true and ensure atomic writes
Memory leaks with large logsMissing stream cleanupConfigure maxsize limits and implement log streaming

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.