Repository navigation
fix(model_gateway): bound HTTP metrics path label to matched route - #1679
Conversation
The HTTP layer-1 metrics labeled each request by
normalize_path_for_metrics(uri.path()), which only collapsed dynamic
segments at path index > 2. Parameterized routes whose dynamic segment
sits at index <= 2 (e.g. /v1/responses/{response_id}, /workers/{worker_id},
/v1/conversations/{conversation_id}, /v1/tokenizers/{tokenizer_id})
therefore passed the raw id through verbatim as the "path" label. Each
distinct id was interned into the never-evicted global STRING_INTERNER and
emitted as a distinct Prometheus series, letting attacker-controlled ids
drive unbounded metric cardinality and unbounded interner growth.
Label by axum's MatchedPath route template instead, falling back to a
fixed "other" when no route matched, which bounds the label set to the
registered route table. Remove the now-obsolete segment-based normalizer.
Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR switches HTTP metrics path labeling from normalizing raw request paths to using Axum matched route templates. A new ChangesPath label refactoring
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request replaces the custom path normalization logic with Axum's MatchedPath extension to bound metric cardinality and prevent unbounded label growth. Feedback suggests optimizing performance on the hot path by using intern_string instead of .to_owned() on the matched path label to avoid allocating a new String on every HTTP request.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| fn call(&mut self, req: Request) -> Self::Future { | ||
| let method = method_to_static_str(req.method().as_str()); | ||
| let path = normalize_path_for_metrics(req.uri().path()); | ||
| let path = matched_path_label(req.extensions()).to_owned(); |
There was a problem hiding this comment.
To avoid allocating a new String on every single HTTP request, we can leverage the existing intern_string helper to obtain a cheaply cloneable Arc<str> instead of calling .to_owned(). Since the matched path templates are bounded and server-controlled, they are safe to intern.
| let path = matched_path_label(req.extensions()).to_owned(); | |
| let path = crate::observability::metrics::intern_string(matched_path_label(req.extensions())); |
References
- For types that are frequently cloned on hot paths and represent a small, repeated set of values, use an interned string type like
Arc<str>to improve performance by making clones cheap.
There was a problem hiding this comment.
Clean fix. matched_path_label correctly reads the MatchedPath extension (populated by axum's router before Router::layer()-applied middleware runs) and falls back to "other" for unmatched paths. The .to_owned() in the call method is necessary since req is moved into the async block. Tests cover matched, unmatched, and interner-growth scenarios through the real layer stack. No issues found — 0 Important, 0 Nit, 0 Pre-existing.
Description
Problem
model_gateway records HTTP layer-1 metrics (
smg_http_requests_total,smg_http_request_duration_seconds) with apathlabel derived from the raw request URI vianormalize_path_for_metrics. That helper only replaces dynamic segments at path index > 2, so parameterized routes whose id sits at index ≤ 2 —/v1/responses/{response_id},/workers/{worker_id},/v1/conversations/{conversation_id},/v1/tokenizers/{tokenizer_id}, etc. — pass the raw id through unchanged. The label is then interned into the process-global, never-evictedSTRING_INTERNERand emitted as a distinct Prometheus time series. An attacker hitting such a route with many distinct ids drives unbounded metric cardinality and unbounded interner memory growth — a remote DoS.(Genuinely unmatched paths hit
sink_handler, whichserver.rsregisters via.fallback(...)after the metrics/logging.layer(...)calls, so they do not flow through these layers today. The reachable vector is matched parameterized routes.)Solution
Label HTTP metrics by axum's
MatchedPathroute template, which is bounded to the registered route table, and map a missingMatchedPathto a fixed"other". A newmatched_path_label(&http::Extensions) -> &strhelper reads the template from request extensions; both call sites (theHttpMetricsLayerservice and theRequestLoggertracing hook) use it.MatchedPathis populated by axum's router before the per-endpoint layer stack runs, so the.layer()-applied middleware (registered on the merged router inserver.rs) observes it for matched routes — verified against axum 0.8.9 and by test. The obsoletenormalize_path_for_metrics/is_dynamic_idhelpers and tests are removed.Operational note: the
pathlabel value changes from normalized request paths to route templates (e.g./v1/responses/{response_id}instead of/v1/responses/resp_abc→{id}). Dashboards/alerts keyed on the old normalized values should be updated.Changes
model_gateway/src/middleware/metrics.rs: replacenormalize_path_for_metrics/is_dynamic_idwithmatched_path_label(returns theMatchedPathtemplate or"other"); use it in the layer; update the module doc.model_gateway/src/middleware/logging.rs:RequestLogger::on_requestlabels viamatched_path_label(request.extensions())."other"; 1000 distinct ids through the realHttpMetricsLayerdo not grow the interner.Test Plan
matched_route_uses_template_label—/v1/responses/resp_abc123against route/v1/responses/{response_id}is observed at aRouter::layer-applied middleware as/v1/responses/{response_id}(provesMatchedPathis available at that layer).unmatched_path_collapses_to_other—/totally/unregistered/aaaa→"other".distinct_ids_on_matched_route_do_not_grow_interner— drives the realHttpMetricsLayer, sends 1000 distinct/v1/responses/resp_{i}requests, asserts global interner growth < 100. Confirmed to FAIL before the fix (reverting the label toreq.uri().path()reports "interner grew by 1000 for 1000 distinct request ids") and pass after.matched_path_label_defaults_to_other_when_absent— helper returns"other"for empty extensions.Authoritative gate (sccache disabled,
RUSTC_WRAPPER=""):Checklist
cargo +nightly fmtpassescargo clippy --all-targets --all-features -- -D warningspassesSummary by CodeRabbit
Bug Fixes
Refactor