diff --git a/server/handlers/admin_metrics_test.go b/server/handlers/admin_metrics_test.go index 268efa4..0285dbe 100644 --- a/server/handlers/admin_metrics_test.go +++ b/server/handlers/admin_metrics_test.go @@ -28,6 +28,7 @@ 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}, @@ -54,6 +55,11 @@ func TestMetrics_SQLiteShape(t *testing.T) { 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) @@ -103,6 +109,7 @@ func TestMetrics_WithCluster(t *testing.T) { } collector := metrics.NewCollector( + "test-node", nil, &mockHub{count: 3}, &mockBus{}, @@ -147,6 +154,7 @@ func TestMetrics_ExtensionCounters(t *testing.T) { gin.SetMode(gin.TestMode) collector := metrics.NewCollector( + "test-node", nil, &mockHub{}, &mockBus{pub: 100, del: 500}, diff --git a/server/main.go b/server/main.go index 1a01ece..150a85d 100644 --- a/server/main.go +++ b/server/main.go @@ -201,15 +201,18 @@ func main() { // ── WebSocket Hub ───────────────────────── hub := events.NewHub(bus, middleware.GetAllowedOrigins(cfg)) + // ── Node Identity ──────────────── + // Used by cluster registry (PG) and metrics endpoint (all deployments). + nodeID := cfg.ClusterNodeID + if nodeID == "" { + hostname, _ := os.Hostname() + nodeID = fmt.Sprintf("%s-%d", hostname, os.Getpid()) + } + // ── Cluster Registry ──────────── // PG-backed node self-registration + heartbeat. No-op on SQLite. var clusterReg *cluster.Registry if database.IsPostgres() && stores.Cluster != nil { - nodeID := cfg.ClusterNodeID - if nodeID == "" { - hostname, _ := os.Hostname() - nodeID = fmt.Sprintf("%s-%d", hostname, os.Getpid()) - } clusterReg = cluster.NewRegistry(nodeID, cfg.ClusterEndpoint, stores.Cluster, hub, cluster.RegistryConfig{ HeartbeatInterval: cfg.ClusterHeartbeatInterval, StaleThreshold: cfg.ClusterStaleThreshold, @@ -811,7 +814,7 @@ func main() { // ── Metrics ───────────────── metricsCollector := metrics.NewCollector( - database.DB, hub, bus, stores, + nodeID, database.DB, hub, bus, stores, sandbox.SandboxStats, triggerEngine.FireCount, startTime, diff --git a/server/metrics/collector.go b/server/metrics/collector.go index e7896d6..d9ffd03 100644 --- a/server/metrics/collector.go +++ b/server/metrics/collector.go @@ -18,6 +18,7 @@ import ( // Snapshot is the top-level JSON response from GET /api/v1/admin/metrics. type Snapshot struct { + NodeID string `json:"node_id"` Runtime RuntimeMetrics `json:"runtime"` DB DBMetrics `json:"db"` Cluster *ClusterMetrics `json:"cluster,omitempty"` @@ -91,6 +92,7 @@ type TriggerFireCountFunc func() int64 // Collector gathers metrics on demand for the admin endpoint. type Collector struct { + nodeID string db *sql.DB hub ConnCounter bus BusCounter @@ -101,8 +103,9 @@ type Collector struct { } // NewCollector creates a metrics collector with all required dependencies. -func NewCollector(db *sql.DB, hub ConnCounter, bus BusCounter, stores store.Stores, sandboxFn SandboxStatsFunc, triggerFn TriggerFireCountFunc, startTime time.Time) *Collector { +func NewCollector(nodeID string, db *sql.DB, hub ConnCounter, bus BusCounter, stores store.Stores, sandboxFn SandboxStatsFunc, triggerFn TriggerFireCountFunc, startTime time.Time) *Collector { return &Collector{ + nodeID: nodeID, db: db, hub: hub, bus: bus, @@ -115,7 +118,7 @@ func NewCollector(db *sql.DB, hub ConnCounter, bus BusCounter, stores store.Stor // Collect gathers all metrics synchronously and returns a snapshot. func (c *Collector) Collect(ctx context.Context) *Snapshot { - snap := &Snapshot{} + snap := &Snapshot{NodeID: c.nodeID} snap.Runtime = c.collectRuntime() snap.DB = c.collectDB(ctx) snap.Cluster = c.collectCluster(ctx) diff --git a/src/js/sw/surfaces/admin/health.js b/src/js/sw/surfaces/admin/health.js index aedf441..d340566 100644 --- a/src/js/sw/surfaces/admin/health.js +++ b/src/js/sw/surfaces/admin/health.js @@ -121,13 +121,13 @@ function NodeCard({ node }) { const ageColor = node.heartbeat_age_ms < 15000 ? 'var(--accent, #2a6)' : 'var(--warning, #c80)'; return html` -
-
- ${node.node_id} - ${ageLabel} +
+
+ ${node.node_id} + ${ageLabel}
- ${node.endpoint && html`
${node.endpoint}
`} -
+ ${node.endpoint && html`
${node.endpoint}
`} +
<${Stat} label="Uptime" value=${formatDuration(stats.uptime_sec)} /> <${Stat} label="WS Clients" value=${formatNum(stats.ws_clients)} /> <${Stat} label="Goroutines" value=${formatNum(stats.goroutines)} /> @@ -188,8 +188,9 @@ export default function HealthSection() { return html`
-
- Refresh +
+ ${data.node_id && html`Viewing from ${data.node_id}`} + Refresh