mirror of
https://github.com/hanzoai/agents.git
synced 2026-10-30 18:48:36 +00:00
/metrics was a net/http handler behind zip.AdaptNetHTTP because every exposition entry point in luxfi/metric returned an http.Handler. v1.11.0 renders one as a value instead — Scrape gives back status, headers and body, and ScrapeTimeout reads the deadline off any header lookup — so the route is a zip handler that writes those three fields. HandlerForWithOpts is now a writer over the same value and decides nothing itself, which is what makes the two answer alike. The bump from v1.8.1 also changes what a scrape gets back. At v1.8.1 the real registry sits behind a `metrics` build tag, and no lane here sets one: the image builds -tags "embedded sqlite_fts5". So DefaultRegistry was the no-op registry and every scrape answered 200 with an empty body — measured, 0 metric families in this package even after registering a gauge. From v1.9.1 the tag is inverted and `metrics_noop` opts out, so the gauges and counters internal/services has always registered — hanzo_agents_gateway_queue_depth and the rest — are on the wire. metrics_test.go pins the answer, not the writer: the status, the media type whole (a scraper reads version=0.0.4 out of it to choose a parser), the exposition text, the deadline under both header spellings, one that is not a number meaning no deadline, and one already spent answered as the refusal — text/plain, nosniff, "metrics gather error". Every row passes on the code above this commit too, run with -tags metrics so the registry is real there. go test ./...: 15 packages pass and 4 fail — application, handlers/ui, services and storage, the same four that fail on main.
53 lines
2.0 KiB
Go
53 lines
2.0 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/agents/control-plane/internal/config"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestMetricsExposition pins what GET /metrics answers, so the route keeps
|
|
// answering it however it is written. The version rides in the media type and a
|
|
// scraper reads it to choose a parser, so the type is pinned whole rather than
|
|
// by prefix. The deadline a scraper asks for is part of the answer: it arrives
|
|
// under either spelling, a value that is not a number means no deadline, and a
|
|
// deadline already spent is refused rather than answered with a partial body.
|
|
func TestMetricsExposition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := newTestServer(config.UIConfig{Enabled: true, Mode: "embedded"}, config.APIConfig{})
|
|
srv.setupRoutes()
|
|
|
|
served := []struct {
|
|
name string
|
|
header map[string]string
|
|
}{
|
|
{"no deadline asked for", nil},
|
|
{"a deadline", map[string]string{"X-Scrape-Timeout-Seconds": "5"}},
|
|
{"a deadline, spelled the older way", map[string]string{"X-Prometheus-Scrape-Timeout-Seconds": "5"}},
|
|
{"a deadline that is not a number", map[string]string{"X-Scrape-Timeout-Seconds": "soon"}},
|
|
}
|
|
for _, s := range served {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
code, hdr, body := doReq(t, srv, http.MethodGet, "/metrics", s.header)
|
|
|
|
require.Equal(t, http.StatusOK, code)
|
|
require.Equal(t, "text/plain; version=0.0.4; charset=utf-8", hdr.Get("Content-Type"))
|
|
require.Contains(t, body, "# HELP hanzo_agents_gateway_queue_depth ")
|
|
require.Contains(t, body, "# TYPE hanzo_agents_gateway_queue_depth gauge")
|
|
})
|
|
}
|
|
|
|
t.Run("a deadline already spent", func(t *testing.T) {
|
|
code, hdr, body := doReq(t, srv, http.MethodGet, "/metrics", map[string]string{"X-Scrape-Timeout-Seconds": "1e-9"})
|
|
|
|
require.Equal(t, http.StatusInternalServerError, code)
|
|
require.Equal(t, "text/plain; charset=utf-8", hdr.Get("Content-Type"))
|
|
require.Equal(t, "nosniff", hdr.Get("X-Content-Type-Options"))
|
|
require.Equal(t, "metrics gather error\n", body)
|
|
})
|
|
}
|