/** * 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: {} }; } /** * DC-097: Prometheus text-format export for /metrics/prometheus * Returns standard Prometheus exposition format text. */ toPrometheus() { const uptimeSec = Math.floor((Date.now() - this.startTime) / 1000); const mem = process.memoryUsage(); const lines = []; lines.push('# HELP dashcaddy_uptime_seconds Server uptime in seconds'); lines.push('# TYPE dashcaddy_uptime_seconds counter'); lines.push(`dashcaddy_uptime_seconds ${uptimeSec}`); lines.push('# HELP dashcaddy_requests_total Total HTTP requests'); lines.push('# TYPE dashcaddy_requests_total counter'); lines.push(`dashcaddy_requests_total ${this.requests.total}`); for (const [status, count] of Object.entries(this.requests.byStatus || {})) { lines.push(`dashcaddy_requests_by_status{status="${status}"} ${count}`); } for (const [method, count] of Object.entries(this.requests.byMethod || {})) { lines.push(`dashcaddy_requests_by_method{method="${method}"} ${count}`); } lines.push('# HELP dashcaddy_errors_total Total errors'); lines.push('# TYPE dashcaddy_errors_total counter'); lines.push(`dashcaddy_errors_total ${this.errors.total}`); lines.push('# HELP dashcaddy_containers_deployed Total containers deployed'); lines.push('# TYPE dashcaddy_containers_deployed counter'); lines.push(`dashcaddy_containers_deployed ${this.business.containersDeployed}`); lines.push('# HELP dashcaddy_process_memory_heap_used_bytes Heap memory used'); lines.push('# TYPE dashcaddy_process_memory_heap_used_bytes gauge'); lines.push(`dashcaddy_process_memory_heap_used_bytes ${mem.heapUsed}`); lines.push('# HELP dashcaddy_process_memory_heap_total_bytes Heap memory allocated'); lines.push('# TYPE dashcaddy_process_memory_heap_total_bytes gauge'); lines.push(`dashcaddy_process_memory_heap_total_bytes ${mem.heapTotal}`); lines.push('# HELP dashcaddy_business_metric Business metrics'); lines.push('# TYPE dashcaddy_business_metric counter'); for (const [key, val] of Object.entries(this.business)) { lines.push(`dashcaddy_business_metric{metric="${key}"} ${val}`); } return lines.join('\n') + '\n'; } } module.exports = new Metrics();