Skip to content

Fix operator precedence allowing exemplars on any metric type - #1188

Merged
csmarchbanks merged 1 commit into
prometheus:masterfrom
sean-kim05:fix/exemplar-metric-type-precedence
Jul 24, 2026
Merged

Fix operator precedence allowing exemplars on any metric type#1188
csmarchbanks merged 1 commit into
prometheus:masterfrom
sean-kim05:fix/exemplar-metric-type-precedence

Conversation

@sean-kim05

Copy link
Copy Markdown
Contributor

_is_valid_exemplar_metric() in openmetrics/exposition.py guards histograms with:

if metric.type in ('histogram') and sample.name.endswith('_bucket') or sample.name == metric.name:

Since and binds tighter than or, this is evaluated as:

(metric.type in ('histogram') and sample.name.endswith('_bucket')) or (sample.name == metric.name)

so the trailing sample.name == metric.name clause runs for every metric type, not just histograms. As a result the OpenMetrics writer attaches exemplars to gauge/info/stateset/summary/untyped metrics whenever a sample name equals the metric name — output that this library's own OpenMetrics parser then rejects (only histogram/gaugehistogram buckets and counters can have exemplars).

Minimal repro:

from prometheus_client.core import Metric
from prometheus_client.registry import CollectorRegistry
from prometheus_client.openmetrics.exposition import generate_latest
from prometheus_client.samples import Exemplar

reg = CollectorRegistry()

class C:
    def collect(self):
        m = Metric("gg", "a gauge", "gauge")
        m.add_sample("gg", {}, 1.0, None, Exemplar({'a': 'b'}, 0.5))
        yield m

reg.register(C())
print(generate_latest(reg).decode())
# emits `gg 1.0 # {a="b"} 0.5` instead of raising ValueError

The fix groups the histogram condition so the same-name clause — which exists to allow native-histogram exemplars, whose sample name equals the metric name — only applies to histograms. I also changed the two metric.type in ('...') checks to ==; with a single string argument they were doing substring matching rather than the intended equality.

Added test_gauge_exemplar next to the existing untyped/non-bucket exemplar tests. It fails before the change (ValueError not raised) and passes after. Full test suite is green and flake8/isort/mypy pass.

@csmarchbanks

_is_valid_exemplar_metric() guarded histograms with

    if metric.type in ('histogram') and sample.name.endswith('_bucket') or sample.name == metric.name:

Because 'and' binds tighter than 'or', this parsed as

    (metric.type in ('histogram') and sample.name.endswith('_bucket')) or (sample.name == metric.name)

so the trailing 'sample.name == metric.name' clause fired for every
metric type. As a result the OpenMetrics writer emitted exemplars on
gauges, info, stateset, summary and untyped metrics whenever a sample
name equalled the metric name -- output that the library's own
OpenMetrics parser rejects (only histogram/gaugehistogram buckets,
counter _total, and native histograms may carry exemplars).

Group the histogram condition correctly so the same-name clause (which
exists to allow native-histogram exemplars) only applies to histograms.
While here, replace the 'metric.type in (...)' single-string membership
checks with '==' -- they were doing substring matching, not the
intended equality.

Add test_gauge_exemplar, which asserts a gauge sample carrying an
exemplar raises ValueError, matching the existing untyped/non-bucket
exemplar tests.

Signed-off-by: Sean Kim <skim8705@gmail.com>

@csmarchbanks csmarchbanks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@csmarchbanks
csmarchbanks merged commit fb072c2 into prometheus:master Jul 24, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants