Skip to content
All essays
WebMarch 22, 202416 min

Application Performance Monitoring: From Metrics to Insights

Learn how to implement comprehensive APM solutions to gain deep insights into your application performance

Ü
Ümit Uz
Mobile & Full Stack Developer

Application Performance Monitoring (APM) goes beyond basic monitoring. It provides deep insights into how your application performs, where bottlenecks exist, and how users experience your software. Let's explore how to implement effective APM.

What is APM?

APM focuses specifically on application performance and user experience. While infrastructure monitoring tells you if servers are running, APM tells you if your application is performing well.

The Three Pillars of APM

  1. 1Real User Monitoring (RUM): Actual user experiences in production
  2. 2Synthetic Monitoring: Simulated user interactions
  3. 3Code-Level Monitoring: Deep performance insights into application code

Real User Monitoring (RUM)

RUM captures how actual users experience your application.

Implementing RUM in Web Applications

typescript
// RUM data collection
class RumCollector {
  private metrics: PerformanceEntry[] = [];

  constructor() {
    this.initNavigationTiming();
    this.initResourceTiming();
    this.initUserInteractions();
  }

  private initNavigationTiming() {
    window.addEventListener('load', () => {
      const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;

      const pageLoadMetrics = {
        dns: timing.domainLookupEnd - timing.domainLookupStart,
        tcp: timing.connectEnd - timing.connectStart,
        ttfb: timing.responseStart - timing.requestStart,
        download: timing.responseEnd - timing.responseStart,
        domProcessing: timing.domComplete - timing.domInteractive,
        totalLoadTime: timing.loadEventEnd - timing.fetchStart
      };

      this.sendMetrics('page-load', pageLoadMetrics);
    });
  }

  private initResourceTiming() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource') {
          const resource = entry as PerformanceResourceTiming;
          this.trackResource(resource);
        }
      }
    });

    observer.observe({ entryTypes: ['resource'] });
  }

  private trackResource(resource: PerformanceResourceTiming) {
    const metrics = {
      name: resource.name,
      type: resource.initiatorType,
      duration: resource.duration - (resource.transferSize || 0),
      size: resource.transferSize,
      cached: resource.transferSize === 0
    };

    this.sendMetrics('resource-load', metrics);
  }

  private initUserInteractions() {
    document.addEventListener('click', (event) => {
      const target = event.target as HTMLElement;
      const start = performance.now();

      requestAnimationFrame(() => {
        const end = performance.now();
        const inputDelay = end - start;

        this.sendMetrics('interaction', {
          type: 'click',
          element: target.tagName,
          delay: inputDelay
        });
      });
    }, true);
  }

  private sendMetrics(type: string, data: any) {
    // Send to your analytics endpoint
    fetch('/api/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ type, data, timestamp: Date.now() })
    }).catch(console.error);
  }
}

// Initialize RUM
new RumCollector();

Core Web Vitals

Google's Core Web Vitals are essential user experience metrics:

typescript
class CoreWebVitals {
  constructor() {
    this.measureLCP();
    this.measureFID();
    this.measureCLS();
  }

  private measureLCP() {
    // Largest Contentful Paint
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.sendMetric('LCP', lastEntry.startTime);
    }).observe({ entryTypes: ['largest-contentful-paint'] });
  }

  private measureFID() {
    // First Input Delay
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        this.sendMetric('FID', entry.processingStart - entry.startTime);
      }
    }).observe({ entryTypes: ['first-input'] });
  }

  private measureCLS() {
    // Cumulative Layout Shift
    let clsValue = 0;
    let sessionValue = 0;
    let sessionEntries: PerformanceEntry[] = [];

    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          const firstSessionEntry = sessionEntries[0];
          const lastSessionEntry = sessionEntries[sessionEntries.length - 1];

          if (sessionValue &&
              entry.startTime - lastSessionEntry.startTime < 1000 &&
              entry.startTime - firstSessionEntry.startTime < 5000) {
            sessionValue += entry.value;
            sessionEntries.push(entry as PerformanceEntry);
          } else {
            sessionValue = entry.value;
            sessionEntries = [entry as PerformanceEntry];
          }

          if (sessionValue > clsValue) {
            clsValue = sessionValue;
            this.sendMetric('CLS', clsValue);
          }
        }
      }
    }).observe({ entryTypes: ['layout-shift'] });
  }

  private sendMetric(name: string, value: number) {
    console.log(`${name}: ${value.toFixed(2)}`);
    // Send to analytics
  }
}

new CoreWebVitals();

Distributed Tracing

Distributed tracing tracks requests as they travel through microservices.

OpenTelemetry for Tracing

typescript
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { JaegerExporter } from '@opentelemetry/exporter-trace-jaeger';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';

const provider = new NodeTracerProvider({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-service',
    [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'production'
  })
});

const exporter = new JaegerExporter({
  endpoint: 'http://jaeger:14268/api/traces'
});

provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();

registerInstrumentations({
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation()
  ]
});

// Manual tracing
import { trace } from '@opentelemetry/api';

app.get('/api/users', async (req, res) => {
  const tracer = trace.getTracer('my-service');
  const span = tracer.startSpan('get-users');

  try {
    span.setAttribute('user.id', req.user?.id);
    const users = await fetchUsers();
    span.setStatus({ code: SpanStatusCode.OK });
    res.json(users);
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: SpanStatusCode.ERROR });
    res.status(500).json({ error: 'Internal error' });
  } finally {
    span.end();
  }
});

Error Tracking

Track and analyze errors in production.

Global Error Handler

typescript
class ErrorTracker {
  private errors: ErrorReport[] = [];

  init() {
    // Global error handler
    globalErrorHandler?.addEventListener('error', (event) => {
      this.trackError({
        message: event.message,
        stack: event.error?.stack,
        url: event.filename,
        line: event.lineno,
        col: event.colno,
        type: 'uncaught error'
      });
    });

    // Unhandled promise rejections
    globalErrorHandler?.addEventListener('unhandledrejection', (event) => {
      this.trackError({
        message: event.reason?.message || 'Unhandled Promise Rejection',
        stack: event.reason?.stack,
        type: 'unhandled rejection'
      });
    });

    // API errors
    this.interceptFetch();
  }

  private interceptFetch() {
    const originalFetch = window.fetch;
    window.fetch = async (...args) => {
      try {
        return await originalFetch(...args);
      } catch (error) {
        this.trackError({
          message: error.message,
          stack: error.stack,
          type: 'network error',
          url: args[0]
        });
        throw error;
      }
    };
  }

  private trackError(error: ErrorReport) {
    const report = {
      ...error,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      url: window.location.href,
      userId: this.getCurrentUserId()
    };

    this.errors.push(report);
    this.sendErrorReport(report);
  }

  private getCurrentUserId(): string | undefined {
    // Get user ID from your auth system
    return (window as any).userId;
  }

  private sendErrorReport(report: ErrorReport) {
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(report)
    }).catch(console.error);
  }
}

interface ErrorReport {
  message: string;
  stack?: string;
  url?: string;
  line?: number;
  col?: number;
  type: string;
  timestamp?: number;
  userAgent?: string;
  userId?: string;
}

new ErrorTracker().init();

Performance Profiling

Profile your application to identify bottlenecks.

CPU Profiling

typescript
import { Inspector } from 'inspector';

class Profiler {
  private session: Inspector.Session | null = null;

  startProfiling() {
    this.session = new Inspector.Session();
    this.session.connect();

    this.session.post('Profiler.enable', () => {
      this.session!.post('Profiler.start');
    });
  }

  stopProfiling() {
    if (!this.session) return;

    this.session.post('Profiler.stop', (err, { profile }) => {
      if (err) {
        console.error('Error stopping profiler:', err);
        return;
      }

      this.saveProfile(profile);
      this.session!.disconnect();
      this.session = null;
    });
  }

  private saveProfile(profile: any) {
    const fs = require('fs');
    fs.writeFileSync(
      `profile-${Date.now()}.cpuprofile`,
      JSON.stringify(profile)
    );
  }
}

// Usage
const profiler = new Profiler();

// Start profiling for 30 seconds
profiler.startProfiling();
setTimeout(() => profiler.stopProfiling(), 30000);

Memory Profiling

typescript
class MemoryProfiler {
  startIntervalProfiling(interval: number = 60000) {
    setInterval(() => {
      const memUsage = process.memoryUsage();
      const heapStats = v8.getHeapStatistics();

      const profile = {
        timestamp: Date.now(),
        heapUsed: memUsage.heapUsed,
        heapTotal: memUsage.heapTotal,
        external: memUsage.external,
        arrayBuffers: memUsage.arrayBuffers,
        heapSizeLimit: heapStats.heap_size_limit,
        totalAvailableSize: heapStats.total_available_size,
        usedHeapSize: heapStats.used_heap_size
      };

      this.sendProfile(profile);
    }, interval);
  }

  private sendProfile(profile: any) {
    // Send to monitoring system
  }

  takeHeapSnapshot() {
    const v8 = require('v8');
    const fs = require('fs');

    const snapshotStream = v8.getHeapSnapshot();
    const fileStream = fs.createWriteStream(`heap-${Date.now()}.heapsnapshot`);

    snapshotStream.pipe(fileStream);
  }
}

Database Performance Monitoring

Monitor database queries and performance.

typescript
import { createPool } from 'mysql2/promise';

class DatabaseMonitor {
  private queryTimes: Map<string, number[]> = new Map();

  async query(sql: string, params?: any[]) {
    const start = process.hrtime.bigint();
    const connection = await this.getConnection();

    try {
      const [results] = await connection.execute(sql, params);
      const duration = Number(process.hrtime.bigint() - start) / 1000000;

      this.recordQuery(sql, duration);
      return results;
    } catch (error) {
      this.recordQuery(sql, -1); // Error
      throw error;
    } finally {
      connection.release();
    }
  }

  private recordQuery(sql: string, duration: number) {
    const normalized = this.normalizeSQL(sql);

    if (!this.queryTimes.has(normalized)) {
      this.queryTimes.set(normalized, []);
    }

    this.queryTimes.get(normalized)!.push(duration);
  }

  private normalizeSQL(sql: string): string {
    return sql
      .replace(/d+/g, '?')
      .replace(/'[^']*'/g, '?')
      .replace(/s+/g, ' ')
      .trim();
  }

  getSlowQueries(threshold: number = 1000) {
    const slowQueries: Array<{ sql: string; avgTime: number; count: number }> = [];

    for (const [sql, times] of this.queryTimes.entries()) {
      const validTimes = times.filter(t => t > 0);
      if (validTimes.length === 0) continue;

      const avgTime = validTimes.reduce((a, b) => a + b, 0) / validTimes.length;

      if (avgTime > threshold) {
        slowQueries.push({
          sql,
          avgTime,
          count: validTimes.length
        });
      }
    }

    return slowQueries.sort((a, b) => b.avgTime - a.avgTime);
  }
}

API Performance Monitoring

Monitor your API endpoints.

typescript
class ApiMonitor {
  private endpoints: Map<string, EndpointStats> = new Map();

  recordRequest(req: any, res: any, duration: number) {
    const key = `${req.method}:${req.route?.path || req.path}`;

    if (!this.endpoints.has(key)) {
      this.endpoints.set(key, {
        requests: 0,
        errors: 0,
        totalDuration: 0,
        minDuration: Infinity,
        maxDuration: 0,
        statusCodes: {}
      });
    }

    const stats = this.endpoints.get(key)!;
    stats.requests++;
    stats.totalDuration += duration;
    stats.minDuration = Math.min(stats.minDuration, duration);
    stats.maxDuration = Math.max(stats.maxDuration, duration);

    if (res.statusCode >= 400) {
      stats.errors++;
    }

    stats.statusCodes[res.statusCode] = (stats.statusCodes[res.statusCode] || 0) + 1;
  }

  getStats() {
    const result: any[] = [];

    for (const [endpoint, stats] of this.endpoints.entries()) {
      result.push({
        endpoint,
        requests: stats.requests,
        errors: stats.errors,
        errorRate: stats.errors / stats.requests,
        avgDuration: stats.totalDuration / stats.requests,
        minDuration: stats.minDuration,
        maxDuration: stats.maxDuration,
        statusCodes: stats.statusCodes
      });
    }

    return result.sort((a, b) => b.avgDuration - a.avgDuration);
  }
}

interface EndpointStats {
  requests: number;
  errors: number;
  totalDuration: number;
  minDuration: number;
  maxDuration: number;
  statusCodes: Record<number, number>;
}

Alerting on Performance Issues

Set up intelligent alerts.

typescript
class PerformanceAlerting {
  checkThresholds(stats: any[]) {
    const issues: string[] = [];

    for (const stat of stats) {
      if (stat.avgDuration > 1000) {
        issues.push(`${stat.endpoint} has high average latency (${stat.avgDuration}ms)`);
      }

      if (stat.errorRate > 0.05) {
        issues.push(`${stat.endpoint} has high error rate (${(stat.errorRate * 100).toFixed(2)}%)`);
      }

      if (stat.maxDuration > 5000) {
        issues.push(`${stat.endpoint} has very slow requests (${stat.maxDuration}ms max)`);
      }
    }

    if (issues.length > 0) {
      this.sendAlert(issues);
    }
  }

  private sendAlert(issues: string[]) {
    // Send to your alerting system
    console.warn('Performance issues detected:', issues);
  }
}

Complete APM Implementation

typescript
import express from 'express';

const app = express();

const apiMonitor = new ApiMonitor();
const performanceAlerting = new PerformanceAlerting();

app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    apiMonitor.recordRequest(req, res, duration);
  });

  next();
});

app.get('/api/monitoring/stats', (req, res) => {
  const stats = apiMonitor.getStats();
  res.json(stats);
});

// Check performance every minute
setInterval(() => {
  const stats = apiMonitor.getStats();
  performanceAlerting.checkThresholds(stats);
}, 60000);

app.listen(8080);

Conclusion

APM provides deep insights into your application's performance and user experience. By implementing RUM, distributed tracing, error tracking, and performance profiling, you gain the visibility needed to build fast, reliable applications.

Remember: APM is an ongoing practice. Continuously monitor, analyze, and optimize based on the insights you gather. Your users will thank you for the great experience.

Related essays

Next essay
Complete Guide to GitHub Actions for CI/CD