Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion erpc/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func Init(
return appCtx
},
Addr: fmt.Sprintf(":%d", *cfg.Metrics.Port),
Handler: promhttp.Handler(),
Handler: newMetricsHandler(),
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
Expand Down Expand Up @@ -158,3 +158,25 @@ func Init(

return nil
}

// newMetricsHandler serves Prometheus on /metrics (and / for back-compat) and a
// cheap /healthz|/health that does not Gather the registry.
//
// Historically promhttp.Handler() was mounted as the root handler, so every
// path — including kubelet probes hitting /healthz — returned the full
// exposition. On hot pods that payload is tens of MB and Gather contends with
// scrapes; probe timeoutSeconds: 5 then fails and kubelet restarts the pod.
func newMetricsHandler() http.Handler {
mux := http.NewServeMux()
metrics := promhttp.Handler()
ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
mux.Handle("/healthz", ok)
mux.Handle("/health", ok)
mux.Handle("/metrics", metrics)
mux.Handle("/", metrics)
return mux
}
57 changes: 57 additions & 0 deletions erpc/metrics_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package erpc

import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestNewMetricsHandler_HealthzIsLightweight(t *testing.T) {
h := newMetricsHandler()

for _, path := range []string{"/healthz", "/health"} {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, path, nil)
h.ServeHTTP(rr, req)

if rr.Code != http.StatusOK {
t.Fatalf("%s: status=%d want 200", path, rr.Code)
}
body, _ := io.ReadAll(rr.Body)
if string(body) != "OK" {
t.Fatalf("%s: body=%q want OK", path, body)
}
ct := rr.Header().Get("Content-Type")
if strings.Contains(ct, "openmetrics") || strings.Contains(ct, "text/plain; version=") {
t.Fatalf("%s: got prometheus content-type %q; health must not Gather", path, ct)
}
}
}

func TestNewMetricsHandler_MetricsStillExposition(t *testing.T) {
h := newMetricsHandler()

for _, path := range []string{"/metrics", "/"} {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, path, nil)
h.ServeHTTP(rr, req)

if rr.Code != http.StatusOK {
t.Fatalf("%s: status=%d want 200", path, rr.Code)
}
body, _ := io.ReadAll(rr.Body)
if !strings.Contains(string(body), "# HELP") && !strings.Contains(string(body), "# TYPE") {
// Empty registry still usually emits process/go collectors via default registerer.
t.Fatalf("%s: expected prometheus exposition, got %q", path, truncate(string(body), 200))
}
}
}

func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
Loading