Repository navigation
Conversation
ErrorAn error occurred while processing your request: |
LukasCuperDT
left a comment
There was a problem hiding this comment.
Nice change and good tests. A few things before merge:
- Unbounded expansion (DoS).
expand_bracesproduces the cartesian product of every{…}group in a user-supplied target.health.prod.{a,b}{a,b}…with 25 groups yields ~33M strings from one unauthenticated request. Please cap the number of groups and/or total expansions (and reject or return[]beyond it). - Sequential backend calls.
health.<env>.*awaitsget_service_healthonce per service in sequence, so N services = N round-trips in a row. Something likefutures::stream::iter(..).buffer_unordered(k)(orjoin_allwith a bound) would keep "All" panels responsive. - Semantics vs Graphite — worth a test or a doc comment:
glob::Patternlets*match across.; Graphite stops at..- Nested braces (
{a,{b,c}}) aren't handled. - The target is split on
.before expansion, so a brace member containing a dot breaks.
LukasCuperDT
left a comment
There was a problem hiding this comment.
Requesting changes per my earlier review: cap brace expansion (currently unbounded cartesian product from user input) and avoid sequential per-service backend calls for globbed health targets.
…n parallel Review follow-up: a target may carry at most 8 brace groups and expand to at most 256 alternatives, anything beyond matches nothing; braces may nest; patterns are matched node by node so * and ? stop at a dot, as in Graphite; health.<env>.<glob> fetches up to 8 services concurrently, order preserved.
|
Addressed in df81808:
|
There was a problem hiding this comment.
Summary
Adds Graphite-style glob/brace expansion to the /render handler for flag.* and health.* targets (src/graphite.rs), replacing the old exact-match + trailing-* logic, and adds a futures dependency for stream-based concurrent health fetches. The intent is sound: health.<env>.* and {a,b,c} multi-value targets now expand against the configured services. However, the reviewed revision has a few concerns that should be verified before merging (CI build/test are still queued, so none of this is compile- or runtime-verified).
Findings
Warning
-
Health branch appears to have lost its early return — no-match / error behavior is unclear (
src/graphite.rs, ~line 347–375, thetarget_parts.len() == 3health block).
The old code returned a200immediately whenget_service_healthsucceeded, and fell through to the generic Graphite path on error. The new code buildsseriesviastream::iter(expand_pattern(...)).map(...). I can only see it collect intoseries, with no visible earlyreturn. If the code now always returnsseries(even when empty) or falls through to the Graphite branch, the behavior changes:- A target that matches no configured service would previously fall through to Graphite (which would 404), but now may return an empty
200response (or hit the Graphite path with an unexpanded brace string). - The previous "error → fall through to Graphite" path should be intentional; confirm it is preserved or explicitly 404/500.
Fix: make the fallthrough behavior explicit — ifexpand_patternreturns empty, return404(matching Graphite semantics) rather than200 [], and preserve/replace the error-fallthrough with a deliberate response.
- A target that matches no configured service would previously fall through to Graphite (which would 404), but now may return an empty
-
expecton required query fields panics the handler (src/graphite.rs,handler_render, ~lines 235–250).
max_data_points.expect(" is required"),query.target.as_ref().expect("Target is required"), etc. will panic (500) on a malformed request instead of returning a clean 400. Pre-existing, but this PR restructures the handler, so it's a good moment to replace these withOptionpropagation andStatusCode::BAD_REQUEST.
Suggestion
-
Unbounded concurrency in health fan-out. The
stream::iter(...).map(...)that fetchesget_service_healthper expanded service will issue one request per matched service. For a large multi-value variable or a broad glob, this is unbounded. Considerbuffer_unordered(N)with a modestNto bound in-flight requests. -
New
expand_patternhelper (src/graphite.rs:409) is not fully visible in this review — I could not inspect its glob/brace semantics (e.g., whether{a,b,c}at a non-last position is handled, escaping, or empty-expansion cases). The flag branch (flag.<env>.{a,b,c}) relies on it producing exactservice.metrickeys that then matchstate.flag_metrics; verify it produces exactly the key format the maps use (e.g.,svc.flagvsflag.svc). Worth a unit test forexpand_patterncovering*,{a,b,c}, and literal names.
Verdict
The core change is reasonable, but the health-branch control flow (finding #1) needs to be confirmed before this merges. I'd like to see the fallthrough/empty-match behavior resolved.
I could not run the build or tests (CI is queued), so the above is based on the diff and surrounding code only.
|
Checked against df81808, the reviewed revision:
CI on this revision: fmt, clippy -D warnings and 80 tests green. |
LukasCuperDT
left a comment
There was a problem hiding this comment.
Thanks for df81808 — bounded concurrency, per-node glob matching and nested braces look good, and I agree with your answers to the autoreview (200 [] unchanged; the expect()s are pre-existing).
One blocker left: the 256 cap is checked after the product is built. expand_pattern calls expand_braces(pattern), which materialises every combination, and only then compares alternatives.len() with MAX_PATTERN_EXPANSIONS. MAX_BRACE_GROUPS limits the number of groups, not members per group: 8 groups × 30 members = 30⁸ ≈ 6.5×10¹¹ strings from a ~500-character query.
Please bound it before expanding, e.g. multiply the top-level member counts of each group (recursively for nested ones) and return [] once the product exceeds 256, or thread a remaining budget through expand_braces and stop as soon as it is exhausted. A test with something like 8 groups × 30 members that returns [] quickly would lock it in.
expand_braces built the whole cartesian product and only then was its length compared with MAX_PATTERN_EXPANSIONS; MAX_BRACE_GROUPS limits the groups, not their members, so 8 groups of 30 members materialised 30^8 strings before being refused. The budget is now threaded through the recursion and expansion stops as soon as it would exceed 256. The cap test gains that 8x30 case and asserts the at-limit pattern is really accepted.
There was a problem hiding this comment.
Summary
This PR adds Graphite-style glob + brace expansion to the /render endpoint for flag.<env>.<service>.<metric> and health.<env>.<service> targets, so that Grafana multi-value variables ({a,b,c}) and * (All) resolve against configured services instead of returning empty. New helpers expand_pattern / expand_braces / expand_braces_within are added with explicit expansion budgets (MAX_BRACE_GROUPS = 8, MAX_PATTERN_EXPANSIONS), plus a set of unit and integration tests. Cargo.toml adds a futures dependency for the async fan-out on the health path.
I could only see a truncated portion of src/graphite.rs (the full bodies of the health fan-out loop and the expand_* helpers were cut off), and CI build/test/Analyze are still in progress, so I could not confirm compilation or run the tests. The items below are scoped to what I was able to verify; please re-check the ones I marked as "verify" once you have the full file in front of you.
Findings
Warning — potential unbounded concurrency on the health.<env>.* path (verify)
src/graphite.rs ~line 352: the health target now does
let series: Vec<serde_json::Value> = stream::iter(expand_pattern(target_parts[2], &known))
.map(|s| ... get_service_health(...)) ...When a panel issues health.<env>.*, expand_pattern can match every configured service, and if the stream is collected with join_all / buffer_unordered without a cap, all backend get_service_health calls are issued simultaneously. With a large service set this can saturate the TSDB connection pool / cause timeouts under load.
Fix: cap the fan-out, e.g. .buffer_unordered(N) or .buffered(N) with a small constant (e.g. 8–16), and confirm the response still includes all services (only ordering changes).
Warning — .expect() on required render fields panics the request (pre-existing, now more reachable)
src/graphite.rs handler_render:
x.max_data_points.expect(" is required") // and
query.max_data_points.expect("Query is required")A /render request without maxDataPoints panics (500) rather than returning a clean 400. This predates the PR, but the glob/brace expansion makes /render a much hotter path, so it's worth converting to a proper Err(StatusCode::BAD_REQUEST) rejection while you're in the file.
Suggestion — glob semantics on the flag path changed from starts_with to glob match
src/graphite.rs ~line 280: the previous code did a plain metric.starts_with(target) when the metric name ended in *. It is now expand_pattern(&metric_name, &known) using glob matching. The tests (test_expand_pattern_exact_name_is_returned_unchanged) confirm exact names are unchanged and */?/[...] behave as Graphite expects, which is the intent. Just double-check that no existing dashboard relied on the old starts_with-on-trailing-* behavior in a way glob doesn't cover (e.g. a trailing * was previously a pure prefix match; glob * matches any suffix within a segment). The added tests look thorough, so this is likely fine — flagging for confirmation.
Suggestion — good: expansion budget guards against DoS
MAX_BRACE_GROUPS = 8 and the expand_braces_within limit (returning None/empty once exceeded) are a sensible defense against pathological brace expressions (e.g. a large {a,b,c,...} in a target). The tests test_expand_pattern_caps_brace_expansion cover this. No change needed.
Notes / open items
- CI is pending:
build,test, andAnalyze (rust)werein_progressat review time, so I could not confirm the code compiles (newfuturesdep +stream::iter/StreamExtusage) or that the new tests pass. Please confirm they go green before merging. - I could not read the full bodies of
expand_pattern/expand_braces_withindue to output truncation; the brace-nesting and stray-brace handling (test_expand_braces) appears to be well tested, but a final look at the helper implementations is warranted for edge cases like an unclosed bracket within a brace member.
No correctness blockers were identified in what I could verify; the design (glob + brace expansion with explicit budgets, exact names preserved, find endpoint untouched) matches the stated goal.
|
Agreed, done in 2ebd2f8: the budget is threaded through the recursion so expansion stops as soon as it would exceed 256 alternatives, and the cap test now covers 8 groups x 30 members returning [] quickly. |
Grafana sends
{a,b,c}for a multi-value variable and*for All, but /render only matched exact service names (plus a trailing*on flag metrics), so every such panel and thehealth.<env>.*alert rules on dashboard.tsi-dev came back empty; the health and flag targets are now expanded against the configured services with the same glob syntax Graphite uses, exact names behave as before, and the find endpoint is untouched.