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
JavaScript
'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;
}
};
};