Repository navigation
feat(promql): lower queries to unified operator and scalar IR - #540
Conversation
5686cf7 to
d9223e3
Compare
bffe654 to
74cb049
Compare
d9223e3 to
26da80b
Compare
74cb049 to
49d558c
Compare
26da80b to
51a5484
Compare
49d558c to
34a7fad
Compare
51a5484 to
b58f2b4
Compare
34a7fad to
7357d5b
Compare
b58f2b4 to
8fb402b
Compare
7357d5b to
eec2e77
Compare
2535016 to
e55e580
Compare
268f020 to
b310b0e
Compare
e55e580 to
397e56a
Compare
b310b0e to
82ea050
Compare
397e56a to
c58ce05
Compare
82ea050 to
7adc4e9
Compare
fb30215 to
d133a1d
Compare
7adc4e9 to
7e6d76f
Compare
69a4e7d to
c08f46a
Compare
8140cb6 to
b2bef90
Compare
c08f46a to
6ee8a19
Compare
6ee8a19 to
d24fa84
Compare
b2bef90 to
04c7694
Compare
d24fa84 to
9a0ade0
Compare
04c7694 to
22ea5e1
Compare
9a0ade0 to
d58225d
Compare
22ea5e1 to
77c6f4e
Compare
d58225d to
c47eee4
Compare
77c6f4e to
6ebe350
Compare
c47eee4 to
1b69820
Compare
6ebe350 to
148c49e
Compare
- timing: a SummaryAgg the lifecycle assignment recomputes at query time now forces the SummaryAgg consuming it to query time (`forces_query_time` reads the assignment), instead of failing with EvaluationUnderMaintenance. - physical export: `PhysicalASAPDAG::validate` requires each node's incoming edges to be exactly its operator's inputs with their roles, so swapped Left/Right edges cannot reorder `a - b` (EdgeRoleMismatch). - physical export: a DAG that fails validation (an empty batch, a hand-timed plan) is an `InvalidPhysicalDAG` error instead of a panic. - precompute: only a TimeRange over (a TimeShift over) a time series Scan takes the raw-sample input contract, as before the port; a table range needs a stored population. - compile: an input binding below a node the PromQL fallback evaluates is an error; the fallback reads raw series slots, so it was silently ignored. A binding below another bound input is still superseded by it, as in `cut`. - compile: drop the scalar-literal binary branch; nothing filled its map. - PromQL fallback: clamp's `NOT (max < min)` is now the IR `Filter` #540 emits; the fallback runs a filter on scalars alone, and the pointwise projection no longer filters on its own. - tests: `raw_series_contract_is_explicit` asserts that a consumed bare selector is instant selection (its comment said the opposite) and that a binding below the fallback is rejected; new `unified_precompute` test. - PromQL fallback: unary minus drops the metric name like every other pointwise projection, following #540 and the Prometheus 3.15 baseline (#511); pointwise_projection_names_and_dynamic_parameters expects it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
148c49e to
578be5c
Compare
Review findings and fixesTwo review rounds. All fixes are in 578be5c (
|
- topk/bottomk by (...): the Limit carries the same partition keys as the
Sort, so `topk by (job) (k, v)` keeps k series per job, not k in total.
Both topk grouping tests now assert the Limit's partition.
- Unary negation drops the metric name, as Prometheus does for vector
negation (`-up` now projects like `abs(up)` and `up * 2`).
- `clamp(v, min, max)` returns no series when `max < min`, as in
Prometheus: unless both bounds are constants in order, the series pass
a `Filter { NOT (max < min) }` before the projection.
- MetricsQL `count(v)` counts series (AggIntent::Count) instead of
distinct sample values (Cardinality with no columns).
- MetricsQL rollups without an explicit range are rejected: the implicit
window is the evaluation step, which lowering does not know. This
extends the existing default_rollup rejection to every rollup.
- MetricsQL bare instant selectors look back one ingestion interval, as
in PromQL. `lower_metricsql_query_workload(workload, now_ms)` reads it
from the workload (new `QueryLanguage::MetricsQL`, validated like
PromQL); the query-level functions take it as a parameter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
- min_of/max_of constant fold returns NaN when any argument is NaN
(Go math.Min/Max), not the other argument.
- histogram_quantiles accepts φ outside [0, 1] and NaN, like
histogram_quantile; the label keeps Prometheus's rendering ("-0.1",
"1.01", "NaN"). bounded_quantile_param removed.
- topk/bottomk/limitk truncate k toward zero; k < 1 lowers to an empty
Limit{n: 0} over the argument; NaN and k overflowing int64 are errors.
- timestamp(v) keeps the sample timestamp only for a (parenthesised)
vector selector; any other argument lowers to a PromqlMap whose sample
is the evaluation timestamp, dropping the metric name.
- label_join(v, dst, sep) with no source labels is accepted (dst empty).
- histogram_fraction errors name the unsupported function (native or
classic buckets); module and walk_histogram docs no longer claim the
histogram_* accessors are lowered.
Each change has a regression test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
- timing: a SummaryAgg the lifecycle assignment recomputes at query time now forces the SummaryAgg consuming it to query time (`forces_query_time` reads the assignment), instead of failing with EvaluationUnderMaintenance. - physical export: `PhysicalASAPDAG::validate` requires each node's incoming edges to be exactly its operator's inputs with their roles, so swapped Left/Right edges cannot reorder `a - b` (EdgeRoleMismatch). - physical export: a DAG that fails validation (an empty batch, a hand-timed plan) is an `InvalidPhysicalDAG` error instead of a panic. - precompute: only a TimeRange over (a TimeShift over) a time series Scan takes the raw-sample input contract, as before the port; a table range needs a stored population. - compile: an input binding below a node the PromQL fallback evaluates is an error; the fallback reads raw series slots, so it was silently ignored. A binding below another bound input is still superseded by it, as in `cut`. - compile: drop the scalar-literal binary branch; nothing filled its map. - PromQL fallback: clamp's `NOT (max < min)` is now the IR `Filter` #540 emits; the fallback runs a filter on scalars alone, and the pointwise projection no longer filters on its own. - tests: `raw_series_contract_is_explicit` asserts that a consumed bare selector is instant selection (its comment said the opposite) and that a binding below the fallback is rejected; new `unified_precompute` test. - PromQL fallback: unary minus drops the metric name like every other pointwise projection, following #540 and the Prometheus 3.15 baseline (#511); pointwise_projection_names_and_dynamic_parameters expects it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
578be5c to
7396e96
Compare
Prometheus 3.15 conformance checkPrometheus v3.15.0's own PromQL test data (
Rejected cases were the deliberate §3.4 gaps (native histograms, fill modifiers, dynamic parameters, |
Why
#511 §1 states the problem: the current representation "mixes query-plan operators and scalar expressions", so "an expression can be placed where a table input is expected and fail only when the plan is checked". #511 §2.3 "Composition without a bridge" and §3.3 "PromQL semantic mapping" give the required PromQL shapes:
§3.3 adds:
up{job="api"}isTimeRange(Instant)over a scan andup[5m]isTimeRange(Range).up > 0is aFilter.up > bool 0is aProjectwithCase(Compare(...), 1.0, 0.0). Vector/vector comparisons keepvector_matchand setreturn_bool. Per-sample math is aProjectwith a scalarFunctionCallwhose "scalar parameters can be expressions; do not require constant-onlyAggIntent::Mathpayloads". §3.4 lists the gaps that "the frontend must reject … until it is supplied", including native-histogram samples, dynamic aggregate/sampling parameters and fill modifiers. §4 requires the §2.3 examples to "need noPromqlScalarBridgeor equivalent constant-wrapper node".#509 §0 "Language-specific frontends" sets the same rule from the planner side: a construct "that cannot be represented faithfully is rejected", and the frontend keeps series identity, evaluation timing and missing-data semantics.
What
This PR covers the PromQL rows of #511 §2.3 and §3.3, and the representable subset of MetricsQL. It also rejects the §3.4 PromQL gaps listed above. It does not change the planner callers: they keep using the legacy entry points until the cutover. It does not upgrade the parser or add runtime execution.
How
This is the frontend stage (#509 §0). Both frontends get a parallel
unifiedmodule. It builds the name-basedUnresolvedOp/UnresolvedScalartree fromasap-frontend-common(added in #539) and resolves it withresolve_root/resolve_scalar_rootintoOperatorNode/ScalarExpr. No legacyQueryExpris built and no conversion between graphs is added.PromQL (
crates/frontend-promql/src/unified/):lower_promql_query_workloadrequiresQueryLanguage::PromQL, runsworkload.validate(), and readsdata_ingestion_intervalatnow_ms. Expired or future evidence fails. Each entry is lowered with its ownrequirements.accuracy.target().Native, the query is rejected (UnsupportedFeature).value_type()isScalar, the query becomesQueryRoot::Scalarthroughlower_scalar. Otherwise it becomesQueryRoot::Operatorthroughwalk. A scalar-typed expression at an operator position is an error, so a scalar never becomes an operator node.lower_scalar). Constant sub-expressions fold toLiteral(Float64). Otherwise-x→Negative, scalar arithmetic →Arithmetic,time()→EvalTimestamp,pi()→Literal(π),scalar(v)→PromqlScalarFromVector(<operator plan of v>), anda < bool b→Case(Compare → 1.0, else 0.0). A scalar comparison withoutboolis rejected. All scalar nodes carryExprSemantics::Promql.PromqlScalarOp { child, scalar, op, scalar_left, return_bool }. The resolver turns it into aFilter(comparison withoutbool, sample values kept) or aProject(arithmetic, or comparison withbool). An operator read inside the scalar (for examplescalar(sum(up))) stays a visible child of that node.BinaryOp { operator: BinaryOperator { kind, vector_match, .. }, return_bool, lhs, rhs }.CompareBoolis no longer used to encodebool.abs,round,clamp*, …), unary minus and calendar functions becomePromqlMap. The resolver projects the sample throughFunctionCall { name: "promql_<fn>", args }(orNegative) and keeps the full series identity. Extra arguments go throughlower_scalar, so they can be dynamic (time() - 1,scalar(...)).round(v)gets a default1.0. Unary minus, math and calendar functions all drop the metric name, as Prometheus 3 does (the docs: define unified operators and SQL/PromQL scalar boundaries #511 baseline is 3.15).clamp(v, min, max)returns no series whenmax < min: unless both bounds are constants in order, the series first pass aFilter { NOT (max < min) }, which lets NaN bounds through as Prometheus does.m{…}→TimeRange { range: data_ingestion_interval, kind: Instant }over the scan.m{…}[w]→TimeRange { range: w, kind: Range }.offset/@wrap the scan inTimeShift.topk/bottomk→Limit { n: Some(k), partition_by: keys }overSort { value, partition_by: keys }, sotopk by (job)keeps k series per job.topkovercount_over_time/sum_over_timestays the heavy-hitterAggIntent::TopK.histogram_quantile. A declaredClassicBucket, or classic-bucket evidence (by (le), a_bucketmetric, anlematcher), gives exactHistogramQuantile. A declaredRawSamplesgives the sketchableQuantilewith the entry's accuracy. Mixed declarations,Native, and undeclared metrics without bucket evidence are rejected.histogram_count/sum/avg/stddev/stdvar/fractionare rejected.topk/bottomk/limitktruncatektoward zero, andk < 1is an emptyLimit { n: Some(0) }(NaN or an overflowingkis an error).min_of/max_ofconstant-fold with NaN propagation (Go'smath.Min/Max).histogram_quantilesaccepts any φ, including NaN and values outside [0, 1], likehistogram_quantile.timestamp(v)uses the sample timestamp only whenvis a vector selector (after parentheses); any other argument gives the evaluation time.label_joinwithout source labels emptiesdst.limitk/limit_ratioparameters, non-constantmin_of/max_of, and fill modifiers.MetricsQL (
crates/frontend-metricsql/src/unified/mod.rs) has the same structure as the legacy MetricsQL lowerer, with these changes: a bare number literal becomesQueryRoot::Scalar(Literal). A number literal operand becomesPromqlScalarOp. Unary minus becomesPromqlScalarOp(* -1). Matchers carryExprSemantics::Promql. Range windows becomeTimeRange(Range), and a bare instant selector becomesTimeRange(Instant)over one ingestion interval, as in PromQL.count(v)counts series (AggIntent::Count). A rollup without an explicit range is rejected, because its implicit window is the evaluation step, which lowering does not know.lower_metricsql_query_workload(workload, now_ms)reads the interval from a MetricsQL workload, validated like a PromQL one (newQueryLanguage::MetricsQL). Everything it did not support before is still rejected.The old entry points stay so each stack layer builds on its own. The
unifiedmodules are documented as "promoted to the root API at planner cutover".Key code interfaces
PromQL,
crates/frontend-promql/src/unified/mod.rs:unified/error.rs:unified/histogram.rs:MetricsQL,
crates/frontend-metricsql/src/unified/mod.rs:Usage:
The output types (
QueryRoot,OperatorNode,ScalarExpr,NonASAPOp) and the unresolved tree (UnresolvedOp::PromqlMap,UnresolvedOp::PromqlScalarOp,resolve_root,resolve_scalar_root) come from earlier PRs in the stack. This PR only uses them. The crate-private entry point isPromqlLowerer::lower_query_with_ingestion_interval(query, accuracy, interval) -> Result<QueryRoot>. Test helpers live intests/unified_support.rs.Fields
PromQL lowering functions
workload&PlanningWorkloadlanguage = PromQL(elseWrongLanguage) and passvalidate()(elseInvalidWorkload). Each entry fromquery_workload.entries()is lowered with its ownrequirements.accuracy.target().data_workload.data_ingestion_intervalis required.now_msu64value_at(now_ms)), elseInvalidWorkload(UnavailableDataIngestionInterval). Expiry is inclusive.histogramsHistogramCatalog*_query_workload*Vec<QueryRoot>Scalarfor scalar-typed queries,Operatorotherwise.lower_promql_workload*Vec<Rc<OperatorNode>>UnsupportedFeature("scalar root: use lower_promql_query_workload").PromqlErrorInvalidWorkload(WorkloadError)now_ms.Parse(String)promql-parserrejected the string.UnsupportedFunction(String)UnsupportedAggregateOp(String)UnsupportedFeature(String)histogram_quantile, a scalar at an operator position, a scalar root on the vector-only API, fill modifiers, and similar.MissingArgument(String)InvalidParameter(String)boolor a NaNtopkk.WrongLanguage(String)Convert(ResolveDAGError)asap-frontend-commonfailed.HistogramKind(what a client declares for a metric)is_sketchablehistogram_quantileresultClassicBucketfalseHistogramQuantile(cumulativelebuckets)NativefalseRawSamplestrueQuantilewith the entry's accuracy target. This is an explicit extension, not standard PromQL histogram semantics.HistogramCatalog: a map from metric name toHistogramKind.new()is empty.with(metric, kind)adds or replaces one entry (builder style).kind_of(metric)looks one up.is_empty()reports an empty catalog. If no metric in the argument is declared, only classic-bucket evidence is accepted.MetricsQL functions and
MetricsqlErrorquery: &straccuracy: AccuracyTargetCount(count,count_over_time) andQuantileintents.ingestion_interval: DurationTimeRange(Instant)).lower_metricsql_query_workloadMetricsQL, validates the workload, readsdata_ingestion_intervalatnow_msand lowers every entry with its own accuracy.MetricsqlExprmetricsql_parser::ast::Expr.parse_metricsqlreturns it;canonical_metricsqlreturns itsto_string().lower_metricsql_queryresultQueryRoot::Scalar(Literal)for a bare number,QueryRoot::Operatorotherwise.lower_metricsqlrejects the scalar case.Parse(String)WrongLanguage(String)InvalidWorkload(WorkloadError)UnsupportedFeature(String)@, subquery step,keep_metric_names, vector-matching modifiers,if/ifnot/default, …).Resolve(String)Before this PR
Before this PR, the PromQL frontend (
crates/frontend-promql/src/promql.rs) and the MetricsQL frontend (crates/frontend-metricsql/src/lib.rs) build only the legacyQueryExpr. On the same queries:QueryExpr(before)2PromqlScalarBridgeleaftime()EvalTimestampoperator leafup * 2BinaryOp{Mul, up, PromqlScalarBridge(2)}BinaryOpinput-upBinaryOp{Mul, up, PromqlScalarBridge(-1)}up > bool 0BinaryOp{CompareBool, up, PromqlScalarBridge(0)}abs(up)Aggregate{[Math(Abs)]}round(up, scalar(sum(other)))Math::Round { to_nearest }needs a constantupvsup[1s]TimeRange{1s}histogram_quantile(0.9, native_latency)(no catalog entry, nole)Quantilehistogram_count(v)Aggregate{[HistogramCount]}After this PR
asap_frontend_promql::unified::lower_promql_query_workload(and the MetricsQLunifiedmodule) lower a query to aQueryRoot: a scalar query is aScalarExpr, anything else a validatedRc<OperatorNode>. Every row of the table above takes its #511 shape: scalars are expressions,up * 2andup > bool 0areProjects,up > 0is aFilter, per-sample functions are typedFunctionCalls with dynamic arguments, and instant and range selectors are distinct. Native-histogram and other §3.4 gaps fail withUnsupportedFeatureinstead of being treated as raw samples. Planner callers are unchanged until the cutover (#542).Evidence
Execution examples from the tests. Screenshots and measurements: not applicable (no visual output, no performance claim).
End to end:
up * scalar(sum(up))Input:
lower_promql_query_workloadon a PromQL workload with this one query,Exactaccuracy and a 1 s ingestion interval (fromtests/unified_scalar_design.rs,scalar_plan_dependencies_remain_visible).The parser types the root as an instant vector, so
walkis used.walk_binarysees that the right operand is scalar-typed. The left sideuplowers toTimeRange{1s, Instant}overScan{up}. The right side goes throughlower_scalar:scalar(...)→PromqlScalarFromVector(<plan of sum(up)>).The result is
PromqlScalarOp { op: Mul, scalar_left: false, return_bool: false }. The resolver adds series identity to the open PromQL schema and builds aProjectwhosevaluecolumn isArithmetic(Mul, sample, PromqlScalarFromVector(..)).Output:
The test asserts
node.children().len() == 2: the plan read by the scalar is a visible dependency, not hidden inside the expression.Before/after on the #511 examples
From
tests/unified_scalar_design.rs,tests/unified_promql_lowering.rsandtests/unified_promql_conformance.rs:2,time(),scalar(sum(up)) + 1,1 < bool 2,-time()QueryRoot::Scalar, type-checks with an empty schemastandalone_scalars_are_expressionsup * 2Project,value = Arithmetic(Mul, sample, Literal(2.0)), pluspromql_drop_metric_name; series identity andtime_indexkeptarithmetic_projects_the_sample_and_preserves_full_identity_and_timeup > 0,0 < upFilter; output schema equals the input schemanon_bool_comparisons_keep_vector_samples_even_with_scalar_on_leftup > bool 0ProjectwithCasebool_comparison_projects_zero_or_one-upProjectwithNegative; metric name dropped, rest of the series identity keptunary_minus_drops_the_metric_nameclamp(up, 5, 1),clamp(up, time() - 1, time())Filter { NOT (max < min) }under the projection; none forclamp(up, 0, 100)clamp_filters_out_inverted_boundsabs(up),round(up, scalar(sum(other))),clamp(up, time() - 1, time()),year(up),hour()ProjectwithFunctionCall;validate_structurepassespointwise_functions_are_typed_scalar_projectionssum by (job) (rate(http_requests_total[5m]))Aggregate{by [2], [Sum]}overAggregate{[Rate]}overTimeRange{5m}sum_by_over_rate_groups_the_outer_suma / on(host) bBinaryOpwithvector_match { kind: On, labels: ["host"] }binary_op_with_on_grouping[w]of the same lengthTimeRange{Instant}vsTimeRange{Range}instant_and_range_selectors_of_equal_length_stay_distincthistogram_quantile(0.9, sum by (le) (rate(..._bucket[5m])))HistogramQuantileheuristic_baseline_is_unchanged_without_a_cataloghistogram_quantile(0.9, foo_bucket)withfoo_bucket: RawSamplesQuantiledeclared_raw_extension_and_native_gap_override_the_heuristichistogram_quantile(0.9, latency_seconds)withClassicBucketHistogramQuantiledeclared_classic_bucket_fixes_the_false_negativeRejected on purpose:
histogram_quantile(0.9, native_latency), no catalogUnsupportedFeature(needs classic-bucket evidence)heuristic_baseline_is_unchanged_without_a_catalogfoo_bucketdeclaredNative(even a bare selector)UnsupportedFeaturedeclared_raw_extension_and_native_gap_override_the_heuristichistogram_count(v)and other native accessors,histogram_fractionUnsupportedFeaturenative_histogram_accessors_are_explicit_gaps,histogram_fraction_is_an_explicit_gaplimitk(scalar(foo), m),limit_ratio(time() % 17 / 17, m),limitk(NaN, m)dynamic_and_non_finite_sample_params_are_rejected1 < 2,time() > 0(scalar comparison withoutbool)InvalidParameter)scalar_comparison_without_bool_is_rejecteda + fill(0) band other fill modifiersUnsupportedFeaturefill_modifiers_are_rejected_not_ignoredmin_of/max_ofnon_constant_min_of_max_of_is_rejected__GAPVerification
crates/frontend-promql/tests/unified_promql_lowering.rsandunified_promql_conformance.rs(the accepted and rejected cases above),unified_scalar_design.rs(the docs: define unified operators and SQL/PromQL scalar boundaries #511 §2.3 scalar examples, e.g. a plan read byscalar(...)stays a visible child),unified_histogram_metadata.rs(catalog-declared histogram kinds override the_bucket/leheuristic);crates/frontend-metricsql/tests/unified_lowering.rs(series count, rollups without a range, instant lookback, workload entry point).Limitpartition, unary minus, clamp bounds and every MetricsQL change have a test that fails without the fix.promql/promqltest/testdata, 21 files, 2,242 cases) was run through this lowering and the feat(runtime): export timed plans as PhysicalASAPDAG and compile them #541 runtime as it stood then. Every case the runtime could execute matched, apart from the lowering differences fixed in the last commit (min_of/max_ofNaN,histogram_quantilesφ,topkk,timestamp()of a non-selector,label_joinwithout sources), each with a regression test, and the gaps in PromQL: remaining gaps vs Prometheus 3.15 (sort_by_label order, classic histogram_fraction, count type, scalar() parameters) #661. Rejected cases were the deliberate §3.4 gaps and experimental syntax promqltest enables. The planner no longer has a PromQL evaluator (feat(runtime): export timed plans as PhysicalASAPDAG and compile them #541), so this check is not repeatable in-repo.cargo fmt --check, workspace all-target Clippy with-D warnings, andcargo test --workspace(1,995 tests, 0 failures) on the PR head.Architectural decisions
unifiedmodule and shared resolver as feat(sql): lower SQL to unified operator and scalar IR #539, no conversion from the legacyQueryExpr.QueryRoot::Scalar, and a scalar at an operator position is an error, so a scalar never becomes an operator node (docs: define unified operators and SQL/PromQL scalar boundaries #511 §2.3). Rejected: wrapping scalars in a bridge operator, as the legacyPromqlScalarBridgedid.PromqlScalarOp, which the resolver expands into aFilter(comparison withoutbool) or aProject; vector/vector operations stayBinaryOp, andreturn_boolreplaces the legacyCompareBool.Limitations and follow-up
lower_promql_workload/lower_metricsqluntil the cutover (refactor(planner): plan the workload on the unified operator IR #542), which also gives the planner's MetricsQL input a planning clock. The temporary parallel entry points are removed at the end of the stack.quantile(scalar(q), up), fill modifiers, request-context durations such asstep()). They are rejected, not supported.data_ingestion_interval, as in the legacy lowerer. docs: define unified operators and SQL/PromQL scalar boundaries #511 §2.1 says theInstantlookback "is not the ingestion interval". This PR adds theInstantkind but does not change the horizon.2*3: MetricsQL: rate/increase/delta/changes use Prometheus semantics instead of VictoriaMetrics #653.sort_by_labelorder (natural sort, tie-break),histogram_fractionover classic buckets,counttyped Int64, scalar parameters read from a series: PromQL: remaining gaps vs Prometheus 3.15 (sort_by_label order, classic histogram_fraction, count type, scalar() parameters) #661.Stack: tracker #528 · base: main (#539 merged) · next: #541.
Human review — do not complete with an agent
🤖 Generated with Claude Code
https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW