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}`);
});
Implementing Winston Logging
Step 1: Install and Configure Winston
npm install winston winston-daily-rotate-file
// 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;
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}`);
});
Configuring PM2 for Production
Step 1: Install PM2
npm install -g pm2
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'
}]
};
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
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;
PM2 Monitoring Dashboard
# Install PM2 web dashboard
pm2 install pm2-server-monit
# View real-time monitoring
pm2 web
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'
})
Challenge 2: Memory Leaks
Problem: Application consuming increasing memory
Solution: PM2 automatic restart on memory threshold
// In ecosystem.config.js
max_memory_restart: '1G',
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);
});
});
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
- Proactive Monitoring: Don't wait for issues to surface—build observability from the start
- Structured Logging: Use consistent log formats and meaningful metadata
- Process Resilience: Leverage PM2's clustering and auto-restart capabilities
- Resource Management: Monitor memory usage and implement appropriate restart policies
- Graceful Operations: Handle shutdowns and deployments without dropping connections
Next Steps
- Implement Log Aggregation: Consider tools like ELK Stack or Splunk for centralized log analysis
- Add APM Integration: Integrate with New Relic, DataDog, or similar services
- Create Alerting Rules: Set up notifications for critical errors and performance degradation
- Automated Deployment: Implement CI/CD pipelines with PM2 integration
- 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)