From c49c7a3c3e92b35baed5944894f882587bff636d Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 01:39:22 +0000 Subject: [PATCH 1/8] Test that metrics label requests by route, not path The new test sends two paths one route answers and three requests no route answers through the metrics middleware, and expects the handler label to hold the route's pattern and "unmatched". It fails: the label is each request's path, so every new path adds series. TestMetricsRecordsServedRequest serves its request outside any router, so it now expects handler="unmatched" where it expected the path handler="/test", as the owner approved in https://git.eeqj.de/sneak/pixa/issues/254#issuecomment-142374. Middleware gets a metricsRegistry field, nil being Prometheus' default registry, so the new test can register with a registry of its own: TestMetricsRecordsServedRequest registers with the default one. Model: opus-5-5 --- internal/middleware/middleware.go | 9 +- .../middleware/middleware_internal_test.go | 82 ++++++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index f6eb83d..7b433c7 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -14,6 +14,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httprate" + "github.com/prometheus/client_golang/prometheus" metrics "github.com/slok/go-http-metrics/metrics/prometheus" ghmm "github.com/slok/go-http-metrics/middleware" "github.com/slok/go-http-metrics/middleware/std" @@ -63,6 +64,10 @@ type Middleware struct { log *slog.Logger config *config.Config clientIP *clientip.Resolver + + // metricsRegistry is the registry Metrics registers its metrics with. + // nil is Prometheus' default registry, the one /metrics serves. + metricsRegistry prometheus.Registerer } // New creates a new Middleware instance. @@ -212,7 +217,9 @@ func (s *Middleware) CORS() func(http.Handler) http.Handler { // Metrics returns a Prometheus metrics middleware. func (s *Middleware) Metrics() func(http.Handler) http.Handler { mdlw := ghmm.New(ghmm.Config{ - Recorder: metrics.NewRecorder(metrics.Config{}), + Recorder: metrics.NewRecorder(metrics.Config{ + Registry: s.metricsRegistry, + }), }) return func(next http.Handler) http.Handler { diff --git a/internal/middleware/middleware_internal_test.go b/internal/middleware/middleware_internal_test.go index 91ba3f3..5801513 100644 --- a/internal/middleware/middleware_internal_test.go +++ b/internal/middleware/middleware_internal_test.go @@ -3,12 +3,15 @@ package middleware import ( "bytes" "log/slog" + "maps" "net/http" "net/http/httptest" "net/url" "strings" "testing" + "github.com/go-chi/chi/v5" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "sneak.berlin/go/pixa/internal/config" @@ -198,7 +201,7 @@ func TestMetricsRecordsServedRequest(t *testing.T) { // The line /metrics shows once one GET /test has been served. const want = `http_request_duration_seconds_count{` + - `code="200",handler="/test",method="GET",service=""} 1` + `code="200",handler="unmatched",method="GET",service=""} 1` mw := &Middleware{log: slog.Default(), config: &config.Config{}} @@ -220,6 +223,83 @@ func TestMetricsRecordsServedRequest(t *testing.T) { } } +// TestMetricsLabelsRequestsByRoute checks that the metrics middleware labels +// each request with the pattern of the route that answers it, never with its +// path: two paths one route answers share that route's pattern, and requests +// no route answers, a 404 or a 405, share "unmatched". With a label per path, +// any client could add series to the registry without limit. The middleware +// registers with a registry of its own here, as another test in this package +// registers with Prometheus' default registry, which can be done only once. +func TestMetricsLabelsRequestsByRoute(t *testing.T) { + t.Parallel() + + registry := prometheus.NewRegistry() + mw := &Middleware{ + log: slog.Default(), + config: &config.Config{}, + metricsRegistry: registry, + } + + // The image route is in a subrouter, as in pixa's router. + router := chi.NewRouter() + router.Use(mw.Metrics()) + router.Route("/v1", func(r chi.Router) { + r.Get("/image/*", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + }) + + for _, req := range []struct{ method, path string }{ + {http.MethodGet, "/v1/image/a.example.com/one.jpg/1x1.png"}, + {http.MethodGet, "/v1/image/b.example.com/two.jpg/1x1.png"}, + {http.MethodGet, "/no-such-route"}, + {http.MethodGet, "/v1/no-such-route"}, + {http.MethodPost, "/v1/image/a.example.com/one.jpg/1x1.png"}, + } { + router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequestWithContext( + t.Context(), req.method, req.path, nil)) + } + + got := requestCountsByHandler(t, registry) + t.Logf("requests counted by handler label: %v", got) + + want := map[string]uint64{"/v1/image/*": 2, "unmatched": 3} + if !maps.Equal(got, want) { + t.Errorf("requests counted by handler label = %v, want %v", got, want) + } +} + +// requestCountsByHandler returns how many requests the request duration +// histogram in registry counted under each value of its handler label. +func requestCountsByHandler( + t *testing.T, registry *prometheus.Registry, +) map[string]uint64 { + t.Helper() + + families, err := registry.Gather() + if err != nil { + t.Fatalf("registry.Gather() error = %v", err) + } + + counts := map[string]uint64{} + + for _, family := range families { + if family.GetName() != "http_request_duration_seconds" { + continue + } + + for _, metric := range family.GetMetric() { + for _, label := range metric.GetLabel() { + if label.GetName() == "handler" { + counts[label.GetValue()] += metric.GetHistogram().GetSampleCount() + } + } + } + } + + return counts +} + // TestLoggingLeavesOutSubmittedSigningKey checks that a login, a POST / // whose form carries the signing key, leaves no trace of the key in the // request's log line. -- 2.54.0 From f1127b7e32864f6b9246a11349a7142b2220fbc5 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 01:45:21 +0000 Subject: [PATCH 2/8] Label request metrics by route, not by path (closes #254) The metrics middleware gave the handler label each request's path, so every new path added series that were never removed, and /metrics listed encrypted URL tokens. The label is now the pattern of the chi route the request matches, or "unmatched" for one that matches none. The metrics library takes the label before calling the handler, so the middleware asks the router with chi's Find which route matches. Model: opus-5-5 --- README.md | 5 ++++- TODO.md | 9 ++++++++ internal/middleware/middleware.go | 36 +++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9dc720d..b39b3ff 100644 --- a/README.md +++ b/README.md @@ -640,7 +640,10 @@ See `configs/config.example.yml` for all options with defaults. - **Metrics**: Prometheus, at `/metrics`: generic HTTP request metrics (duration, response size, requests in flight) and the Go runtime and process metrics; requests are measured and `/metrics` is served only when - `metrics.username` and `metrics.password` are set + `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 - **Logging**: stdlib slog ## Entrypoints diff --git a/TODO.md b/TODO.md index 8d09a38..700a6b0 100644 --- a/TODO.md +++ b/TODO.md @@ -62,6 +62,15 @@ P2: security: per-origin rate limiting not in `allowlist.IsAllowed`, which also matches `referer_blocklist`, where a `Referer` is blocked whatever its port. A URL is signed over its host as it writes it, in lower case, with any IPv6 brackets, `:` and port kept. +- 2026-10-15 the request metrics are labelled by route, not by path (closes + #254): their `handler` label was each request's path, so every new path, a 404 + or an encrypted URL, added series to the Prometheus registry that were never + removed, and `/metrics` listed every encrypted URL's token. The label is now + the pattern of the chi route the request matches, such as `/v1/image/*`, or + `unmatched` for a request that matches none: one the router answers 404 or + 405, or a CORS preflight. The metrics library takes the label before the + request is routed, so the metrics middleware asks the router which route + matches. - 2026-10-15 a `:` in the source path or query no longer moves where the path ends and the query begins (closes #247): the signed data and the cache key joined host, path and query with `:` and no escaping, so a URL signed for diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 7b433c7..274ac50 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -11,6 +11,7 @@ import ( "time" basicauth "github.com/99designs/basicauth-go" + "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httprate" @@ -214,7 +215,11 @@ func (s *Middleware) CORS() func(http.Handler) http.Handler { }) } -// Metrics returns a Prometheus metrics middleware. +// Metrics returns a Prometheus metrics middleware. Its handler label is the +// 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. func (s *Middleware) Metrics() func(http.Handler) http.Handler { mdlw := ghmm.New(ghmm.Config{ Recorder: metrics.NewRecorder(metrics.Config{ @@ -223,10 +228,37 @@ func (s *Middleware) Metrics() func(http.Handler) http.Handler { }) return func(next http.Handler) http.Handler { - return std.Handler("", mdlw, next) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + std.Handler(routePattern(r), mdlw, next).ServeHTTP(w, r) + }) } } +// routePattern returns the pattern of the chi route r matches, or +// "unmatched" when it matches none or is not served by a chi router. The +// metrics library takes the label before the request is routed, so the +// router is asked which route matches, with the path it routes on: +// r.URL.RawPath when set, else r.URL.Path. +func routePattern(r *http.Request) string { + pattern := "" + + rctx := chi.RouteContext(r.Context()) + if rctx != nil { + path := r.URL.RawPath + if path == "" { + path = r.URL.Path + } + + pattern = rctx.Routes.Find(chi.NewRouteContext(), r.Method, path) + } + + if pattern == "" { + return "unmatched" + } + + return pattern +} + // MetricsAuth returns a basic auth middleware for the metrics endpoint. func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler { return basicauth.New( -- 2.54.0 From 5f1436379b4fcdb513f2e036505b06c8b58c1ee0 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 10:14:26 +0000 Subject: [PATCH 3/8] Test that metrics label requests for /v1 and /v1/ unmatched No route answers /v1 or /v1/, the path the image subrouter is mounted at, and the router answers both with 404, yet the metrics middleware labels them "/v1" and "/v1/". The test now sends both and expects them counted under "unmatched". It fails. Model: opus-5-5 --- internal/middleware/middleware_internal_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/middleware/middleware_internal_test.go b/internal/middleware/middleware_internal_test.go index 5801513..404d4eb 100644 --- a/internal/middleware/middleware_internal_test.go +++ b/internal/middleware/middleware_internal_test.go @@ -226,7 +226,8 @@ func TestMetricsRecordsServedRequest(t *testing.T) { // TestMetricsLabelsRequestsByRoute checks that the metrics middleware labels // each request with the pattern of the route that answers it, never with its // path: two paths one route answers share that route's pattern, and requests -// no route answers, a 404 or a 405, share "unmatched". With a label per path, +// no route answers, a 404 or a 405, share "unmatched", among them requests for +// the path the subrouter is mounted at, /v1 and /v1/. With a label per path, // any client could add series to the registry without limit. The middleware // registers with a registry of its own here, as another test in this package // registers with Prometheus' default registry, which can be done only once. @@ -254,6 +255,8 @@ func TestMetricsLabelsRequestsByRoute(t *testing.T) { {http.MethodGet, "/v1/image/b.example.com/two.jpg/1x1.png"}, {http.MethodGet, "/no-such-route"}, {http.MethodGet, "/v1/no-such-route"}, + {http.MethodGet, "/v1"}, + {http.MethodGet, "/v1/"}, {http.MethodPost, "/v1/image/a.example.com/one.jpg/1x1.png"}, } { router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequestWithContext( @@ -263,7 +266,7 @@ func TestMetricsLabelsRequestsByRoute(t *testing.T) { got := requestCountsByHandler(t, registry) t.Logf("requests counted by handler label: %v", got) - want := map[string]uint64{"/v1/image/*": 2, "unmatched": 3} + want := map[string]uint64{"/v1/image/*": 2, "unmatched": 5} if !maps.Equal(got, want) { t.Errorf("requests counted by handler label = %v, want %v", got, want) } -- 2.54.0 From cdfca2fa03ccd23d9edc0d1c04df5b1909a280ba Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 10:26:04 +0000 Subject: [PATCH 4/8] Label requests for a subrouter's own path unmatched in metrics For /v1 and /v1/, the path the image subrouter is mounted at, chi's Find answers that path as the pattern without asking the subrouter, which chi hands the request as / and which has no route there, so the router answers 404. routePattern now asks the subrouter mounted there whether it has a route for /, and labels the request "unmatched" when it has none. Model: opus-5-5 --- internal/middleware/middleware.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 274ac50..4745a86 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -8,6 +8,7 @@ import ( "net/http" "net/netip" "regexp" + "strings" "time" basicauth "github.com/99designs/basicauth-go" @@ -250,6 +251,17 @@ func routePattern(r *http.Request) string { } pattern = rctx.Routes.Find(chi.NewRouteContext(), r.Method, path) + + // For the path a subrouter is mounted at, such as /v1 or /v1/, Find + // answers that path without asking the subrouter, which chi hands + // the request as /. + for _, route := range rctx.Routes.Routes() { + if route.SubRoutes != nil && + route.Pattern == strings.TrimSuffix(pattern, "/")+"/*" && + !route.SubRoutes.Match(chi.NewRouteContext(), r.Method, "/") { + pattern = "" + } + } } if pattern == "" { -- 2.54.0 From 2330ad80376ea630b40506f0f78cbe111c6d86f6 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 10:39:12 +0000 Subject: [PATCH 5/8] Keep the metrics entry at the top of Completed Steps The rebase onto next placed the entry for this change below the one next gained meanwhile; the TODO.md workflow puts a finished step at the top. Model: opus-5-5 --- TODO.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/TODO.md b/TODO.md index 700a6b0..dd2d75e 100644 --- a/TODO.md +++ b/TODO.md @@ -51,6 +51,15 @@ P2: security: per-origin rate limiting not in the cache is made from the cached source when there is one. `README.md` says so under `/v1/image/`. A refusal from the negative cache now counts as a miss, as any other request that misses the variant cache does. +- 2026-10-15 the request metrics are labelled by route, not by path (closes + #254): their `handler` label was each request's path, so every new path, a 404 + or an encrypted URL, added series to the Prometheus registry that were never + removed, and `/metrics` listed every encrypted URL's token. The label is now + the pattern of the chi route the request matches, such as `/v1/image/*`, or + `unmatched` for a request that matches none: one the router answers 404 or + 405, or a CORS preflight. The metrics library takes the label before the + request is routed, so the metrics middleware asks the router which route + matches. - 2026-10-15 the host of a `/v1/image/` URL is lower-cased, and one with a port needs a signature (closes #246): with `cdn.example.com` allowlisted, `CDN.Example.com` and `cdn.example.com:8443` passed the allowlist too, and @@ -62,15 +71,6 @@ P2: security: per-origin rate limiting not in `allowlist.IsAllowed`, which also matches `referer_blocklist`, where a `Referer` is blocked whatever its port. A URL is signed over its host as it writes it, in lower case, with any IPv6 brackets, `:` and port kept. -- 2026-10-15 the request metrics are labelled by route, not by path (closes - #254): their `handler` label was each request's path, so every new path, a 404 - or an encrypted URL, added series to the Prometheus registry that were never - removed, and `/metrics` listed every encrypted URL's token. The label is now - the pattern of the chi route the request matches, such as `/v1/image/*`, or - `unmatched` for a request that matches none: one the router answers 404 or - 405, or a CORS preflight. The metrics library takes the label before the - request is routed, so the metrics middleware asks the router which route - matches. - 2026-10-15 a `:` in the source path or query no longer moves where the path ends and the query begins (closes #247): the signed data and the cache key joined host, path and query with `:` and no escaping, so a URL signed for -- 2.54.0 From 09c55235c32a708b0b247b4093c64e2a6e80d6ba Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 12:18:06 +0000 Subject: [PATCH 6/8] Test that metrics label a request with an empty path as / A request line such as GET http://pixa.example HTTP/1.1 leaves the request's path empty; chi routes it as /, so its label is the / route's pattern. Model: opus-5-5 --- .../middleware/middleware_internal_test.go | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/internal/middleware/middleware_internal_test.go b/internal/middleware/middleware_internal_test.go index 404d4eb..0e17a05 100644 --- a/internal/middleware/middleware_internal_test.go +++ b/internal/middleware/middleware_internal_test.go @@ -227,10 +227,12 @@ func TestMetricsRecordsServedRequest(t *testing.T) { // each request with the pattern of the route that answers it, never with its // path: two paths one route answers share that route's pattern, and requests // no route answers, a 404 or a 405, share "unmatched", among them requests for -// the path the subrouter is mounted at, /v1 and /v1/. With a label per path, -// any client could add series to the registry without limit. The middleware -// registers with a registry of its own here, as another test in this package -// registers with Prometheus' default registry, which can be done only once. +// the path the subrouter is mounted at, /v1 and /v1/. A request with an empty +// path is answered by the / route, as chi routes it as /, and is labelled /. +// With a label per path, any client could add series to the registry without +// limit. The middleware registers with a registry of its own here, as another +// test in this package registers with Prometheus' default registry, which can +// be done only once. func TestMetricsLabelsRequestsByRoute(t *testing.T) { t.Parallel() @@ -244,6 +246,9 @@ func TestMetricsLabelsRequestsByRoute(t *testing.T) { // The image route is in a subrouter, as in pixa's router. router := chi.NewRouter() router.Use(mw.Metrics()) + router.Get("/", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) router.Route("/v1", func(r chi.Router) { r.Get("/image/*", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) @@ -258,15 +263,21 @@ func TestMetricsLabelsRequestsByRoute(t *testing.T) { {http.MethodGet, "/v1"}, {http.MethodGet, "/v1/"}, {http.MethodPost, "/v1/image/a.example.com/one.jpg/1x1.png"}, + // The request line GET http://pixa.example HTTP/1.1 leaves both + // r.URL.Path and r.URL.RawPath empty. + {http.MethodGet, "http://pixa.example"}, } { - router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequestWithContext( - t.Context(), req.method, req.path, nil)) + r := httptest.NewRequestWithContext( + t.Context(), req.method, req.path, nil) + t.Logf("%s %s: r.URL.Path %q, r.URL.RawPath %q", + req.method, req.path, r.URL.Path, r.URL.RawPath) + router.ServeHTTP(httptest.NewRecorder(), r) } got := requestCountsByHandler(t, registry) t.Logf("requests counted by handler label: %v", got) - want := map[string]uint64{"/v1/image/*": 2, "unmatched": 5} + want := map[string]uint64{"/": 1, "/v1/image/*": 2, "unmatched": 5} if !maps.Equal(got, want) { t.Errorf("requests counted by handler label = %v, want %v", got, want) } -- 2.54.0 From e5059086d74630f148e05bd9ae3c82d326ddb0bf Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 12:32:27 +0000 Subject: [PATCH 7/8] Label a request with an empty path by the / route in metrics When both r.URL.Path and r.URL.RawPath are empty, as for the request line GET http://pixa.example HTTP/1.1, chi routes the request as /, but routePattern asked Find about the empty path and labelled it unmatched. It now uses / then, as chi does. After the rebase onto next, the TODO.md entry is back at the top of Completed Steps. Model: opus-5-5 --- TODO.md | 18 +++++++++--------- internal/middleware/middleware.go | 6 +++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/TODO.md b/TODO.md index dd2d75e..529b4cb 100644 --- a/TODO.md +++ b/TODO.md @@ -41,6 +41,15 @@ P2: security: per-origin rate limiting expired. The goroutine runs with the disk cache disabled (`cache_max_bytes: 0`) too, where it does nothing else, as the negative cache is used either way. +- 2026-10-15 the request metrics are labelled by route, not by path (closes + #254): their `handler` label was each request's path, so every new path, a 404 + or an encrypted URL, added series to the Prometheus registry that were never + removed, and `/metrics` listed every encrypted URL's token. The label is now + the pattern of the chi route the request matches, such as `/v1/image/*`, or + `unmatched` for a request that matches none: one the router answers 404 or + 405, or a CORS preflight. The metrics library takes the label before the + request is routed, so the metrics middleware asks the router which route + matches. - 2026-10-15 a cached variant is served while its source URL is in the negative cache (closes #245): `Get` checked the negative cache before the variant cache, so once a fetch of the source failed, as one does when another variant @@ -51,15 +60,6 @@ P2: security: per-origin rate limiting not in the cache is made from the cached source when there is one. `README.md` says so under `/v1/image/`. A refusal from the negative cache now counts as a miss, as any other request that misses the variant cache does. -- 2026-10-15 the request metrics are labelled by route, not by path (closes - #254): their `handler` label was each request's path, so every new path, a 404 - or an encrypted URL, added series to the Prometheus registry that were never - removed, and `/metrics` listed every encrypted URL's token. The label is now - the pattern of the chi route the request matches, such as `/v1/image/*`, or - `unmatched` for a request that matches none: one the router answers 404 or - 405, or a CORS preflight. The metrics library takes the label before the - request is routed, so the metrics middleware asks the router which route - matches. - 2026-10-15 the host of a `/v1/image/` URL is lower-cased, and one with a port needs a signature (closes #246): with `cdn.example.com` allowlisted, `CDN.Example.com` and `cdn.example.com:8443` passed the allowlist too, and diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 4745a86..a35c23e 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -239,7 +239,7 @@ func (s *Middleware) Metrics() func(http.Handler) http.Handler { // "unmatched" when it matches none or is not served by a chi router. The // metrics library takes the label before the request is routed, so the // router is asked which route matches, with the path it routes on: -// r.URL.RawPath when set, else r.URL.Path. +// r.URL.RawPath when set, else r.URL.Path, else /. func routePattern(r *http.Request) string { pattern := "" @@ -250,6 +250,10 @@ func routePattern(r *http.Request) string { path = r.URL.Path } + if path == "" { + path = "/" + } + pattern = rctx.Routes.Find(chi.NewRouteContext(), r.Method, path) // For the path a subrouter is mounted at, such as /v1 or /v1/, Find -- 2.54.0 From 547456f90e6f4077f4c8430fe69993dd49b05ca4 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 14:04:10 +0000 Subject: [PATCH 8/8] Keep the metrics entry at the top of Completed Steps Model: opus-5-5 --- TODO.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/TODO.md b/TODO.md index 529b4cb..893924a 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 are labelled by route, not by path (closes + #254): their `handler` label was each request's path, so every new path, a 404 + or an encrypted URL, added series to the Prometheus registry that were never + removed, and `/metrics` listed every encrypted URL's token. The label is now + the pattern of the chi route the request matches, such as `/v1/image/*`, or + `unmatched` for a request that matches none: one the router answers 404 or + 405, or a CORS preflight. The metrics library takes the label before the + request is routed, so the metrics middleware asks the router which route + matches. - 2026-10-15 expired negative cache rows are removed on a schedule (closes #244): the row of a failed fetch, which makes pixa answer 502 for that source URL for 5 minutes, was deleted only when the same URL was requested again @@ -41,15 +50,6 @@ P2: security: per-origin rate limiting expired. The goroutine runs with the disk cache disabled (`cache_max_bytes: 0`) too, where it does nothing else, as the negative cache is used either way. -- 2026-10-15 the request metrics are labelled by route, not by path (closes - #254): their `handler` label was each request's path, so every new path, a 404 - or an encrypted URL, added series to the Prometheus registry that were never - removed, and `/metrics` listed every encrypted URL's token. The label is now - the pattern of the chi route the request matches, such as `/v1/image/*`, or - `unmatched` for a request that matches none: one the router answers 404 or - 405, or a CORS preflight. The metrics library takes the label before the - request is routed, so the metrics middleware asks the router which route - matches. - 2026-10-15 a cached variant is served while its source URL is in the negative cache (closes #245): `Get` checked the negative cache before the variant cache, so once a fetch of the source failed, as one does when another variant -- 2.54.0