feat(grafana): emit native PROMQL for vector matching on capable targets - #442
Merged
shmsr merged 1 commit intoSep 22, 2026
Conversation
Elasticsearch gained PromQL vector matching on Serverless and Stack 9.6 (elastic/elasticsearch#155634), but `_PROMQL_UNSUPPORTED_RE` still listed `on(`, `ignoring(`, `group_left`, and `group_right` among the constructs blocked unconditionally. That list predates the capability, so every matched panel fell through to the ES|QL join/ratio approximation no matter what the target could actually evaluate — losing labels and warning about it on clusters that could have run the operator's PromQL verbatim. Going native needs two independent facts, and guessing either one is expensive: emitting a query the target cannot plan replaces a rendering panel with a hard Kibana error, which is strictly worse than an approximation. So both are established rather than assumed. The target half is probed, not version-gated, because Serverless carries the capability without a 9.6 `version.number`. The probe matches `vector(1)` operands so it needs no index or data, and it fails closed: only HTTP 200 enables the native path, so an older stack, an auth error, or an unreachable cluster all keep today's behavior. The shape half exists because support is necessary but not sufficient — Elasticsearch only matches operands whose label set it can determine statically, and rejects anything else with `vector matching requires operands with concrete label sets`. A new AST predicate models that rule, so a raw selector, a `rate()`/`*_over_time()` over one, or `without (...)` keeps the ES|QL translation. This is why the issue's own `group_left` example over bare selectors stays on ES|QL: emitting it natively would break a working panel. Panels that stay behind say which of the two halves declined, distinguishing an unverifiable probe from a verified absence so the operator knows whether to upgrade, fix access, or reshape the query. Panels that go native record `minimum_kibana_version: 9.6.0`, because the artifact is portable while the query it carries is not. Comment handling is part of the gate rather than an afterthought: PromQL allows a comment between a token and its parenthesis, so `on # note\n(x)` would otherwise hide a matcher from the check and route it native on a target that cannot run it. Comments are stripped from the raw expression, where their extent is exact; the gates that scan `_clean_promql_for_native` output keep scanning it with comments intact, because flattening newlines destroys that extent and stripping there would swallow real operands. `or`/`and`/`unless` with a matcher stay blocked (elastic/elasticsearch#158181). The verifier's label-set classifier now also recognizes the 9.6 wording, so a shape that ever slips past this gate is reported as a translator bug instead of going unlabeled.
1 of 3 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 #440.
Was the issue valid?
Yes — with one correction to its own acceptance table, explained under Deviation below.
Root cause
_PROMQL_UNSUPPORTED_REinobservability_migration/adapters/source/grafana/panels.pylisted
on(,ignoring(,group_left, andgroup_rightamong the PromQL constructsblocked from native emission unconditionally. That list predates
elastic/elasticsearch#155634, so every vector-matched panel fell through to the ES|QL
join/ratio approximation regardless of what the target could evaluate — dropping labels
and emitting semantic-loss warnings on clusters that could have run the operator's PromQL
verbatim.
The fix
Native emission now requires two independent facts, each established rather than assumed,
because emitting a query the target cannot plan replaces a rendering panel with a hard
Kibana error — strictly worse than an approximation.
1. The target evaluates it. New
PROMQL_VECTOR_MATCHINGruntime feature, detected bya probe rather than by reading
version.number, so Serverless (which has the capabilitywithout a 9.6 version string) is recognized. The probe uses
vector(1)operands so itneeds no index or data:
vector(1)is deliberate: a probe over real selectors cannot answer the question, becausea capable target still rejects a bare selector and returns HTTP 500 for a matched binary
op over metrics the index lacks — either shape would report "unsupported" on a capable
cluster. The probe fails closed (only HTTP 200 enables native), the opposite default from
PROMQL_COMMAND_V0, and its verdict is printed underTarget PromQL profile.2. The operand shapes are plannable. Support is necessary but not sufficient:
Elasticsearch only vector-matches operands whose label set it can determine statically, and
rejects the rest with
vector matching requires operands with concrete label sets. A newAST predicate (
promql_vector_matching_has_indeterminate_operand) models that rule —aggregations with
by (...), baresum(...), andvector(n)are concrete and propagatethrough functions, unary ops, parens, and arithmetic; raw selectors,
rate()/*_over_time()over them, and
without (...)are not.Panels that stay on ES|QL carry a note naming which half declined, distinguishing an
unverifiable probe (auth error, transport failure) from a verified absence, so the operator
knows whether to upgrade, fix access, or reshape the query. Panels that go native record
minimum_kibana_version: 9.6.0, gated on the emitted query so a fallback never raises thefloor.
or/and/unlesswith a matcher stay blocked (elastic/elasticsearch#158181).Deviation from the issue's acceptance table
The issue lists panel 4 —
node_hwmon_temp_celsius * on(chip) group_left(chip_name) node_hwmon_chip_names— as "expected: PROMQL index=…". Against a live 9.6.0-SNAPSHOT that query returns:
Both operands are raw selectors. Emitting it natively would convert a rendering panel into
a hard error, so it stays on ES|QL and explains why in its notes. Panels 1–3 behave as the
issue specifies.
Related code paths checked
can_use_native_promqland every other gate it runs (_PROMQL_UNSUPPORTED_RE,_PROMQL_HISTOGRAM_QUANTILE_RE,_PROMQL_EMPTY_METRICLESS_SELECTOR_RE,_promql_has_unsupported_comparison,_promql_has_known_server_bug)._PROMQL_UNSUPPORTED_RE— onlycan_use_native_promql, so removing thefour tokens has no hidden blast radius.
_translate_panel_native_promql,_translate_multi_target_native_promql)._generate_esql_for_alertcallscan_use_native_promqlwithout aruntime-feature profile, so matched-expression alerts keep ES|QL. Pinned by a test, since
the shared gate now has a path that can return True.
_dashboard_minimum_kibana_versionand its existing 9.5 floors._promql_has_unmatchable_distinct_metric_binop/_promql_has_unmatchable_vector_matchfrom Grafana: element-wise distinct-metric PromQL arithmetic via native PROMQL silently returns zero rows (panel reported clean, renders empty) #376, which handle implicit matching and are unaffected.
parity-rig/verifier/classifier.py, so a shape that ever slips past the gate isclassified as a translator bug rather than going unlabeled.
Side effects considered
input, unmodelled node types, and analysis errors all keep the fallback.
parenthesis, so
on # note\n(x)parses ason(x). Comments are stripped from the rawexpression, where their extent is exact. The gates that scan
_clean_promql_for_nativeoutput deliberately keep scanning it with comments intact: flattening newlines destroys a
comment's extent, and stripping there would swallow the operand after it and hide
or/and/unless/ comparisons. Quote tracking covers",', and backtick raw strings soa
#inside a label value is data, not a comment.test_detect_target_runtime_features_uses_capability_namesnow counts label-matcher probes by query instead of total POSTs, so an unrelated feature
probe cannot look like a regression.
Empirical verification
Control run on unmodified base
d80e8ecvs this branch, same input, same target, sameseeded data:
PROMQLPROMQL(byte-identical)on()aligned ratioTS … SUM(CASE(...))PROMQL … / on(instance) …ignoring()device, labels not retainedPROMQL … / ignoring(device) …; warnings gone, statusmigratedon() + group_leftminimum_kibana_version9.5.09.6.0Direct
_queryagainst 9.6.0-SNAPSHOT: panel 2 → HTTP 200, 183 rows, columnsvalue, step, instance; panel 3 → HTTP 200, 61 rows; panel 4 → HTTP 400 as quoted above.Negative case against a real pre-9.6 cluster (Elasticsearch 9.5.0 container): the probe
returns HTTP 400
VectorBinaryArithmetic queries with group modifiers are not supported at this time→ statesupported: False, confidence: verified. The profile printsunsupported, all three matcher panels stay ES|QL, and the emitted queries arebyte-identical to what base code emits against that same target. On a pre-9.6 target
this change is a no-op.
Tests
tests/test_grafana_issue_440_vector_matching.py: 60 tests / 55 subtests covering theAST shape model (concrete and indeterminate operands), the eligibility gate, the capability
probe's five status paths, comment and quote handling, end-to-end panel emission on capable
and incapable targets, note wording, the version floor, and alert routing.
make test: 6432 passed, 61 skipped, 586 subtests passed.make lint: clean.make typecheck(mypy): clean.Kibana visual verification
Performed by driving the already signed-in Chrome over CDP. The Chrome DevTools MCP could
not attach — the profile's
DevToolsActivePortheld a stale browser GUID — so the same realbrowser and session were used through Playwright's
connect_over_cdp, which yields the sameevidence (screenshots, panel text, console, network).
Dashboard
obs-migrate-promql-on-ignoring-evalin view mode, cache-bypassing reload, atLast 1 hourand re-checked atLast 3 hours:No results found,Migration Required, or anyerror text.
the driver's own injected reload script, not the application.
instance_*series; panel 3 renders a single series ≈1.34; panel 4'sES|QL fallback still renders its nine
chip_N / chip_name_Mseries unchanged.x/y/breakdown_byaccessors match the columns its query actuallyreturns — the mismatch that produces "invalid column" render failures.
and branch produce the same values, which is the expected result: native evaluation did not
shift numbers, it removed the approximation and its semantic-loss warnings.
Re-run after every review fix; the emitted
native/andir/artifacts are byte-identicalto the verified build.
Pre-existing issues found, not fixed here
_clean_promql_for_nativeflattens newlines, so a PromQL comment swallows the rest of theexpression in the emitted native query. Base already emits native
PROMQLforsum(a) # note\n+ sum(b), producing a silently truncated query. Out of scope; filedseparately.
grafana-validate-uploadedhas no--insecureor--ca-certflag and ignoresOBS_MIGRATE_INSECURE, so it cannot validate a self-signed local stack — it fails withCERTIFICATE_VERIFY_FAILED. Worked around by replaying the uploaded saved object'squeries directly.