Performance Monitoring Tools - Complete Guide
Published: September 25, 2024 | Reading time: 25 minutes
Performance Monitoring Overview
Effective performance monitoring is essential for maintaining optimal Node.js applications:
Monitoring Benefits
# Performance Monitoring Benefits
- Real-time performance insights
- Proactive issue detection
- Performance optimization
- Resource utilization tracking
- User experience monitoring
- Capacity planning
- Cost optimization
Built-in Performance Monitoring
Node.js Performance APIs
Built-in Performance Monitoring
# Built-in Performance Monitoring
# 1. Performance Hooks
const { performance, PerformanceObserver } = require('perf_hooks');
// Measure function execution time
function measurePerformance(fn, label) {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(`${label}: ${(end - start).toFixed(2)}ms`);
return result;
}
// Performance observer
const obs = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
obs.observe({ entryTypes: ['measure', 'mark'] });
# 2. Memory Monitoring
const v8 = require('v8');
class MemoryMonitor {
constructor() {
this.baseline = null;
this.threshold = 100 * 1024 * 1024; // 100MB
this.interval = null;
}
start(intervalMs = 30000) {
this.baseline = process.memoryUsage();
console.log('Memory baseline set:', this.formatMemory(this.baseline));
this.interval = setInterval(() => {
this.checkMemory();
}, intervalMs);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
checkMemory() {
const current = process.memoryUsage();
const increase = current.heapUsed - this.baseline.heapUsed;
if (increase > this.threshold) {
console.warn(`Memory usage increased by ${this.formatBytes(increase)}`);
}
console.log('Current memory:', this.formatMemory(current));
}
formatMemory(memory) {
return {
rss: this.formatBytes(memory.rss),
heapTotal: this.formatBytes(memory.heapTotal),
heapUsed: this.formatBytes(memory.heapUsed),
external: this.formatBytes(memory.external)
};
}
formatBytes(bytes) {
return Math.round(bytes / 1024 / 1024) + ' MB';
}
}
const memoryMonitor = new MemoryMonitor();
# 3. CPU Monitoring
const os = require('os');
class CPUMonitor {
constructor() {
this.cpuUsage = process.cpuUsage();
this.lastCheck = Date.now();
}
getCPUUsage() {
const currentUsage = process.cpuUsage(this.cpuUsage);
const currentTime = Date.now();
const timeDiff = currentTime - this.lastCheck;
const userTime = currentUsage.user / 1000000; // Convert to seconds
const systemTime = currentUsage.system / 1000000;
const totalTime = userTime + systemTime;
const cpuPercent = (totalTime / (timeDiff / 1000)) * 100;
this.cpuUsage = process.cpuUsage();
this.lastCheck = currentTime;
return {
user: userTime,
system: systemTime,
total: totalTime,
percent: Math.min(cpuPercent, 100)
};
}
getSystemInfo() {
return {
cpus: os.cpus().length,
loadAverage: os.loadavg(),
uptime: os.uptime(),
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
platform: os.platform(),
arch: os.arch()
};
}
}
const cpuMonitor = new CPUMonitor();
# 4. Event Loop Monitoring
class EventLoopMonitor {
constructor() {
this.lag = 0;
this.interval = null;
}
start(intervalMs = 1000) {
this.interval = setInterval(() => {
this.measureLag();
}, intervalMs);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
measureLag() {
const start = process.hrtime.bigint();
setImmediate(() => {
const lag = Number(process.hrtime.bigint() - start) / 1000000;
this.lag = lag;
if (lag > 10) { // 10ms threshold
console.warn(`Event loop lag: ${lag.toFixed(2)}ms`);
}
});
}
getLag() {
return this.lag;
}
}
const eventLoopMonitor = new EventLoopMonitor();
# 5. Request Monitoring
class RequestMonitor {
constructor() {
this.requests = new Map();
this.stats = {
total: 0,
errors: 0,
slowRequests: 0,
averageResponseTime: 0
};
}
startRequest(req) {
const requestId = req.id || Math.random().toString(36).substr(2, 9);
req.requestId = requestId;
this.requests.set(requestId, {
start: Date.now(),
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip
});
return requestId;
}
endRequest(req, res) {
const requestId = req.requestId;
if (!requestId) return;
const request = this.requests.get(requestId);
if (!request) return;
const duration = Date.now() - request.start;
const isError = res.statusCode >= 400;
const isSlow = duration > 1000; // 1 second threshold
this.stats.total++;
if (isError) this.stats.errors++;
if (isSlow) this.stats.slowRequests++;
this.stats.averageResponseTime =
(this.stats.averageResponseTime * (this.stats.total - 1) + duration) / this.stats.total;
this.requests.delete(requestId);
if (isSlow || isError) {
console.warn('Request issue:', {
requestId,
method: request.method,
url: request.url,
status: res.statusCode,
duration: duration + 'ms',
isError,
isSlow
});
}
}
getStats() {
return { ...this.stats };
}
}
const requestMonitor = new RequestMonitor();
# 6. Database Monitoring
class DatabaseMonitor {
constructor() {
this.queries = new Map();
this.stats = {
totalQueries: 0,
slowQueries: 0,
errors: 0,
averageQueryTime: 0
};
}
startQuery(queryId, query, params) {
this.queries.set(queryId, {
start: Date.now(),
query,
params
});
}
endQuery(queryId, error) {
const query = this.queries.get(queryId);
if (!query) return;
const duration = Date.now() - query.start;
const isSlow = duration > 1000; // 1 second threshold
const isError = !!error;
this.stats.totalQueries++;
if (isSlow) this.stats.slowQueries++;
if (isError) this.stats.errors++;
this.stats.averageQueryTime =
(this.stats.averageQueryTime * (this.stats.totalQueries - 1) + duration) / this.stats.totalQueries;
this.queries.delete(queryId);
if (isSlow || isError) {
console.warn('Database query issue:', {
queryId,
query: query.query,
duration: duration + 'ms',
isSlow,
isError,
error: error?.message
});
}
}
getStats() {
return { ...this.stats };
}
}
const dbMonitor = new DatabaseMonitor();
# 7. Custom Metrics Collector
class MetricsCollector {
constructor() {
this.metrics = new Map();
this.counters = new Map();
this.gauges = new Map();
this.histograms = new Map();
}
// Counter metrics
incrementCounter(name, value = 1, labels = {}) {
const key = this.getKey(name, labels);
const current = this.counters.get(key) || 0;
this.counters.set(key, current + value);
}
getCounter(name, labels = {}) {
const key = this.getKey(name, labels);
return this.counters.get(key) || 0;
}
// Gauge metrics
setGauge(name, value, labels = {}) {
const key = this.getKey(name, labels);
this.gauges.set(key, value);
}
getGauge(name, labels = {}) {
const key = this.getKey(name, labels);
return this.gauges.get(key) || 0;
}
// Histogram metrics
observeHistogram(name, value, labels = {}) {
const key = this.getKey(name, labels);
const histogram = this.histograms.get(key) || [];
histogram.push(value);
// Keep only last 1000 values
if (histogram.length > 1000) {
histogram.shift();
}
this.histograms.set(key, histogram);
}
getHistogramStats(name, labels = {}) {
const key = this.getKey(name, labels);
const histogram = this.histograms.get(key) || [];
if (histogram.length === 0) {
return { count: 0, sum: 0, avg: 0, min: 0, max: 0 };
}
const sorted = histogram.sort((a, b) => a - b);
const sum = histogram.reduce((a, b) => a + b, 0);
return {
count: histogram.length,
sum,
avg: sum / histogram.length,
min: sorted[0],
max: sorted[sorted.length - 1],
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)]
};
}
getKey(name, labels) {
const labelStr = Object.keys(labels)
.sort()
.map(key => `${key}=${labels[key]}`)
.join(',');
return labelStr ? `${name}{${labelStr}}` : name;
}
getAllMetrics() {
return {
counters: Object.fromEntries(this.counters),
gauges: Object.fromEntries(this.gauges),
histograms: Object.fromEntries(this.histograms)
};
}
}
const metricsCollector = new MetricsCollector();
# 8. Performance Dashboard
class PerformanceDashboard {
constructor() {
this.monitors = {
memory: memoryMonitor,
cpu: cpuMonitor,
eventLoop: eventLoopMonitor,
requests: requestMonitor,
database: dbMonitor,
metrics: metricsCollector
};
}
getDashboardData() {
return {
timestamp: new Date().toISOString(),
memory: this.monitors.memory.formatMemory(process.memoryUsage()),
cpu: this.monitors.cpu.getCPUUsage(),
eventLoop: {
lag: this.monitors.eventLoop.getLag()
},
requests: this.monitors.requests.getStats(),
database: this.monitors.database.getStats(),
metrics: this.monitors.metrics.getAllMetrics(),
system: this.monitors.cpu.getSystemInfo()
};
}
startMonitoring() {
this.monitors.memory.start();
this.monitors.eventLoop.start();
// Log dashboard data every 30 seconds
setInterval(() => {
const data = this.getDashboardData();
console.log('Performance Dashboard:', JSON.stringify(data, null, 2));
}, 30000);
}
stopMonitoring() {
this.monitors.memory.stop();
this.monitors.eventLoop.stop();
}
}
const dashboard = new PerformanceDashboard();
# 9. Health Check Endpoint
app.get('/health', (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: cpuMonitor.getCPUUsage(),
eventLoop: eventLoopMonitor.getLag(),
requests: requestMonitor.getStats(),
database: dbMonitor.getStats()
};
res.json(health);
});
# 10. Metrics Endpoint
app.get('/metrics', (req, res) => {
const metrics = {
timestamp: new Date().toISOString(),
...metricsCollector.getAllMetrics()
};
res.json(metrics);
});
module.exports = {
MemoryMonitor,
CPUMonitor,
EventLoopMonitor,
RequestMonitor,
DatabaseMonitor,
MetricsCollector,
PerformanceDashboard
};
External Performance Monitoring Tools
APM and Monitoring Solutions
External Monitoring Tools
# External Performance Monitoring Tools
# 1. New Relic Integration
const newrelic = require('newrelic');
// Custom metrics
newrelic.recordMetric('Custom/ResponseTime', responseTime);
newrelic.recordMetric('Custom/ErrorRate', errorRate);
// Custom events
newrelic.recordCustomEvent('UserAction', {
action: 'login',
userId: userId,
timestamp: Date.now()
});
// Transaction tracing
newrelic.startWebTransaction('/api/users', function() {
// Your application code here
newrelic.endTransaction();
});
# 2. DataDog Integration
const tracer = require('dd-trace');
// Initialize tracer
tracer.init({
service: 'my-node-app',
env: process.env.NODE_ENV,
version: process.env.APP_VERSION
});
// Custom spans
const span = tracer.startSpan('custom.operation');
span.setTag('user.id', userId);
span.setTag('operation.type', 'database');
span.finish();
// Custom metrics
const StatsD = require('node-statsd');
const client = new StatsD({
host: 'localhost',
port: 8125,
prefix: 'myapp.'
});
client.increment('api.requests');
client.timing('api.response_time', responseTime);
client.gauge('memory.usage', memoryUsage);
# 3. Prometheus Integration
const promClient = require('prom-client');
// Create metrics
const httpRequestDuration = new promClient.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 promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const memoryUsage = new promClient.Gauge({
name: 'nodejs_memory_usage_bytes',
help: 'Node.js memory usage in bytes',
labelNames: ['type']
});
// Middleware to collect metrics
function prometheusMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const labels = {
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode
};
httpRequestDuration.observe(labels, duration);
httpRequestTotal.inc(labels);
});
next();
}
// Memory usage collector
setInterval(() => {
const memUsage = process.memoryUsage();
memoryUsage.set({ type: 'rss' }, memUsage.rss);
memoryUsage.set({ type: 'heapTotal' }, memUsage.heapTotal);
memoryUsage.set({ type: 'heapUsed' }, memUsage.heapUsed);
memoryUsage.set({ type: 'external' }, memUsage.external);
}, 5000);
// Metrics endpoint
app.get('/metrics', (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(promClient.register.metrics());
});
# 4. Grafana Integration
const grafana = require('grafana-api');
const grafanaClient = new grafana({
url: process.env.GRAFANA_URL,
apiKey: process.env.GRAFANA_API_KEY
});
// Create dashboard
async function createDashboard() {
const dashboard = {
dashboard: {
title: 'Node.js Performance Dashboard',
panels: [
{
title: 'Memory Usage',
type: 'graph',
targets: [
{
expr: 'nodejs_memory_usage_bytes{type="heapUsed"}',
legendFormat: 'Heap Used'
}
]
},
{
title: 'Request Rate',
type: 'graph',
targets: [
{
expr: 'rate(http_requests_total[5m])',
legendFormat: 'Requests/sec'
}
]
}
]
}
};
try {
await grafanaClient.dashboard.create(dashboard);
console.log('Dashboard created successfully');
} catch (error) {
console.error('Failed to create dashboard:', error);
}
}
# 5. Elastic APM Integration
const apm = require('elastic-apm-node');
// Initialize APM
apm.start({
serviceName: 'my-node-app',
serviceVersion: process.env.APP_VERSION,
environment: process.env.NODE_ENV,
serverUrl: process.env.APM_SERVER_URL,
secretToken: process.env.APM_SECRET_TOKEN
});
// Custom spans
const span = apm.startSpan('custom-operation');
span.setLabel('user.id', userId);
span.setLabel('operation.type', 'database');
span.end();
// Custom metrics
apm.addLabels({
'user.id': userId,
'environment': process.env.NODE_ENV
});
# 6. Sentry Integration
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1
});
// Performance monitoring
Sentry.addBreadcrumb({
message: 'User performed action',
category: 'user',
level: 'info',
data: {
action: 'login',
userId: userId
}
});
// Custom performance spans
const transaction = Sentry.startTransaction({
name: 'User Login',
op: 'auth.login'
});
const span = transaction.startChild({
op: 'db.query',
description: 'SELECT * FROM users WHERE id = ?'
});
// Your database query here
span.finish();
transaction.finish();
# 7. Custom Monitoring Service
class CustomMonitoringService {
constructor(config) {
this.config = config;
this.metrics = new Map();
this.alerts = [];
}
async sendMetrics(metrics) {
try {
const response = await fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.apiKey}`
},
body: JSON.stringify({
service: this.config.serviceName,
timestamp: new Date().toISOString(),
metrics
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
console.log('Metrics sent successfully');
} catch (error) {
console.error('Failed to send metrics:', error);
}
}
async checkAlerts(metrics) {
for (const alert of this.alerts) {
if (alert.condition(metrics)) {
await this.sendAlert(alert, metrics);
}
}
}
async sendAlert(alert, metrics) {
try {
const response = await fetch(this.config.alertEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.apiKey}`
},
body: JSON.stringify({
alert: alert.name,
message: alert.message,
metrics,
timestamp: new Date().toISOString()
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
console.log('Alert sent successfully');
} catch (error) {
console.error('Failed to send alert:', error);
}
}
addAlert(name, condition, message) {
this.alerts.push({ name, condition, message });
}
}
const monitoringService = new CustomMonitoringService({
endpoint: process.env.MONITORING_ENDPOINT,
alertEndpoint: process.env.ALERT_ENDPOINT,
apiKey: process.env.MONITORING_API_KEY,
serviceName: process.env.SERVICE_NAME
});
# 8. Real-time Monitoring
const WebSocket = require('ws');
class RealTimeMonitor {
constructor(port = 8080) {
this.wss = new WebSocket.Server({ port });
this.clients = new Set();
this.wss.on('connection', (ws) => {
this.clients.add(ws);
console.log('Client connected to monitoring');
ws.on('close', () => {
this.clients.delete(ws);
console.log('Client disconnected from monitoring');
});
});
this.startBroadcasting();
}
startBroadcasting() {
setInterval(() => {
const metrics = {
timestamp: new Date().toISOString(),
memory: process.memoryUsage(),
cpu: cpuMonitor.getCPUUsage(),
eventLoop: eventLoopMonitor.getLag(),
requests: requestMonitor.getStats(),
database: dbMonitor.getStats()
};
this.broadcast(metrics);
}, 1000); // Broadcast every second
}
broadcast(data) {
const message = JSON.stringify(data);
this.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
}
const realTimeMonitor = new RealTimeMonitor();
# 9. Log-based Monitoring
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console()
]
});
// Performance logging
function logPerformance(operation, duration, metadata = {}) {
logger.info('Performance metric', {
operation,
duration,
timestamp: new Date().toISOString(),
...metadata
});
}
// Error logging
function logError(error, context = {}) {
logger.error('Application error', {
error: {
name: error.name,
message: error.message,
stack: error.stack
},
context,
timestamp: new Date().toISOString()
});
}
# 10. Monitoring Configuration
const monitoringConfig = {
enabled: process.env.MONITORING_ENABLED === 'true',
interval: parseInt(process.env.MONITORING_INTERVAL) || 30000,
thresholds: {
memory: parseInt(process.env.MEMORY_THRESHOLD) || 100 * 1024 * 1024,
cpu: parseFloat(process.env.CPU_THRESHOLD) || 80,
eventLoop: parseInt(process.env.EVENT_LOOP_THRESHOLD) || 10,
responseTime: parseInt(process.env.RESPONSE_TIME_THRESHOLD) || 1000
},
alerts: {
enabled: process.env.ALERTS_ENABLED === 'true',
email: process.env.ALERT_EMAIL,
webhook: process.env.ALERT_WEBHOOK
}
};
module.exports = {
monitoringConfig,
CustomMonitoringService,
RealTimeMonitor
};
Monitoring Best Practices
Performance Monitoring Strategies
Monitoring Principles
- Monitor key metrics
- Set appropriate thresholds
- Use multiple monitoring tools
- Implement alerting
- Regular performance reviews
- Capacity planning
- Continuous optimization
Key Metrics
- Response time
- Throughput
- Error rate
- Memory usage
- CPU utilization
- Event loop lag
- Database performance
Summary
Node.js performance monitoring involves several key components:
- Built-in Monitoring: Performance hooks, memory monitoring, and CPU tracking
- External Tools: APM solutions, Prometheus, Grafana, and custom monitoring
- Best Practices: Key metrics, thresholds, alerting, and continuous optimization
- Real-time Monitoring: WebSocket-based monitoring and log-based tracking
Need More Help?
Struggling with performance monitoring or need help implementing comprehensive monitoring strategies? Our Node.js experts can help you set up effective monitoring solutions.
Get Monitoring Help