Repository navigation
fix(grafana): route distinct-metric PromQL arithmetic off native PROMQL - #438
Merged
shmsr merged 3 commits intoSep 7, 2026
Conversation
Elasticsearch's native PROMQL command keeps `__name__` in the implicit vector-matching key, while Prometheus excludes it. Element-wise arithmetic between two different metric names with no explicit matcher therefore can never match: `A / B` returns zero rows where the source panel shows real values. The panel was still reported `migrated` at confidence 0.9 with no warning, so the dashboard shipped looking clean and rendered empty — the silent semantic gap the "degrade gracefully" rule exists to prevent. Probing a live 9.5.0-SNAPSHOT showed the divergence is wider than the bare selector form in the report: Elasticsearch also propagates `__name__` through rate(), abs(), *_over_time() and scalar arithmetic, where Prometheus drops it, so `rate(A[5m]) / rate(B[5m])` is broken too. Aggregations do drop it, which is why `sum(A)/sum(B)` was unaffected and must stay on the native path. Detect the shape on the PromQL AST by resolving which metric names each operand's result carries, and decline the native path when both sides are determinate, non-scalar and disjoint. Those panels fall through to the existing per-key ES|QL STATS/EVAL translator, which computes the real ratio, and carry a note explaining the reroute. This reverses the routing elastic#138/elastic#146 introduced for this subset only: they assumed the native command evaluated the implicit label-set match the way Prometheus does, and it does not. Alerts need the same treatment for a stronger reason — a rule that never fires is quieter than an empty panel — so `_has_source_faithful_query` keeps them automated through the ES|QL route instead of dropping them to manual. It promises a query only when the translator can actually emit one: being steered off the native path is not enough, since the translator has no ES|QL form for changes(), absent() or predict_linear(). Both it and `_generate_esql_for_alert` now share one predicate so they cannot disagree. Explicit on()/ignoring() matchers and `__name__` regex selectors are left alone: Elasticsearch rejects both loudly with a 400, so they never fail silently and are already detectable.
The new probe could promise a source-faithful query for control-bound $var matchers that generation then left empty. Fail that path closed. Also skip the colocated per-document renderer when live co-occurrence proves A and B never share a document, so prometheus-rw ratios like Grafana 763 no longer render empty.
Keep the sparse-document agg(A op B) ES|QL path from this PR and the agg(A or B) classification rewrite from elastic#436; the two bullets overlap in docs/sources/grafana.md.
5 of 8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #376.
Is the issue valid?
Yes. Reproduced on the unmodified base at
0a5340ad64b43764d74d8ae3a64fe22a8bb23a56, and confirmed against a live Elasticsearch 9.5.0-SNAPSHOT rather than taken on faith.Root cause
Elasticsearch's native
PROMQLcommand keeps__name__in the implicit vector-matching key; Prometheus excludes it when matching binary-operator operands. Two different metric names can therefore never match, soA / Breturns zero rows while the source panel shows real values — and the panel was still reportedmigratedat confidence 0.9 with no warning.Bisected against the same index:
A/BaloneA / B(distinct names)B / B(same name)sum(A) / sum(B)B * 2(vector × scalar)Probing further showed the divergence is wider than the report: Elasticsearch also propagates
__name__throughrate(),abs(),*_over_time()and scalar arithmetic, where Prometheus drops it. Sorate(A[5m]) / rate(B[5m])is broken too, not just the bare-selector form. Aggregations do drop the name (andtopk/bottomkpreserve it), which is whysum(A)/sum(B)was unaffected and must stay native.Code paths checked
can_use_native_promqland its existing decline gates (_PROMQL_UNSUPPORTED_RE, unsupported comparisons, range-on-nonselector, nested aggregation, known server bugs)._translate_panel_native_promqlandbuild_native_promql_query.promql.py(_parse_fragment,_ast_from_node,_ast_binary_fragment,_ast_matchers), since thePromQLFragmentIR loses the detail the decision needs._has_source_faithful_query,_generate_esql_for_alert,classify_automation_tier,build_es_query_rule_params.on()/ignoring()joins) and grafana-migrate: PromQL ratio/difference panels migrate as ES|QL approximations instead of native PromQL #138/grafana-migrate: distinct-metric PromQL arithmetic on single-value (gauge/stat) panels still degrades to ES|QL approximation #146 (which deliberately routed distinct-metric arithmetic onto the native path).The fix
Option 1 from the issue — detect and re-route.
promql_has_unmatchable_distinct_metric_binopwalks the AST and resolves the set of metric names each operand's result carries (_ast_result_metric_names). When both sides are determinate, non-scalar and disjoint, the operation can never match, socan_use_native_promqldeclines it and the panel degrades to the existing per-key ES|QLSTATS/EVALtranslator, carrying an operator-visible note.Excluded by design: set operators (
and/or/unless), where distinct names are normal and correct; expliciton()/ignoring()matchers and__name__regex selectors, which Elasticsearch rejects loudly with a 400 and so never fail silently; and anything indeterminate, which suppresses the flag rather than triggering it.Alerts get the same reroute for a stronger reason — a rule that never fires is quieter than an empty panel.
_has_source_faithful_querykeeps them automated via ES|QL, but only promises a query when the translator can actually emit one, since it has no ES|QL form forchanges(),absent()orpredict_linear(). It and_generate_esql_for_alertnow share a single feasibility predicate so they cannot disagree.Side effects considered
Statefulset replicasmovesmigrated/0.9 →migrated_with_warnings/0.6. That reads as a downgrade but is an upgrade in correctness: the old status described a query returning nothing.x,y,breakdown_by). Two HPA panels were already on the ES|QL path and only picked up the explanatory note; their queries are byte-identical.sum(A)/sum(B).can_use_native_promqlinitially made these alerts fall to manual, breakingtest_group_interval_reaches_kibana_schedulewithKeyError: 'schedule'. That was a real regression, not a stale test._has_source_faithful_queryis called ~9 times per alert: 0.588 ms vs 0.184 ms baseline for affected alerts, and zero probes for unaffected ones.Tests
make test6260 passed, 53 skipped, 497 subtests.make lint(ruff + 502 source headers + skill mirror/structure) andmake typecheck(mypy, 10 source files) both clean.tests/test_grafana_issue_376_vector_matching.py: 38 tests / 24 subtests covering unmatchable shapes, matchable shapes that must not regress, macro resolution, native eligibility, alert routing, and probe/real-call parity.test_migrate.pythat encoded the grafana-migrate: PromQL ratio/difference panels migrate as ES|QL approximations instead of native PromQL #138/grafana-migrate: distinct-metric PromQL arithmetic on single-value (gauge/stat) panels still degrades to ES|QL approximation #146 behavior were renamed and inverted; three were added, including an end-to-end guard for the exact reported panel. Three previously asserted only "not PROMQL" and now also requireSTATS,computed_value, and exact status/confidence.No pre-existing failures on the base. One environment caveat:
test_bump_version_script.pyfails under a restrictive sandbox withPermissionErrorcreating a.cursortemp directory — an environment artifact, passing normally.Kibana visual verification
Chrome DevTools MCP against a local 9.5.0-SNAPSHOT stack.
Dashboard
obs-migrate-kube-state-metrics-v2opened in view mode with a cache-bypassing hard reload. The previously-emptyStatefulset replicaspanel now renders a line at 100% with aprometheusseries, matching Grafana.Because the panel is driven by dashboard variables, a green default-state render alone would not prove filters work, so the
namespacecontrol was exercised frommonitoring→production: the series switchedprometheus→postgresand still rendered at 100%, correlating control selection with the affected query. Console showed only a CSP violation from the MCP's own injected script (an artifact, not an application error), and none after the interaction. All 89 xhr/fetch requests returned 200, including everyesql_asynccall. Neighbouring Cluster tiles held at 1.82% / 12.50% / 3.13%, and the twonot_feasiblechanges()panels kept their expected "Migration Required" state.Re-verified after the follow-up review round by re-running the migration and diffing the normalized native artifact against the uploaded one: identical, so the saved object was unchanged.