Feat v0.6.4 health metrics (#39)
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m42s
CI/CD / test-sqlite (push) Successful in 2m48s
CI/CD / build-and-deploy (push) Successful in 1m5s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #39.
This commit is contained in:
2026-03-31 14:05:49 +00:00
committed by xcaliber
parent 3d4228f868
commit 36d6158940
22 changed files with 937 additions and 286 deletions

View File

@@ -34,6 +34,26 @@ type Registry struct {
startTime time.Time
stopCh chan struct{}
wg sync.WaitGroup
// Optional callbacks — set after construction via setters.
sandboxStats func() (exec, errors uint64, avgMs float64)
triggerFires func() int64
extensionCount func() int
}
// SetSandboxStats registers a callback for sandbox execution counters.
func (r *Registry) SetSandboxStats(fn func() (uint64, uint64, float64)) {
r.sandboxStats = fn
}
// SetTriggerFireCount registers a callback for trigger fire count.
func (r *Registry) SetTriggerFireCount(fn func() int64) {
r.triggerFires = fn
}
// SetExtensionCount registers a callback for active extension count.
func (r *Registry) SetExtensionCount(fn func() int) {
r.extensionCount = fn
}
// NewRegistry creates a cluster registry instance.
@@ -129,13 +149,28 @@ func (r *Registry) collectStats() json.RawMessage {
runtime.ReadMemStats(&m)
stats := map[string]any{
"goroutines": runtime.NumGoroutine(),
"heap_alloc": m.HeapAlloc,
"heap_sys": m.HeapSys,
"gc_cycles": m.NumGC,
"gc_pause_ns": m.PauseNs[(m.NumGC+255)%256],
"uptime_sec": time.Since(r.startTime).Seconds(),
"ws_clients": r.hub.ConnCount(),
"goroutines": runtime.NumGoroutine(),
"heap_alloc": m.HeapAlloc,
"heap_sys": m.HeapSys,
"stack_in_use": m.StackInuse,
"gc_cycles": m.NumGC,
"gc_pause_ns": m.PauseNs[(m.NumGC+255)%256],
"gc_cpu_pct": m.GCCPUFraction * 100,
"uptime_sec": time.Since(r.startTime).Seconds(),
"ws_clients": r.hub.ConnCount(),
}
if r.extensionCount != nil {
stats["extensions_loaded"] = r.extensionCount()
}
if r.sandboxStats != nil {
exec, errors, avgMs := r.sandboxStats()
stats["starlark_exec_total"] = exec
stats["starlark_errors_total"] = errors
stats["starlark_avg_duration_ms"] = avgMs
}
if r.triggerFires != nil {
stats["trigger_fires_total"] = r.triggerFires()
}
data, _ := json.Marshal(stats)

View File

@@ -75,6 +75,46 @@ func TestCollectStats(t *testing.T) {
}
}
func TestCollectStatsFattened(t *testing.T) {
hub := &mockHub{count: 3}
reg := NewRegistry("test-node", "", nil, hub, RegistryConfig{
HeartbeatInterval: 10 * time.Second,
StaleThreshold: 30 * time.Second,
})
reg.SetSandboxStats(func() (uint64, uint64, float64) { return 100, 5, 12.3 })
reg.SetTriggerFireCount(func() int64 { return 42 })
reg.SetExtensionCount(func() int { return 8 })
data := reg.collectStats()
var stats map[string]any
if err := json.Unmarshal(data, &stats); err != nil {
t.Fatalf("collectStats returned invalid JSON: %v", err)
}
// New fattened keys
fatKeys := []string{
"stack_in_use", "gc_cpu_pct",
"extensions_loaded", "starlark_exec_total", "starlark_errors_total",
"starlark_avg_duration_ms", "trigger_fires_total",
}
for _, key := range fatKeys {
if _, ok := stats[key]; !ok {
t.Errorf("missing fattened stats key: %s", key)
}
}
if v := stats["extensions_loaded"].(float64); int(v) != 8 {
t.Errorf("extensions_loaded = %v, want 8", v)
}
if v := stats["starlark_exec_total"].(float64); int(v) != 100 {
t.Errorf("starlark_exec_total = %v, want 100", v)
}
if v := stats["trigger_fires_total"].(float64); int(v) != 42 {
t.Errorf("trigger_fires_total = %v, want 42", v)
}
}
func TestRegistryStartStop(t *testing.T) {
ms := &mockClusterStore{heartbeatRows: 1}
hub := &mockHub{count: 0}