Skip to content

Support for CEL - #267

Open
mrueg wants to merge 1 commit into
prometheus-community:masterfrom
mrueg:add-cel
Open

Support for CEL#267
mrueg wants to merge 1 commit into
prometheus-community:masterfrom
mrueg:add-cel

Conversation

@mrueg

@mrueg mrueg commented Oct 30, 2023

Copy link
Copy Markdown
Member

This adds support for queries using the Common Expression Language (CEL), as an alternative to JSONPath.

See: #263

Config format

  • Make decision on config format

Resolved in favour of selecting an engine for the query, rather than adding a parallel set of cel/labels_cel fields:

    - name: example_cel_global_value
      engine: cel # default is jsonpath
      path: '.counter'
      help: Example of a top-level global value scrape in the json
      labels:
        environment: '"beta"'              # a static label is a CEL string literal
        location: '"planet-" + .location'  # a dynamic label is any expression

The engine is chosen per metric and applies to all of its expressions — path, labels, values and epochTimestamp — so both engines can be mixed freely within a module. Unknown engines are rejected when the config is loaded.

  • Add support for labels, values and epochTimestamp
  • Add documentation

How expressions are evaluated

The members of the scraped document are bound as variables, so .counter and counter both select a top-level member. The document itself is additionally bound as root, which is what makes documents that are not JSON objects reachable at all — a top-level list is scraped with path: 'root'. A member actually named root shadows that binding.

Expressions are parsed once and their programs cached, instead of building a CEL environment for every value, label and scrape. They are compiled when the metric list is built, so a broken expression is reported by --config.check rather than by every scrape. Expressions are deliberately not type-checked, since the shape of the scraped document is only known at scrape time; a member the document does not contain is reported when the expression is evaluated.

Values are rendered to match the JSONPath engine: a JSON null becomes NaN, and whole numbers are formatted without an exponent. allow_missing_key covers CEL as well, where it skips the metric for an unknown member, an unknown key or an index out of bounds, while any other evaluation error (e.g. a type mismatch) is still reported.

Also in this PR: epochTimestamp never took effect

yaml.v2 derives the key from the lowercased field name, so it looked for epochtimestamp and silently ignored the epochTimestamp spelling that examples/config.yml and the docs use. The field has an explicit tag now, and a custom UnmarshalYAML keeps accepting the all-lowercase spelling so existing configs do not regress.

That alone was not enough to make the feature work with JSONPath: a JSON number decodes to a float64, which renders as 1.657568506e+09 once it grows large enough, and SanitizeIntValue could not parse that. It now accepts float-formatted integers, so timestamps work on both engines.

The timestamped examples were object scrapes reading a top-level timestamp, which cannot work — the timestamp is looked up in the same document as the value. They are value scrapes now, and examples/data.json uses milliseconds, which is what time.UnixMilli expects. The README states the unit.

Tests

  • exporter/cel_test.go: expression evaluation (member access, root, macros, nulls, large integers, top-level arrays and scalars, JSON output), allow_missing_key behaviour, program caching, and error cases.
  • test/config/good-cel.yml: a CEL translation of good.yml, asserted against the same golden response, so both engines are shown to produce identical output.
  • test/config/allow_missing_key.yml: CEL modules added to the existing table.
  • exporter/util_test.go: SanitizeIntValue coverage, including the exponent notation above.

Dependencies

Uses cel.dev/cel-go v0.32.0. Note that cel-go renamed its module path from github.com/google/cel-go to cel.dev/cel-go in that release.

@mrueg

mrueg commented Oct 31, 2023

Copy link
Copy Markdown
Member Author

@SuperQ @rustycl0ck any thoughts on this? Would love to get some early feedback to see if this is something you'd be interested in adding.

@sbor23

sbor23 commented Nov 7, 2023

Copy link
Copy Markdown

Thanks a lot for this MR, just tested this branch and it seems to work fine. We used:

    - name: example_cel_global_value
      cel: '.state == "printing" ? 1 : 0'
      help: Example of a top-level global value scrape in the json
      labels:
        environment: beta # static label

This should potentially solve a lot of related issues that need a bit more advance logic, such as:
#151

Please add a bit of documentation, then it would be perfect 🚀

@mrueg

mrueg commented Nov 7, 2023

Copy link
Copy Markdown
Member Author

@sbor23 thanks for testing it! I still need to figure out a couple of things here, I have updated the original messages

@mrueg
mrueg force-pushed the add-cel branch 2 times, most recently from 75a0c7e to 72ab3d8 Compare November 16, 2023 14:07
@mrueg

mrueg commented Nov 16, 2023

Copy link
Copy Markdown
Member Author

I went ahead with the engine field solution

    - name: example_cel_global_value
      engine: cel
      path: '.counter'
      help: Example of a top-level global value scrape in the json using cel
      valuetype: 'gauge'
      labels:
        environment: "\"beta\"" # static label. Quotes need to be escaped for CEL
        location: "\"planet-\"+.location" # dynamic label. Quotes need to be escaped for CEL

@sbor23

sbor23 commented Jan 8, 2026

Copy link
Copy Markdown

@mrueg any reason this still has WIP in the title? Any chance it can be reviewed and merged?

Metrics can select 'engine: cel' to be scraped with CEL (Common
Expression Language) instead of JSONPath. The engine is chosen per
metric and applies to its path, labels, values and epoch timestamp,
so both engines can be mixed within a module.

The members of the scraped document are bound as variables, and the
document itself is bound as 'root', which is what makes documents that
are not JSON objects, such as a top-level list, reachable. Expressions
are parsed once and their programs cached, and they are compiled when
the metric list is built, so '--config.check' reports a broken
expression instead of every scrape doing so. Unknown engines are
rejected when the config is loaded.

Values are rendered to match the JSONPath engine: a null becomes NaN,
and whole numbers are formatted without an exponent. 'allow_missing_key'
covers CEL as well, where it skips the metric for an unknown member, an
unknown key or an index out of bounds.

Also make 'epochTimestamp' take effect: yaml.v2 keyed the field as
'epochtimestamp', so the documented spelling was silently ignored. Both
spellings are accepted now, and SanitizeIntValue parses the exponent
notation the JSONPath engine renders large numbers in, which is what
kept timestamps from working there.

The CEL value to JSON conversion is adapted from
https://github.com/google/cel-go/blob/cfbf821f1b458533051306305a39b743db7c4bdb/codelab/codelab.go#L274
(Apache-2.0 Licensed)

Signed-off-by: Manuel Rüger <manuel@rueg.eu>
@mrueg mrueg changed the title WIP: Initial support for CEL Support for CEL Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants