Files
dashcaddy/dashcaddy-api/src/monitoring/metrics.js
T
Krystie 503de258b8 [grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work
Committed by Hermes autonomous QA sprint 2026-08-13.
These files were modified during the Aug 12 sprint but never committed.
2026-08-12 17:34:10 -07:00

166 lines
5.3 KiB
JavaScript

/**
* 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();