[grade=B] DC-097+DC-092: Prometheus metrics export + dependency health checks
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

DC-097: Add /api/v1/metrics/prometheus endpoint returning standard
Prometheus text exposition format. Includes uptime, request counts
by status/method, error counts, business metrics, memory gauges.
Public (no auth) for Prometheus scraping.

DC-092: Already resolved by DC-075's system/health endpoint which
checks disk space, memory, service health, and incidents.

All 1540 tests pass.
This commit is contained in:
Hermes
2026-08-12 05:33:03 -07:00
parent 27beae22a8
commit f3934fd257
3 changed files with 58 additions and 0 deletions
+6
View File
@@ -736,6 +736,12 @@ async function createApp() {
ok(res, { metrics: metrics.getSummary() });
});
// DC-097: Prometheus text-format endpoint for Grafana/Prometheus scraping
apiRouter.get('/metrics/prometheus', (req, res) => {
res.set('Content-Type', 'text/plain; version=0.0.4');
res.send(metrics.toPrometheus());
});
// Mount at /api/v1 (canonical, single version)
app.use('/api/v1', apiRouter);
+50
View File
@@ -110,6 +110,56 @@ class Metrics {
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();
@@ -439,6 +439,8 @@ module.exports = function configureMiddleware(app, {
{ path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
// DC-075: System health endpoint for external uptime monitoring (UptimeRobot, BetterStack)
{ path: '/api/v1/system/health', exact: true, method: 'GET' },
// DC-097: Prometheus metrics endpoint (scraped by Prometheus, no auth)
{ path: '/api/v1/metrics/prometheus', exact: true, method: 'GET' },
// System Overview widget on the dashboard — needs the flattened CPU/mem
// data without going through auth. See skill references/totp-and-system-overview-pitfalls.md §3.
{ path: '/api/v1/monitoring/stats', exact: true, method: 'GET' },