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")