diff --git a/cmd/server/routes.go b/cmd/server/routes.go index c440da8e..0ddf87f6 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -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, diff --git a/cmd/server/routes_test.go b/cmd/server/routes_test.go index 36301beb..1a8475e9 100644 --- a/cmd/server/routes_test.go +++ b/cmd/server/routes_test.go @@ -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 {