diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index 29bbfa0..97a78a0 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -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); diff --git a/dashcaddy-api/src/monitoring/metrics.js b/dashcaddy-api/src/monitoring/metrics.js index 09b196d..ffbd847 100644 --- a/dashcaddy-api/src/monitoring/metrics.js +++ b/dashcaddy-api/src/monitoring/metrics.js @@ -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(); diff --git a/dashcaddy-api/src/utilities/middleware.js b/dashcaddy-api/src/utilities/middleware.js index 510c195..7c257bb 100644 --- a/dashcaddy-api/src/utilities/middleware.js +++ b/dashcaddy-api/src/utilities/middleware.js @@ -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' },