diff --git a/internal/automc/static/index.html b/internal/automc/static/index.html index 0259e5b..587725b 100644 --- a/internal/automc/static/index.html +++ b/internal/automc/static/index.html @@ -74,6 +74,7 @@ Server address (host) Backend + Active @@ -106,11 +107,14 @@ async function refreshRoutes() { count.textContent = '(' + j.routes.length + ')'; for (const r of j.routes) { const tr = document.createElement('tr'); - tr.innerHTML = '' + r.server_address + '' + r.backend + ''; + tr.innerHTML = '' + r.server_address + '' + + '' + r.backend + '' + + '' + r.active_connections + ''; rows.appendChild(tr); } } - document.getElementById('meta').textContent = '— ' + j.routes.length + ' routes'; + document.getElementById('meta').textContent = + '— ' + j.routes.length + ' routes · ' + (j.total_connections || 0) + ' active conn'; } catch (e) { document.getElementById('meta').textContent = '— api error'; } diff --git a/internal/automc/uipage.go b/internal/automc/uipage.go index fc3e81e..99b0a97 100644 --- a/internal/automc/uipage.go +++ b/internal/automc/uipage.go @@ -13,6 +13,7 @@ import ( "time" "github.com/itzg/mc-router/server" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" ) @@ -56,26 +57,80 @@ func startUI(ctx context.Context, uiBinding string, bus *LogBus) { // RouteSnapshot is one row of the routes table the UI renders. Same shape as // the upstream /routes JSON but flatter — the UI doesn't need both backend -// and scalingTarget shown separately. +// and scalingTarget shown separately. ActiveConnections is the live gauge +// value scraped from the Prometheus registry (-1 if the metric isn't +// registered, which can happen briefly at startup). type RouteSnapshot struct { - ServerAddress string `json:"server_address"` - Backend string `json:"backend"` + ServerAddress string `json:"server_address"` + Backend string `json:"backend"` + ActiveConnections int `json:"active_connections"` } func routesSnapshotHandler(w http.ResponseWriter, _ *http.Request) { mappings := server.Routes.GetMappings() + perRoute, total := scrapeActiveConnections() + out := make([]RouteSnapshot, 0, len(mappings)) for addr, backend := range mappings { - out = append(out, RouteSnapshot{ServerAddress: addr, Backend: backend}) + conns, ok := perRoute[addr] + if !ok { + conns = 0 + } + out = append(out, RouteSnapshot{ServerAddress: addr, Backend: backend, ActiveConnections: conns}) } sort.Slice(out, func(i, j int) bool { return out[i].ServerAddress < out[j].ServerAddress }) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ - "routes": out, - "at": time.Now(), + "routes": out, + "total_connections": total, + "at": time.Now(), }) } +// scrapeActiveConnections walks the Prometheus default registry to extract +// two gauges that mc-router exposes: +// +// mc_router_active_connections (no labels) +// mc_router_server_active_connections{server_address=…} (per route) +// +// Returns (perServerAddress, total). On any gathering error returns zero +// values silently — the UI shows 0 rather than blocking on a metrics issue. +func scrapeActiveConnections() (map[string]int, int) { + per := map[string]int{} + total := 0 + families, err := prometheus.DefaultGatherer.Gather() + if err != nil { + return per, 0 + } + for _, mf := range families { + switch mf.GetName() { + case "mc_router_active_connections": + for _, m := range mf.GetMetric() { + if g := m.GetGauge(); g != nil { + total = int(g.GetValue()) + } + } + case "mc_router_server_active_connections": + for _, m := range mf.GetMetric() { + var addr string + for _, lp := range m.GetLabel() { + if lp.GetName() == "server_address" { + addr = lp.GetValue() + break + } + } + if addr == "" { + continue + } + if g := m.GetGauge(); g != nil { + per[addr] = int(g.GetValue()) + } + } + } + } + return per, total +} + func sseLogsHandler(bus *LogBus) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream")