/** * Simple metrics collector for DashCaddy API * Tracks request counts, durations, errors, and business metrics * No external dependencies — all in-memory */ class Metrics { constructor() { this.startTime = Date.now(); this.requests = { total: 0, byStatus: {}, byMethod: {}, byPath: {}, }; this.errors = { total: 0, byType: {}, }; this.business = { containersDeployed: 0, containersDeleted: 0, containerUpdates: 0, dnsRecordsCreated: 0, backupsCreated: 0, totpLogins: 0, siteAdded: 0, siteRemoved: 0, credentialRotations: 0, }; } recordRequest(method, path, statusCode, durationMs) { this.requests.total++; this.requests.byStatus[statusCode] = (this.requests.byStatus[statusCode] || 0) + 1; this.requests.byMethod[method] = (this.requests.byMethod[method] || 0) + 1; const normalized = this.normalizePath(path); if (!this.requests.byPath[normalized]) { this.requests.byPath[normalized] = { count: 0, totalDuration: 0 }; } const entry = this.requests.byPath[normalized]; entry.count++; entry.totalDuration += durationMs; } recordError(errorType) { this.errors.total++; this.errors.byType[errorType] = (this.errors.byType[errorType] || 0) + 1; } recordBusinessEvent(eventType) { if (eventType in this.business) { this.business[eventType]++; } } normalizePath(p) { return p .replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/:id') .replace(/\/[0-9a-f]{12,}/gi, '/:id') .replace(/\/\d+/g, '/:n'); } getSummary() { const uptimeMs = Date.now() - this.startTime; const uptimeSec = Math.floor(uptimeMs / 1000); const topEndpoints = Object.entries(this.requests.byPath) .sort((a, b) => b[1].count - a[1].count) .slice(0, 15) .map(([path, s]) => ({ path, count: s.count, avgMs: Math.round(s.totalDuration / s.count) })); return { uptime: { ms: uptimeMs, human: this.formatUptime(uptimeSec) }, requests: { total: this.requests.total, perSecond: uptimeSec > 0 ? +(this.requests.total / uptimeSec).toFixed(2) : 0, byStatus: this.requests.byStatus, byMethod: this.requests.byMethod, topEndpoints, }, errors: { total: this.errors.total, rate: this.requests.total > 0 ? +((this.errors.total / this.requests.total) * 100).toFixed(2) : 0, byType: this.errors.byType, }, business: this.business, process: { memory: process.memoryUsage(), pid: process.pid, nodeVersion: process.version, }, }; } formatUptime(sec) { const d = Math.floor(sec / 86400); const h = Math.floor((sec % 86400) / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; if (d > 0) return `${d}d ${h}h ${m}m`; if (h > 0) return `${h}h ${m}m ${s}s`; if (m > 0) return `${m}m ${s}s`; return `${s}s`; } reset() { this.startTime = Date.now(); this.requests = { total: 0, byStatus: {}, byMethod: {}, byPath: {} }; this.errors = { total: 0, byType: {} }; } } module.exports = new Metrics();