Monitoring & Logging
Implement comprehensive monitoring, logging, and alerting for production Node.js applications. This is a foundational concept in server-side JavaScript development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world Node.js experience. Take your time with each section and practice the examples
Application Monitoring
Set up monitoring to track application performance, errors, and user behavior in production environments.. This is an essential concept that every Node.js developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Prometheus & Grafana
// prometheus-client setup
const prometheus = require('prom-client');
// Create a Registry to register the metrics
const register = new prometheus.Registry();
// Add a default label which is added to all metrics
register.setDefaultLabels({
app: 'myapp'
});
// Enable the collection of default metrics
prometheus.collectDefaultMetrics({ register });
// Custom metrics
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new prometheus.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
// Register custom metrics
register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
register.registerMetric(activeConnections);
// Express middleware for metrics
const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route ? req.route.path : req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
});
next();
};
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
module.exports = { register, metricsMiddleware }; Structured Logging
// winston logger configuration
const winston = require('winston');
const { combine, timestamp, errors, json, printf, colorize } = winston.format;
// Custom format
const logFormat = printf(({ level, message, timestamp, stack, ...meta }) => {
return JSON.stringify({
timestamp,
level,
message,
stack,
...meta
});
});
// Create logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp(),
errors({ stack: true }),
json()
),
defaultMeta: { service: 'myapp' },
transports: [
// Write all logs with level 'error' and below to 'error.log'
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// Write all logs to 'combined.log'
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// If we're not in production, log to the console
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: combine(
colorize(),
timestamp(),
logFormat
)
}));
}
// Express middleware for request logging
const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
userAgent: req.get('User-Agent'),
ip: req.ip
});
});
next();
};
module.exports = { logger, requestLogger };Error Tracking
// Sentry integration
const Sentry = require('@sentry/node');
const { nodeProfilingIntegration } = require('@sentry/profiling-node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
// Express error handler
const errorHandler = (err, req, res, next) => {
// Log error
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
userAgent: req.get('User-Agent'),
ip: req.ip
});
// Send to Sentry
Sentry.captureException(err);
// Send response
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
};
// Health check endpoint
app.get('/health', (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version
};
res.json(health);
});
module.exports = { errorHandler };Alerting & Notifications
// Alerting system
const nodemailer = require('nodemailer');
const axios = require('axios');
class AlertManager {
constructor() {
this.emailTransporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
}
async sendEmailAlert(alert) {
const mailOptions = {
from: process.env.SMTP_FROM,
to: process.env.ALERT_EMAIL,
subject: `[ALERT] ${alert.title}`,
html: `
<h2>${alert.title}</h2>
<p><strong>Severity:</strong> ${alert.severity}</p>
<p><strong>Message:</strong> ${alert.message}</p>
<p><strong>Timestamp:</strong> ${alert.timestamp}</p>
<p><strong>Service:</strong> ${alert.service}</p>
`
};
await this.emailTransporter.sendMail(mailOptions);
}
async sendSlackAlert(alert) {
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
const payload = {
text: `🚨 ${alert.title}`,
attachments: [{
color: alert.severity === 'critical' ? 'danger' : 'warning',
fields: [
{ title: 'Severity', value: alert.severity, short: true },
{ title: 'Service', value: alert.service, short: true },
{ title: 'Message', value: alert.message, short: false },
{ title: 'Timestamp', value: alert.timestamp, short: true }
]
}]
};
await axios.post(webhookUrl, payload);
}
async sendAlert(alert) {
try {
await Promise.all([
this.sendEmailAlert(alert),
this.sendSlackAlert(alert)
]);
} catch (error) {
logger.error('Failed to send alert', { error: error.message });
}
}
}
// Alert conditions
class AlertConditions {
constructor(alertManager) {
this.alertManager = alertManager;
this.thresholds = {
errorRate: 0.05, // 5%
responseTime: 1000, // 1 second
memoryUsage: 0.8, // 80%
cpuUsage: 0.8 // 80%
};
}
checkErrorRate(errorCount, totalRequests) {
const errorRate = errorCount / totalRequests;
if (errorRate > this.thresholds.errorRate) {
this.alertManager.sendAlert({
title: 'High Error Rate',
severity: 'critical',
message: `Error rate is ${(errorRate * 100).toFixed(2)}%, threshold is ${(this.thresholds.errorRate * 100)}%`,
service: 'myapp',
timestamp: new Date().toISOString()
});
}
}
checkMemoryUsage() {
const memUsage = process.memoryUsage();
const memUsagePercent = memUsage.heapUsed / memUsage.heapTotal;
if (memUsagePercent > this.thresholds.memoryUsage) {
this.alertManager.sendAlert({
title: 'High Memory Usage',
severity: 'warning',
message: `Memory usage is ${(memUsagePercent * 100).toFixed(2)}%, threshold is ${(this.thresholds.memoryUsage * 100)}%`,
service: 'myapp',
timestamp: new Date().toISOString()
});
}
}
}
module.exports = { AlertManager, AlertConditions };Mini-Project: Complete Monitoring System
// Complete monitoring and observability system
const express = require('express');
const prometheus = require('prom-client');
const winston = require('winston');
const Sentry = require('@sentry/node');
// Initialize Sentry
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0
});
// Prometheus metrics
const register = new prometheus.Registry();
register.setDefaultLabels({ app: 'myapp' });
prometheus.collectDefaultMetrics({ register });
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new prometheus.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
register.registerMetric(activeConnections);
// Winston logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'myapp' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console());
}
// Express app
const app = express();
// Middleware
app.use(Sentry.requestHandler());
app.use(express.json());
// Metrics middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route ? req.route.path : req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
});
next();
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
userAgent: req.get('User-Agent'),
ip: req.ip
});
});
next();
});
// Routes
app.get('/health', (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version
};
res.json(health);
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
app.get('/api/test', (req, res) => {
res.json({ message: 'Test endpoint working' });
});
// Error handling
app.use(Sentry.errorHandler());
app.use((err, req, res, next) => {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method
});
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('SIGINT received, shutting down gracefully');
process.exit(0);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
module.exports = app;