DEV Community

Cover image for PM2 + Winston Logs: Monitor Your Node.js App Like a Pro
Sarvesh
Sarvesh

Posted on

PM2 + Winston Logs: Monitor Your Node.js App Like a Pro

Introduction

Picture this: It's 3 AM, your phone buzzes with alerts, and your Node.js application has mysteriously crashed in production. Users are complaining, your boss is asking questions, and you're frantically trying to piece together what went wrong with nothing but sparse console.log statements.
Sound familiar? You're not alone.
Production-ready Node.js applications require robust process management and comprehensive logging strategies. Today, we'll explore how combining PM2 (Process Manager 2) with Winston logging creates a powerful monitoring solution that transforms reactive debugging into proactive system management.


Why Process Management and Logging Matter

The Problem with Basic Node.js Deployment

A typical Node.js application runs as a single process. When that process crashes, your entire application goes down. Without proper logging, diagnosing issues becomes a guessing game.

The Solution: PM2 + Winston

PM2 handles the process management layer:

  • Automatic restarts on crashes
  • Load balancing across CPU cores
  • Zero-downtime deployments
  • Built-in monitoring

Winston manages the logging layer:

  • Structured, queryable logs
  • Multiple output destinations
  • Log levels and filtering
  • Automatic log rotation

Setting Up Our Example Application

Let's build a simple e-commerce API to demonstrate these concepts. Our app will handle user authentication, product management, and order processing.

// app.js - Basic Express Setup
const express = require('express');
const app = express();

// Middleware
app.use(express.json());

// Routes
app.get('/api/products', (req, res) => {
  // Simulate potential failure
  if (Math.random() < 0.1) {
    throw new Error('Database connection failed');
  }
  res.json({ products: ['Laptop', 'Phone', 'Tablet'] });
});

app.post('/api/orders', (req, res) => {
  const { userId, productId, quantity } = req.body;

  // Simulate processing
  if (!userId || !productId) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  res.json({ orderId: Date.now(), status: 'confirmed' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Implementing Winston Logging

Step 1: Install and Configure Winston

npm install winston winston-daily-rotate-file
Enter fullscreen mode Exit fullscreen mode
// logger.js - Winston Configuration
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

// Custom format for better readability
const logFormat = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.errors({ stack: true }),
  winston.format.json()
);

// Create logger instance
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: logFormat,
  defaultMeta: { service: 'ecommerce-api' },
  transports: [
    // Console output for development
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),

    // File output with rotation
    new DailyRotateFile({
      filename: 'logs/application-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '14d'
    }),

    // Separate error log
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error' 
    })
  ]
});

module.exports = logger;
Enter fullscreen mode Exit fullscreen mode

Step 2: Integrate Logging into Your Application

// app.js - Updated with Winston
const express = require('express');
const logger = require('./logger');
const app = express();

app.use(express.json());

// Request logging middleware
app.use((req, res, next) => {
  logger.info('Request received', {
    method: req.method,
    url: req.url,
    ip: req.ip,
    userAgent: req.get('User-Agent')
  });
  next();
});

