Files
Dash/apps/backend/internal/api/metrics.go
Tomas DvorakandDevin c84a4be876 feat: ops layer — incidents, maintenance, status pages, badges, metrics (merge plan M4)
incidents + incident_updates + maintenance_windows + status_pages
tables. Monitor transitions auto-open incidents on down and
auto-resolve on recovery; an active maintenance window covering
the monitor suppresses alerts and auto-incidents, and status
pages report the covered monitor as `maintenance`.

Public status view: GET /api/status-pages/:slug/public aggregates
overall state, monitor uptime, systems, open incidents, and
scheduled maintenance; rendered read-only at /status/:slug.

GET /api/badge/:kind/:id.svg serves stateless shields; GET
/api/metrics exposes Prometheus text over existing tables;
POST /api/import/csv?kind=monitors|domains does header-driven
bulk import; /api/export v2 folds monitors/domains/systems rows
into the backup payload.

Frontend: /incidents with timeline + status flow, /status admin
for pages + windows, /status/:slug public page; nav and palette
wired.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-21 19:21:21 +02:00

76 lines
2.8 KiB
Go

package api
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// /api/metrics — Prometheus text exposition over the tables we already have.
// No registry, no deps: the counts are one-line SQL each.
func promEscape(s string) string {
return strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", `\n`).Replace(s)
}
func (s *Server) metrics(c *gin.Context) {
var b strings.Builder
count := func(q string) int {
var n int
_ = s.db.QueryRow(q).Scan(&n)
return n
}
gauge := func(name, help string, v int) {
fmt.Fprintf(&b, "# HELP %s %s\n# TYPE %s gauge\n%s %d\n", name, help, name, name, v)
}
gauge("dash_monitors_total", "Configured monitors", count(`SELECT COUNT(*) FROM monitors`))
gauge("dash_monitors_up", "Monitors currently up", count(`SELECT COUNT(*) FROM monitors WHERE status='up'`))
gauge("dash_monitors_down", "Monitors currently down", count(`SELECT COUNT(*) FROM monitors WHERE status='down'`))
gauge("dash_domains_total", "Tracked domains", count(`SELECT COUNT(*) FROM domains`))
gauge("dash_domains_expired", "Domains past expiry", count(`SELECT COUNT(*) FROM domains WHERE expiry_date IS NOT NULL AND datetime(expiry_date) < datetime('now')`))
gauge("dash_domains_expiring_30d", "Domains expiring within 30 days",
count(`SELECT COUNT(*) FROM domains WHERE expiry_date IS NOT NULL
AND datetime(expiry_date) >= datetime('now')
AND datetime(expiry_date) <= datetime('now','+30 days')`))
gauge("dash_systems_total", "Registered systems", count(`SELECT COUNT(*) FROM systems`))
gauge("dash_systems_up", "Systems currently reporting", count(`SELECT COUNT(*) FROM systems WHERE status='up'`))
gauge("dash_systems_down", "Systems gone silent", count(`SELECT COUNT(*) FROM systems WHERE status='down'`))
gauge("dash_incidents_open", "Open or acknowledged incidents", count(`SELECT COUNT(*) FROM incidents WHERE status IN ('open','ack')`))
gauge("dash_heartbeats_total", "Recorded monitor heartbeats", count(`SELECT COUNT(*) FROM heartbeats`))
// Per-monitor up/down gauge — label cardinality is user-controlled and small.
b.WriteString("# TYPE dash_monitor_up gauge\n")
if rows, err := s.db.Query(`SELECT name, status FROM monitors`); err == nil {
for rows.Next() {
var name, st string
if rows.Scan(&name, &st) == nil {
v := 0
if st == "up" {
v = 1
}
fmt.Fprintf(&b, "dash_monitor_up{name=%q} %d\n", promEscape(name), v)
}
}
rows.Close()
}
b.WriteString("# TYPE dash_system_up gauge\n")
if rows, err := s.db.Query(`SELECT name, status FROM systems`); err == nil {
for rows.Next() {
var name, st string
if rows.Scan(&name, &st) == nil {
v := 0
if st == "up" {
v = 1
}
fmt.Fprintf(&b, "dash_system_up{name=%q} %d\n", promEscape(name), v)
}
}
rows.Close()
}
c.Data(http.StatusOK, "text/plain; version=0.0.4; charset=utf-8", []byte(b.String()))
}