automc ui: per-route active connection counts + total in header
CI / validate (push) Successful in 15s
CI / docker (push) Successful in 12s

Pulls mc_router_active_connections + mc_router_server_active_connections
from the Prometheus default registry on every /api/routes call. UI
gains an Active column on the routes table and the header subtitle
shows "N routes · M active conn".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 18:39:59 +02:00
parent b995e012be
commit 39d7d4586b
2 changed files with 67 additions and 8 deletions
+61 -6
View File
@@ -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")