UNPKG

observio

Version:

A developer-focused monitoring package for Express apps with real-time performance insights, slow API tracking, and clean dashboard UI.

358 lines (301 loc) 13.1 kB
'use strict'; const express = require('express'); const os = require('os'); const path = require('path'); /** * Simple, reliable monitoring for Express apps */ module.exports = function apexMonitoring(userApp, opts = {}) { const basePath = (opts.path || '/monitor').replace(/\/+$/, ''); const updateInterval = opts.updateInterval || 2000; const demoMode = Boolean(opts.demo || process.env.APEX_MONITOR_DEMO === '1'); const monitorApp = express(); // Simple, reliable metrics storage let metrics = { // System metrics cpu: 0, memory: { used: 0, total: 0, free: 0 }, // Request metrics totalRequests: 0, requestsPerSecond: 0, activeConnections: 0, // Response time metrics avgResponseTime: 0, maxResponseTime: 0, minResponseTime: Infinity, allResponseTimes: [], // Error metrics totalErrors: 0, errorRate: 0, // API metrics apis: new Map(), // route -> { count, totalTime, maxTime, minTime, errors } // Status codes statusCodes: {}, // Previous values for trends prevCpu: 0, prevMemory: 0, prevRequests: 0, prevErrors: 0 }; // Request counter for RPS calculation let requestCount = 0; let requestTimes = []; // Update system metrics function updateSystemMetrics() { // CPU calculation const cpus = os.cpus(); let totalIdle = 0, totalTick = 0; cpus.forEach(cpu => { for (let type in cpu.times) { totalTick += cpu.times[type]; } totalIdle += cpu.times.idle; }); const idle = totalIdle / cpus.length; const total = totalTick / cpus.length; metrics.cpu = Math.round((100 - (100 * idle / total)) * 100) / 100; // Memory calculation const memUsage = process.memoryUsage(); const totalMem = os.totalmem(); const freeMem = os.freemem(); metrics.memory.used = Math.round(memUsage.rss / 1024 / 1024); // MB metrics.memory.total = Math.round(totalMem / 1024 / 1024); // MB metrics.memory.free = Math.round(freeMem / 1024 / 1024); // MB } // Calculate RPS function calculateRPS() { const now = Date.now(); requestTimes = requestTimes.filter(time => now - time < 1000); // Keep last 1 second metrics.requestsPerSecond = requestTimes.length; } // Update API metrics function updateApiMetrics() { const apiArray = Array.from(metrics.apis.entries()).map(([route, data]) => ({ route, count: data.count, avgTime: data.count > 0 ? (data.totalTime / data.count) : 0, maxTime: data.maxTime, minTime: data.minTime === Infinity ? 0 : data.minTime, errors: data.errors, errorRate: data.count > 0 ? (data.errors / data.count) * 100 : 0 })).sort((a, b) => b.avgTime - a.avgTime); return apiArray.slice(0, 10); // Top 10 slowest APIs } function computePercentile(values, percentile) { if (!values || values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const idx = Math.min(sorted.length - 1, Math.floor((percentile / 100) * sorted.length)); return sorted[idx]; } // Request tracking middleware function trackingMiddleware(req, res, next) { if (req.originalUrl && req.originalUrl.startsWith(basePath)) return next(); const start = Date.now(); metrics.activeConnections++; metrics.totalRequests++; requestCount++; requestTimes.push(start); const route = req.route?.path || req.path || req.originalUrl || 'unknown'; const method = req.method; res.on('finish', () => { const duration = (Date.now() - start) / 1000; // Convert to seconds metrics.activeConnections--; // Track response times metrics.allResponseTimes.push(duration); if (metrics.allResponseTimes.length > 1000) { metrics.allResponseTimes.shift(); } // Update min/max response times metrics.maxResponseTime = Math.max(metrics.maxResponseTime, duration); metrics.minResponseTime = Math.min(metrics.minResponseTime, duration); // Calculate average response time if (metrics.allResponseTimes.length > 0) { metrics.avgResponseTime = metrics.allResponseTimes.reduce((a, b) => a + b, 0) / metrics.allResponseTimes.length; } // Track API metrics const routeKey = `${method} ${route}`; if (!metrics.apis.has(routeKey)) { metrics.apis.set(routeKey, { count: 0, totalTime: 0, maxTime: 0, minTime: Infinity, errors: 0 }); } const apiData = metrics.apis.get(routeKey); apiData.count++; apiData.totalTime += duration; apiData.maxTime = Math.max(apiData.maxTime, duration); apiData.minTime = Math.min(apiData.minTime, duration); const statusCode = res.statusCode || 200; // Track errors if (statusCode >= 400) { metrics.totalErrors++; apiData.errors++; } // Update error rate metrics.errorRate = metrics.totalRequests > 0 ? (metrics.totalErrors / metrics.totalRequests) * 100 : 0; // Update status codes if (!metrics.statusCodes[statusCode]) { metrics.statusCodes[statusCode] = 0; } metrics.statusCodes[statusCode]++; }); next(); } // Periodic updates setInterval(() => { // Optional demo mode to generate synthetic traffic/metrics if (demoMode) { const now = Date.now(); const newRequests = Math.floor(5 + Math.random() * 15); const demoRoutes = [ { method: 'GET', path: '/api/users' }, { method: 'POST', path: '/api/login' }, { method: 'GET', path: '/api/orders' }, { method: 'PUT', path: '/api/user/:id' }, { method: 'DELETE', path: '/api/order/:id' }, { method: 'GET', path: '/health' } ]; for (let i = 0; i < newRequests; i++) { // Distribute timestamps across the last second requestTimes.push(now - Math.floor(Math.random() * 950)); metrics.totalRequests++; // Simulate a request const picked = demoRoutes[Math.floor(Math.random() * demoRoutes.length)]; const routeKey = `${picked.method} ${picked.path}`; if (!metrics.apis.has(routeKey)) { metrics.apis.set(routeKey, { count: 0, totalTime: 0, maxTime: 0, minTime: Infinity, errors: 0 }); } const apiData = metrics.apis.get(routeKey); // Simulate response time (seconds), biased by method/endpoint const base = picked.method === 'POST' || picked.method === 'PUT' ? 0.12 : 0.06; const jitter = (Math.random() ** 2) * 0.25; // skew towards smaller const duration = Math.max(0.01, base + jitter); metrics.allResponseTimes.push(duration); if (metrics.allResponseTimes.length > 1000) metrics.allResponseTimes.shift(); metrics.maxResponseTime = Math.max(metrics.maxResponseTime, duration); metrics.minResponseTime = Math.min(metrics.minResponseTime, duration); metrics.avgResponseTime = metrics.allResponseTimes.reduce((a, b) => a + b, 0) / metrics.allResponseTimes.length; apiData.count++; apiData.totalTime += duration; apiData.maxTime = Math.max(apiData.maxTime, duration); apiData.minTime = Math.min(apiData.minTime, duration); // Simulate status codes const r = Math.random(); let statusCode = 200; if (r < 0.04) statusCode = 500; // 4% server errors else if (r < 0.10) statusCode = 404; // 6% client errors else if (r < 0.14) statusCode = 302; // 4% redirects else if (r < 0.18) statusCode = 201; // 4% created if (!metrics.statusCodes[statusCode]) metrics.statusCodes[statusCode] = 0; metrics.statusCodes[statusCode]++; if (statusCode >= 400) { metrics.totalErrors++; apiData.errors++; } } // Keep statusCodes map from growing unbounded: drop very old rare codes const maxCodes = 50; const codeKeys = Object.keys(metrics.statusCodes); if (codeKeys.length > maxCodes) { codeKeys.sort((a, b) => metrics.statusCodes[a] - metrics.statusCodes[b]); for (let i = 0; i < codeKeys.length - maxCodes; i++) { delete metrics.statusCodes[codeKeys[i]]; } } } updateSystemMetrics(); calculateRPS(); }, updateInterval); // Initial update updateSystemMetrics(); // Apply middleware userApp.use(trackingMiddleware); // Serve static files monitorApp.use(express.static(path.join(__dirname, 'public'))); // Dashboard route monitorApp.get('/', (_req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // API endpoint monitorApp.get('/api/stats', (_req, res) => { updateSystemMetrics(); calculateRPS(); const slowApis = updateApiMetrics(); // Compute latency percentiles (seconds) const p95 = computePercentile(metrics.allResponseTimes, 95); // Calculate trends const cpuTrend = metrics.prevCpu ? ((metrics.cpu - metrics.prevCpu) / metrics.prevCpu * 100) : 0; const memoryTrend = metrics.prevMemory ? ((metrics.memory.used - metrics.prevMemory) / metrics.prevMemory * 100) : 0; const requestTrend = metrics.prevRequests ? ((metrics.totalRequests - metrics.prevRequests) / metrics.prevRequests * 100) : 0; const errorTrend = metrics.prevErrors ? ((metrics.totalErrors - metrics.prevErrors) / metrics.prevErrors * 100) : 0; // Update previous values metrics.prevCpu = metrics.cpu; metrics.prevMemory = metrics.memory.used; metrics.prevRequests = metrics.totalRequests; metrics.prevErrors = metrics.totalErrors; res.json({ system: { cpu: metrics.cpu, memory: metrics.memory, uptime: process.uptime(), nodeVersion: process.version, platform: os.platform(), arch: os.arch(), cpuCores: os.cpus().length }, requests: { total: metrics.totalRequests, perSecond: metrics.requestsPerSecond, activeConnections: metrics.activeConnections }, performance: { avgResponseTime: metrics.avgResponseTime, avgResponseTimeMs: Math.round(metrics.avgResponseTime * 1000), p95: p95, p95Ms: Math.round(p95 * 1000), maxResponseTime: metrics.maxResponseTime, maxResponseTimeMs: Math.round(metrics.maxResponseTime * 1000), minResponseTime: metrics.minResponseTime === Infinity ? 0 : metrics.minResponseTime, minResponseTimeMs: Math.round((metrics.minResponseTime === Infinity ? 0 : metrics.minResponseTime) * 1000), errorRate: metrics.errorRate, totalErrors: metrics.totalErrors }, slowApis: slowApis, statusCodes: metrics.statusCodes, trends: { cpu: cpuTrend, memory: memoryTrend, requests: requestTrend, errors: errorTrend }, timestamp: Date.now() }); }); // Mount the monitor app userApp.use(basePath, monitorApp); return { getMetrics: () => metrics, resetMetrics: () => { metrics.totalRequests = 0; metrics.totalErrors = 0; metrics.allResponseTimes = []; metrics.apis.clear(); metrics.statusCodes = {}; metrics.maxResponseTime = 0; metrics.minResponseTime = Infinity; } }; };