Bound the /metrics method label (closes #261)
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:
2026-08-23 23:47:04 +00:00
parent fd5966f807
commit 2ef52bbac2
5 changed files with 436 additions and 19 deletions

View File

@@ -1677,6 +1677,40 @@ gauge. The outcome counters move only after the status change has been
written, so a transition the database rejected is never reported as an
outcome that happened.
#### Inbound HTTP metrics
The middleware records three more on the same registry:
| Metric | Type | Labels |
| ------ | ---- | ------ |
| `http_request_duration_seconds` | histogram | `service`, `handler`, `method`, `code` |
| `http_response_size_bytes` | histogram | `service`, `handler`, `method`, `code` |
| `http_requests_inflight` | gauge | `service`, `handler` |
Two of those labels are written once per request from bytes the client
chose, so both are bounded to something this service registers:
- `handler` is the chi route pattern — `/webhook/{uuid}`, never the
concrete path. A request matching no route carries `(unmatched)`,
and no entrypoint UUID ever reaches a label.
- `method` is the request method when the router can route it, and
`(unmatched)` otherwise. `net/http` accepts any RFC 9110 token as a
method, so the raw value bounds the label at nothing; the nine chi
matches routes for stay distinguishable, and a token that could only
ever have produced a 405 does not get a series of its own.
The other two are not request-controlled: `code` is the status one of
this service's own handlers wrote, and `service` is a fixed empty
string.
`http_requests_inflight` is deliberately aggregate — its `handler` is
always `(all)`, one series counting the requests in flight across the
whole service. The gauge is incremented before routing and decremented
after the handler returns, and the route pattern exists only between
those two moments, so labelling it by pattern would increment one
series and decrement another, leaving every pattern permanently off by
the number of requests it served.
### Rate Limiting
Global blanket rate limiting middleware (e.g., a per-IP throttle shared