`n

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