All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m46s
CI/CD / test-sqlite (pull_request) Successful in 2m53s
CI/CD / build-and-deploy (pull_request) Successful in 1m42s
Add node_id to metrics snapshot so the health tab shows which instance the user is viewing from. Hoist nodeID computation so it works on both SQLite and Postgres deployments. Fix cluster node card overflow with text-overflow ellipsis and proper flex constraints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
4.8 KiB
Go
188 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"switchboard-core/metrics"
|
|
"switchboard-core/store"
|
|
)
|
|
|
|
// mockHub implements metrics.ConnCounter for tests.
|
|
type mockHub struct{ count int }
|
|
|
|
func (m *mockHub) ConnCount() int { return m.count }
|
|
|
|
// mockBus implements metrics.BusCounter for tests.
|
|
type mockBus struct{ pub, del int64 }
|
|
|
|
func (m *mockBus) PublishCount() int64 { return m.pub }
|
|
func (m *mockBus) DeliverCount() int64 { return m.del }
|
|
|
|
func TestMetrics_SQLiteShape(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
collector := metrics.NewCollector(
|
|
"test-node",
|
|
nil, // no DB
|
|
&mockHub{count: 5},
|
|
&mockBus{pub: 10, del: 20},
|
|
store.Stores{}, // no cluster store
|
|
func() (uint64, uint64, float64) { return 42, 3, 12.5 },
|
|
func() int64 { return 7 },
|
|
time.Now().Add(-10*time.Minute),
|
|
)
|
|
|
|
h := NewMetricsHandler(collector)
|
|
r := gin.New()
|
|
r.GET("/api/v1/admin/metrics", h.GetMetrics)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
|
|
var snap metrics.Snapshot
|
|
if err := json.Unmarshal(w.Body.Bytes(), &snap); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
// Node ID
|
|
if snap.NodeID != "test-node" {
|
|
t.Errorf("node_id = %q, want %q", snap.NodeID, "test-node")
|
|
}
|
|
|
|
// Runtime
|
|
if snap.Runtime.WSClients != 5 {
|
|
t.Errorf("ws_clients = %d, want 5", snap.Runtime.WSClients)
|
|
}
|
|
if snap.Runtime.UptimeSec < 600 {
|
|
t.Errorf("uptime_sec = %f, want >= 600", snap.Runtime.UptimeSec)
|
|
}
|
|
|
|
// Cluster should be nil (SQLite)
|
|
if snap.Cluster != nil {
|
|
t.Errorf("cluster should be nil for SQLite, got %+v", snap.Cluster)
|
|
}
|
|
|
|
// Extensions
|
|
if snap.Extensions.StarlarkExecTotal != 42 {
|
|
t.Errorf("starlark_exec_total = %d, want 42", snap.Extensions.StarlarkExecTotal)
|
|
}
|
|
if snap.Extensions.StarlarkErrorsTotal != 3 {
|
|
t.Errorf("starlark_errors_total = %d, want 3", snap.Extensions.StarlarkErrorsTotal)
|
|
}
|
|
if snap.Extensions.StarlarkAvgDuration != 12.5 {
|
|
t.Errorf("starlark_avg_duration_ms = %f, want 12.5", snap.Extensions.StarlarkAvgDuration)
|
|
}
|
|
if snap.Extensions.TriggerFiresTotal != 7 {
|
|
t.Errorf("trigger_fires_total = %d, want 7", snap.Extensions.TriggerFiresTotal)
|
|
}
|
|
if snap.Extensions.EventBusPublished != 10 {
|
|
t.Errorf("event_bus_published = %d, want 10", snap.Extensions.EventBusPublished)
|
|
}
|
|
if snap.Extensions.EventBusDelivered != 20 {
|
|
t.Errorf("event_bus_delivered = %d, want 20", snap.Extensions.EventBusDelivered)
|
|
}
|
|
}
|
|
|
|
func TestMetrics_WithCluster(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mock := &mockClusterStore{
|
|
nodes: []store.ClusterNode{
|
|
{
|
|
NodeID: "node-1",
|
|
Endpoint: "http://node-1:8080",
|
|
Heartbeat: time.Now(),
|
|
Stats: json.RawMessage(`{"uptime_sec":120,"ws_clients":3}`),
|
|
},
|
|
},
|
|
}
|
|
|
|
collector := metrics.NewCollector(
|
|
"test-node",
|
|
nil,
|
|
&mockHub{count: 3},
|
|
&mockBus{},
|
|
store.Stores{Cluster: mock},
|
|
func() (uint64, uint64, float64) { return 0, 0, 0 },
|
|
func() int64 { return 0 },
|
|
time.Now(),
|
|
)
|
|
|
|
h := NewMetricsHandler(collector)
|
|
r := gin.New()
|
|
r.GET("/api/v1/admin/metrics", h.GetMetrics)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
|
|
var snap metrics.Snapshot
|
|
if err := json.Unmarshal(w.Body.Bytes(), &snap); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
if snap.Cluster == nil {
|
|
t.Fatal("cluster should not be nil with cluster store")
|
|
}
|
|
if snap.Cluster.Size != 1 {
|
|
t.Errorf("cluster.size = %d, want 1", snap.Cluster.Size)
|
|
}
|
|
if snap.Cluster.Nodes[0].NodeID != "node-1" {
|
|
t.Errorf("node_id = %q, want %q", snap.Cluster.Nodes[0].NodeID, "node-1")
|
|
}
|
|
if snap.Cluster.Nodes[0].UptimeSec != 120 {
|
|
t.Errorf("uptime_sec = %f, want 120", snap.Cluster.Nodes[0].UptimeSec)
|
|
}
|
|
}
|
|
|
|
func TestMetrics_ExtensionCounters(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
collector := metrics.NewCollector(
|
|
"test-node",
|
|
nil,
|
|
&mockHub{},
|
|
&mockBus{pub: 100, del: 500},
|
|
store.Stores{},
|
|
func() (uint64, uint64, float64) { return 1000, 50, 8.3 },
|
|
func() int64 { return 25 },
|
|
time.Now(),
|
|
)
|
|
|
|
h := NewMetricsHandler(collector)
|
|
r := gin.New()
|
|
r.GET("/api/v1/admin/metrics", h.GetMetrics)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/metrics", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
var snap metrics.Snapshot
|
|
_ = json.Unmarshal(w.Body.Bytes(), &snap)
|
|
|
|
if snap.Extensions.StarlarkExecTotal != 1000 {
|
|
t.Errorf("exec total = %d, want 1000", snap.Extensions.StarlarkExecTotal)
|
|
}
|
|
if snap.Extensions.EventBusPublished != 100 {
|
|
t.Errorf("bus published = %d, want 100", snap.Extensions.EventBusPublished)
|
|
}
|
|
if snap.Extensions.EventBusDelivered != 500 {
|
|
t.Errorf("bus delivered = %d, want 500", snap.Extensions.EventBusDelivered)
|
|
}
|
|
}
|