automc ui: per-route active connection counts + total in header
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:
@@ -74,6 +74,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Server address (host)</th>
|
<th>Server address (host)</th>
|
||||||
<th>Backend</th>
|
<th>Backend</th>
|
||||||
|
<th style="text-align:right;">Active</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="route-rows"></tbody>
|
<tbody id="route-rows"></tbody>
|
||||||
@@ -106,11 +107,14 @@ async function refreshRoutes() {
|
|||||||
count.textContent = '(' + j.routes.length + ')';
|
count.textContent = '(' + j.routes.length + ')';
|
||||||
for (const r of j.routes) {
|
for (const r of j.routes) {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = '<td>' + r.server_address + '</td><td>' + r.backend + '</td>';
|
tr.innerHTML = '<td>' + r.server_address + '</td>' +
|
||||||
|
'<td>' + r.backend + '</td>' +
|
||||||
|
'<td style="text-align:right;font-variant-numeric:tabular-nums;">' + r.active_connections + '</td>';
|
||||||
rows.appendChild(tr);
|
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) {
|
} catch (e) {
|
||||||
document.getElementById('meta').textContent = '— api error';
|
document.getElementById('meta').textContent = '— api error';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/itzg/mc-router/server"
|
"github.com/itzg/mc-router/server"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/sirupsen/logrus"
|
"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
|
// 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
|
// 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 {
|
type RouteSnapshot struct {
|
||||||
ServerAddress string `json:"server_address"`
|
ServerAddress string `json:"server_address"`
|
||||||
Backend string `json:"backend"`
|
Backend string `json:"backend"`
|
||||||
|
ActiveConnections int `json:"active_connections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func routesSnapshotHandler(w http.ResponseWriter, _ *http.Request) {
|
func routesSnapshotHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
mappings := server.Routes.GetMappings()
|
mappings := server.Routes.GetMappings()
|
||||||
|
perRoute, total := scrapeActiveConnections()
|
||||||
|
|
||||||
out := make([]RouteSnapshot, 0, len(mappings))
|
out := make([]RouteSnapshot, 0, len(mappings))
|
||||||
for addr, backend := range 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 })
|
sort.Slice(out, func(i, j int) bool { return out[i].ServerAddress < out[j].ServerAddress })
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"routes": out,
|
"routes": out,
|
||||||
"at": time.Now(),
|
"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 {
|
func sseLogsHandler(bus *LogBus) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
|||||||
Reference in New Issue
Block a user