Bound the /metrics method label (closes #261)
All checks were successful
check / check (push) Successful in 3m6s
All checks were successful
check / check (push) Successful in 3m6s
The `method` label was recorded as `r.Method` verbatim. net/http
accepts any RFC 9110 token as a method and passes it through, so the
label was bounded at nothing: 300 requests to `/` carrying random
12-character method tokens took a live scrape from 81 lines to 7,606,
unauthenticated and on a route with no rate limiter. This is the same
remote memory-exhaustion vector the handler label carried, reached
through a second dimension.
The fix goes in the same recorder seam that bounds the handler label,
which is renamed to reflect that it now bounds both. A method the
router can route is kept verbatim, so real methods stay
distinguishable; anything else carries the existing `(unmatched)`
sentinel, deliberately the same spelling rather than a second one for
the same idea. The bound is ten values.
The retained set is restated against the net/http constants because
chi's own methodMap is unexported. A token outside it can only ever
produce chi's 405, so folding those together loses nothing a scrape
could have used.
README's Metrics section gains the inbound HTTP metrics, the bound on
each of their labels, and the aggregate
`http_requests_inflight{handler="(all)"}` semantics.
This commit is contained in:
@@ -97,20 +97,24 @@ func metricsTestRouter(
|
||||
return r, reg
|
||||
}
|
||||
|
||||
// drivePaths sends one POST per supplied path and returns how many
|
||||
// responses carried each status code.
|
||||
func drivePaths(
|
||||
t *testing.T,
|
||||
h http.Handler,
|
||||
paths []string,
|
||||
) map[int]int {
|
||||
// probe is one request a cardinality assertion sends. Both label
|
||||
// dimensions that have leaked are request-controlled — the path and
|
||||
// the method — so both vary here and one driver sends them.
|
||||
type probe struct {
|
||||
method string
|
||||
path string
|
||||
}
|
||||
|
||||
// drive sends every probe and returns how many responses carried each
|
||||
// status code.
|
||||
func drive(t *testing.T, h http.Handler, probes []probe) map[int]int {
|
||||
t.Helper()
|
||||
|
||||
codes := make(map[int]int)
|
||||
|
||||
for _, p := range paths {
|
||||
for _, p := range probes {
|
||||
req := httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodPost, p, nil,
|
||||
t.Context(), p.method, p.path, nil,
|
||||
)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
@@ -120,6 +124,25 @@ func drivePaths(
|
||||
return codes
|
||||
}
|
||||
|
||||
// drivePaths sends one POST per supplied path.
|
||||
func drivePaths(
|
||||
t *testing.T,
|
||||
h http.Handler,
|
||||
paths []string,
|
||||
) map[int]int {
|
||||
t.Helper()
|
||||
|
||||
probes := make([]probe, 0, len(paths))
|
||||
|
||||
for _, p := range paths {
|
||||
probes = append(
|
||||
probes, probe{method: http.MethodPost, path: p},
|
||||
)
|
||||
}
|
||||
|
||||
return drive(t, h, probes)
|
||||
}
|
||||
|
||||
// receiverPaths returns n distinct /webhook/ paths, each naming a
|
||||
// fresh UUID exactly as an unauthenticated flood would.
|
||||
func receiverPaths(n int) []string {
|
||||
|
||||
Reference in New Issue
Block a user