fix(go): add eventLoop to /api/health with GC pause percentiles, fixes #147

Go's /api/health was missing the eventLoop object that Node.js provides.
The perf.js frontend reads health.eventLoop.p95Ms which crashed with
'Cannot read properties of undefined' when served by the Go server.

Adds eventLoop field using GC pause data from runtime.MemStats.PauseNs
(last 256 pauses) to compute p50Ms, p95Ms, p99Ms, currentLagMs, maxLagMs
— matching the Node.js response shape exactly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-03-27 11:04:43 -07:00
co-authored by Copilot
parent 8a0f731452
commit 407c49e017
2 changed files with 34 additions and 0 deletions
+23
View File
@@ -297,6 +297,22 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
}
}
// Build eventLoop-equivalent from GC pause data (matches Node.js shape)
var gcPauses []float64
n := int(m.NumGC)
if n > 256 {
n = 256
}
for i := 0; i < n; i++ {
idx := (int(m.NumGC) - n + i) % 256
gcPauses = append(gcPauses, float64(m.PauseNs[idx])/1e6)
}
sortedPauses := sortedCopy(gcPauses)
var lastPauseMs float64
if m.NumGC > 0 {
lastPauseMs = float64(m.PauseNs[(m.NumGC+255)%256]) / 1e6
}
writeJSON(w, map[string]interface{}{
"status": "ok",
"engine": "go",
@@ -311,6 +327,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
"external": 0,
"heapMB": round(float64(m.HeapAlloc)/1048576, 1),
},
"eventLoop": map[string]interface{}{
"currentLagMs": round(lastPauseMs, 1),
"maxLagMs": round(percentile(sortedPauses, 1.0), 1),
"p50Ms": round(percentile(sortedPauses, 0.5), 1),
"p95Ms": round(percentile(sortedPauses, 0.95), 1),
"p99Ms": round(percentile(sortedPauses, 0.99), 1),
},
"goRuntime": map[string]interface{}{
"goroutines": runtime.NumGoroutine(),
"gcPauses": m.NumGC,
+11
View File
@@ -81,6 +81,17 @@ func TestHealthEndpoint(t *testing.T) {
t.Error("expected estimatedMB in packetStore")
}
// Verify eventLoop (GC pause metrics matching Node.js shape)
el, ok := body["eventLoop"].(map[string]interface{})
if !ok {
t.Fatal("expected eventLoop object in health response")
}
for _, field := range []string{"currentLagMs", "maxLagMs", "p50Ms", "p95Ms", "p99Ms"} {
if _, ok := el[field]; !ok {
t.Errorf("expected %s in eventLoop", field)
}
}
// Verify cache has real structure
cache, ok := body["cache"].(map[string]interface{})
if !ok {