diff --git a/README.md b/README.md index 87d3838..13a6a3b 100644 --- a/README.md +++ b/README.md @@ -685,7 +685,9 @@ See `configs/config.example.yml` for all options with defaults. `metrics.username` and `metrics.password` are set. A request's `handler` label is the pattern of the route it matches, such as `/v1/image/*`, never its path. Requests that match no route share the label `unmatched`: those the router - answers 404 or 405, and CORS preflights + answers 404 or 405, and CORS preflights. A request's `method` label is its + method when that is `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, + `OPTIONS`, `CONNECT` or `TRACE`, and `other` for any other method - **Logging**: stdlib slog ## Entrypoints diff --git a/TODO.md b/TODO.md index 724a404..3c33482 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,15 @@ P2: security: per-origin rate limiting # Completed Steps +- 2026-10-15 the request metrics record any method that is not a standard one as + `other` (closes #274): their `method` label was each request's method as sent, + and Go's HTTP server accepts any method name, so a client sending made-up + methods added series to the Prometheus registry that were never removed. The + label is now the request's method only for `GET`, `HEAD`, `POST`, `PUT`, + `PATCH`, `DELETE`, `OPTIONS`, `CONNECT` and `TRACE`. The metrics library reads + the method from the request it is handed, so the metrics middleware hands it a + copy of the request carrying the label; the handlers still see the method the + client sent. - 2026-10-15 logging out ends every login session (closes #256): a login session was a cookie with nothing kept on the server, so `/logout` removed it from one browser and a copy of it kept working for its 30 days. Every login is made diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index a35c23e..4053a0a 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -220,7 +220,10 @@ func (s *Middleware) CORS() func(http.Handler) http.Handler { // pattern of the chi route a request matches, such as /v1/image/*, or // "unmatched" for a request that matches none, so the routes fix how many // values the label has. It is never the path, with which any client could -// add series to the registry without limit. +// add series to the registry without limit. Its method label is the +// request's method when that is a standard one, such as GET, and "other" +// for any other: Go's HTTP server accepts any method name, so a label per +// method would let a client add series the same way. func (s *Middleware) Metrics() func(http.Handler) http.Handler { mdlw := ghmm.New(ghmm.Config{ Recorder: metrics.NewRecorder(metrics.Config{ @@ -230,7 +233,19 @@ func (s *Middleware) Metrics() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - std.Handler(routePattern(r), mdlw, next).ServeHTTP(w, r) + // The metrics library takes the method label from the request + // it is handed, so it is handed a shallow copy of r whose + // method is the label. next is served r itself, through the + // response writer the library wraps to see the status and size. + labelled := r.WithContext(r.Context()) + labelled.Method = methodLabel(r.Method) + + serveNext := http.HandlerFunc( + func(wrapped http.ResponseWriter, _ *http.Request) { + next.ServeHTTP(wrapped, r) + }) + + std.Handler(routePattern(r), mdlw, serveNext).ServeHTTP(w, labelled) }) } } @@ -275,6 +290,20 @@ func routePattern(r *http.Request) string { return pattern } +// methodLabel returns method when it is one of the standard methods net/http +// names: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, CONNECT and TRACE. +// For any other method it returns "other". +func methodLabel(method string) string { + switch method { + case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, + http.MethodPatch, http.MethodDelete, http.MethodOptions, + http.MethodConnect, http.MethodTrace: + return method + default: + return "other" + } +} + // MetricsAuth returns a basic auth middleware for the metrics endpoint. func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler { return basicauth.New(