Skip to content

Promql label functions - #155688

Draft
eyalkoren wants to merge 2 commits into
elastic:mainfrom
eyalkoren:promql-label-functions
Draft

Promql label functions#155688
eyalkoren wants to merge 2 commits into
elastic:mainfrom
eyalkoren:promql-label-functions

Conversation

@eyalkoren

Copy link
Copy Markdown
Contributor

Closes #136256

Adds the PromQL metadata-manipulation functions label_replace and label_join, lowering them into the ES|QL time-series pipeline. This is intentionally a draft: it implements the functions by manipulating the series identity (_timeseries) blob, which leaves two without-related cases diverging from Prometheus. v1 will avoid _timeseries manipulation entirely, so this PR is kept as the reference for the full, identity-manipulating approach if we choose to pursue it later.

The core challenge: _timeseries identity

Label functions change a series' identity (its label set). In ES|QL a series identity is carried as an opaque _timeseries blob, and adding/renaming/deleting a label on a vector that must retain its identity means writing into that blob (PromqlSetLabel). Two properties of the model make this hard:

  • Exclusion is applied where the blob is built. without(x) is lowered to a skipFieldNames filter on the block loader that assembles the _timeseries blob. That blob is built purely from the index's stored dimension fields (mappingLookup.dimensionFieldMappers()), so a label derived by label_replace/label_join — which has no dimension mapper — is never in the candidate set, and excluding it is a no-op. label_replace then writes the derived label into the already-built blob later in the compute pipeline, after the only exclusion step has run.
  • The blob is opaque. Once a derived label is written into _timeseries, downstream stages group by the blob as-is; there is no cheap way to drop a single derived label back out of it.

By contrast, by(...) and bare aggregation never touch the blob: the derived label is materialized as a concrete column used directly as a grouping key (by), or the identity is dropped and series collapse (bare). Those paths behave correctly; the paths that require blob manipulation are where behavior diverges.

Implementation

Analysis. label_replace and label_join are PromQL built-ins (PromqlBuiltinFunctionDefinitions / PromqlFunctionRegistry, FunctionType.METADATA_MANIPULATION). Rather than the generic PromQL function builder, ResolvePromqlFunctions resolves them to a dedicated logical node, MetadataManipulationFunction. Because the destination label is brand-new (absent from the index mapping), the node mints a stable destination attribute once — the Grok/Dissect generated-attribute pattern — and the analyzer adds it to the PromQL resolution scope, so an enclosing by(dst) / KEEP dst binds to the derived label exactly like a stored one.

Translation (TranslatePromqlToEsqlPlan#translateMetadataManipulation) lowers the node to ES|QL primitives:

  1. It selects an identity representation from the node's position: whole-identity contexts (bare / without / group-all) keep the opaque _timeseries blob; an enclosing by drops _timeseries and consumes the destination as a concrete column.
  2. It materializes a first-pass identity "seam" (an innermost aggregate over _tsid + time bucket) when the child is a raw vector, exposing the source-label columns the derivation reads.
  3. It derives the destination value once at the seam:
    • label_replacePromqlRegexExtract, an internal scalar using RE2/J for byte-for-byte Prometheus parity (RE2 syntax, (?P<name>), no backreferences) with Go Expand replacement ($1, ${name});
    • label_join → a CONCAT-based join of the source labels with the separator.
      The derived value encodes Prometheus's three outcomes: set (non-empty), delete (matched-but-empty → ""), no-op (no match → null, leaving any pre-existing destination untouched).
  4. It writes the derived value: always as a concrete, groupable destination column (coalescing over any pre-existing destination), and — only in whole-identity contexts — into the _timeseries blob via PromqlSetLabel. That scalar rewrites the blob's single passthrough namespace (labels for Prometheus data, attributes for OTel, with __name__ special-cased) and re-emits keys in canonical sorted order, so two series whose label sets become equal serialize to byte-identical blobs and collapse on the same key.
  5. It inserts a PromqlCollisionCheck at the seam (before any outer aggregate), keyed on the exposed identity plus the time bucket. Two distinct series mapped onto the same identity within a bucket fail the query with PromQL's "vector cannot contain metrics with the same labelset" error instead of silently merging. The check is a coordinator-only artifact (PromqlCollisionCheckExec / PromqlCollisionCheckOperator) and is never serialized.

Compatibility. The two internal scalars (PromqlRegexExtract, PromqlSetLabel) are versioned writeables gated by the promql_label_functions transport version; RE2/J is added as a new dependency (licenses included).

Not yet supported

Two deviations from Prometheus, verified by execution and captured as intentionally-failing tests in x-pack/plugin/esql/src/internalClusterTest/.../action/PromqlLabelFunctionsUnsupportedIT.java (left red on purpose — they document the gaps):

  • without on a derived label — e.g. sum without (region) (label_replace(m, "region", "$1", "pod", "(.+)")). Prometheus drops region; here it survives in the identity, because the exclusion is applied as a skipFieldNames filter when the block loader builds the blob from stored dimensions, where the derived label does not exist.
  • without () over a relabel — Prometheus groups by every label, so all distinct series survive; here the _timeseries identity is dropped and distinct series collapse into one.

What is handled: the relabel collision guard (two series relabeled onto the same labelset within a bucket → PromQL "vector cannot contain metrics with the same labelset" error). Those cases were checked and behave as Prometheus does, including in-place delete/overwrite and nested relabels.

Why it's a draft, and the v1 direction

All the divergences stem from _timeseries manipulation, so v1 will not support without. The open question for v1 is whether label_replace/label_join can be supported for the non-without grouping modes — bare (NONE) and by-labels — without any _timeseries manipulation, since those already surface the derived label as a concrete column rather than writing it into the blob. This PR remains open as the reference implementation for the full approach should we later decide to manipulate _timeseries.

@eyalkoren eyalkoren self-assigned this Aug 2, 2026
@eyalkoren eyalkoren added the :StorageEngine/PromQL PromQL support for Elastic label Aug 2, 2026
@elasticsearchmachine elasticsearchmachine added v9.6.0 external-contributor Pull request authored by a developer outside the Elasticsearch team labels Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor Pull request authored by a developer outside the Elasticsearch team :StorageEngine/PromQL PromQL support for Elastic v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add label-processing functions

2 participants