app.get('/api/products', (req, res) => {
  try {
    logger.info('Fetching products');

    // Simulate potential failure
    if (Math.random() < 0.1) {
      throw new Error('Database connection failed');
    }

    const products = ['Laptop', 'Phone', 'Tablet'];
    logger.info('Products fetched successfully', { count: products.length });
    res.json({ products });
  } catch (error) {
    logger.error('Failed to fetch products', { error: error.message, stack: error.stack });
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/api/orders', (req, res) => {
  const { userId, productId, quantity } = req.body;

  logger.info('Order creation attempt', { userId, productId, quantity });

  if (!userId || !productId) {
    logger.warn('Order creation failed - missing fields', { userId, productId });
    return res.status(400).json({ error: 'Missing required fields' });
  }

  const orderId = Date.now();
  logger.info('Order created successfully', { orderId, userId, productId, quantity });

  res.json({ orderId, status: 'confirmed' });
});

// Global error handler
app.use((error, req, res, next) => {
  logger.error('Unhandled error', {
    error: error.message,
    stack: error.stack,
    url: req.url,
    method: req.method
  });
  res.status(500).json({ error: 'Something went wrong!' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`Server started on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Configuring PM2 for Production

Step 1: Install PM2

npm install -g pm2
Enter fullscreen mode Exit fullscreen mode

Step 2: Create PM2 Configuration

// ecosystem.config.js - PM2 Configuration
module.exports = {
  apps: [{
    name: 'ecommerce-api',
    script: 'app.js',
    instances: 'max', // Use all CPU cores
    exec_mode: 'cluster',

    // Environment variables
    env: {
      NODE_ENV: 'development',
      PORT: 3000,
      LOG_LEVEL: 'debug'
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 8000,
      LOG_LEVEL: 'info'
    },

    // PM2 specific settings
    watch: false,
    max_memory_restart: '1G',
    error_file: 'logs/pm2-error.log',
    out_file: 'logs/pm2-out.log',
    log_file: 'logs/pm2-combined.log',
    time: true,

    // Restart policies
    restart_delay: 4000,
    max_restarts: 10,
    min_uptime: '10s'
  }]
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Deploy with PM2

# Start application
pm2 start ecosystem.config.js --env production

# Monitor processes
pm2 monit

# View logs
pm2 logs

# Restart application
pm2 restart ecommerce-api

# Zero-downtime reload
pm2 reload ecommerce-api
Enter fullscreen mode Exit fullscreen mode

Advanced Monitoring Strategies

Log Aggregation and Analysis

// monitoring.js - Enhanced Monitoring
const logger = require('./logger');

class PerformanceMonitor {
  static trackRequest(req, res, next) {
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      const logLevel = duration > 1000 ? 'warn' : 'info';

      logger.log(logLevel, 'Request completed', {
        method: req.method,
        url: req.url,
        statusCode: res.statusCode,
        duration: `${duration}ms`,
        slow: duration > 1000
      });
    });

    next();
  }

  static logSystemMetrics() {
    const used = process.memoryUsage();

    logger.info('System metrics', {
      memory: {
        rss: Math.round(used.rss / 1024 / 1024 * 100) / 100,
        heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100,
        heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100,
        external: Math.round(used.external / 1024 / 1024 * 100) / 100
      },
      uptime: process.uptime(),
      pid: process.pid
    });
  }
}

// Log system metrics every 5 minutes
setInterval(() => {
  PerformanceMonitor.logSystemMetrics();
}, 5 * 60 * 1000);

module.exports = PerformanceMonitor;
Enter fullscreen mode Exit fullscreen mode

PM2 Monitoring Dashboard

# Install PM2 web dashboard
pm2 install pm2-server-monit

# View real-time monitoring
pm2 web
Enter fullscreen mode Exit fullscreen mode

Troubleshooting Common Issues

Challenge 1: Log File Growth

Problem: Log files consuming disk space
Solution: Implement log rotation and cleanup

// Add to winston configuration
new DailyRotateFile({
  filename: 'logs/application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxSize: '20m',
  maxFiles: '14d', // Keep 14 days
  auditFile: 'logs/audit.json'
})
Enter fullscreen mode Exit fullscreen mode

Challenge 2: Memory Leaks

Problem: Application consuming increasing memory
Solution: PM2 automatic restart on memory threshold

// In ecosystem.config.js
max_memory_restart: '1G',
Enter fullscreen mode Exit fullscreen mode

Challenge 3: Handling Graceful Shutdowns

// graceful-shutdown.js
const logger = require('./logger');

process.on('SIGTERM', () => {
  logger.info('SIGTERM received, starting graceful shutdown');

  server.close((err) => {
    if (err) {
      logger.error('Error during shutdown', { error: err.message });
      process.exit(1);
    }

    logger.info('Server closed successfully');
    process.exit(0);
  });
});
Enter fullscreen mode Exit fullscreen mode

Production Deployment Checklist

Environment Setup

  • PM2 installed globally
  • Log directories created with proper permissions
  • Environment variables configured
  • SSL certificates installed (if applicable)

Monitoring Configuration

  • Log levels set appropriately for production
  • Log rotation configured
  • Error alerting set up
  • Performance metrics tracking enabled

PM2 Configuration

  • Cluster mode enabled for CPU utilization
  • Memory restart limits set
  • Process restart policies configured
  • Health checks implemented

Key Takeaways

  1. Proactive Monitoring: Don't wait for issues to surface—build observability from the start
  2. Structured Logging: Use consistent log formats and meaningful metadata
  3. Process Resilience: Leverage PM2's clustering and auto-restart capabilities
  4. Resource Management: Monitor memory usage and implement appropriate restart policies
  5. Graceful Operations: Handle shutdowns and deployments without dropping connections

Next Steps

  1. Implement Log Aggregation: Consider tools like ELK Stack or Splunk for centralized log analysis
  2. Add APM Integration: Integrate with New Relic, DataDog, or similar services
  3. Create Alerting Rules: Set up notifications for critical errors and performance degradation
  4. Automated Deployment: Implement CI/CD pipelines with PM2 integration
  5. Load Testing: Use tools like Artillery or k6 to validate your monitoring setup under load

👋 Connect with Me

Thanks for reading! If you found this post helpful or want to discuss similar topics in full stack development, feel free to connect or reach out:

🔗 LinkedIn: https://www.linkedin.com/in/sarvesh-sp/

🌐 Portfolio: https://sarveshsp.netlify.app/

📨 Email: sarveshsp@duck.com

Found this article useful? Consider sharing it with your network and following me for more in-depth technical content on Node.js, performance optimization, and full-stack development best practices.

Top comments (0)