From fb926b377667d8fb14448b199b151faad2917860 Mon Sep 17 00:00:00 2001 From: JackyHsu-X Date: Mon, 26 Oct 2026 13:38:11 +0300 Subject: [PATCH] Escape exemplar label names using the negotiated scheme Signed-off-by: JackyHsu-X --- prometheus_client/openmetrics/exposition.py | 8 ++-- tests/openmetrics/test_exposition.py | 42 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 5d276e2f..8cf6cc4b 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -32,13 +32,13 @@ def _is_valid_exemplar_metric(metric, sample): return False -def _compose_exemplar_string(metric, sample, exemplar): +def _compose_exemplar_string(metric, sample, exemplar, escaping=UNDERSCORES): """Constructs an exemplar string.""" if not _is_valid_exemplar_metric(metric, sample): raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter") labels = '{{{0}}}'.format(','.join( ['{}="{}"'.format( - k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) + escape_label_name(k, escaping), v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) for k, v in sorted(exemplar.labels.items())])) if exemplar.timestamp is not None: exemplarstr = ' # {} {} {}'.format( @@ -84,7 +84,7 @@ def generate_latest(registry, escaping=UNDERSCORES, version="1.0.0"): if labelstr: labelstr = "{" + labelstr + "}" if s.exemplar: - exemplarstr = _compose_exemplar_string(metric, s, s.exemplar) + exemplarstr = _compose_exemplar_string(metric, s, s.exemplar, escaping) else: exemplarstr = '' timestamp = '' @@ -139,7 +139,7 @@ def generate_latest(registry, escaping=UNDERSCORES, version="1.0.0"): if s.native_histogram.nh_exemplars: for nh_ex in s.native_histogram.nh_exemplars: - nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex) + nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex, escaping) exemplarstr += nh_exemplarstr value = '' diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 3ba4031a..cb574e69 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -17,6 +17,48 @@ ) +@pytest.mark.parametrize(('kind', 'version'), [ + ('counter', '1.0.0'), ('counter', '2.0.0'), + ('histogram', '1.0.0'), ('histogram', '2.0.0'), + ('native_histogram', '2.0.0'), +]) +@pytest.mark.parametrize(('escaping', 'label_name', 'expected_name'), [ + (UNDERSCORES, 'trace.id', 'trace_id'), + (DOTS, 'trace.id', 'trace_dot_id'), + (VALUES, 'trace.id', 'U__trace_2e_id'), + (ALLOWUTF8, 'trace.id', '"trace.id"'), + (ALLOWUTF8, '花火', '"花火"'), + (DOTS, 'trace_id', 'trace__id'), + (VALUES, '花火', 'U___82b1__706b_'), + (ALLOWUTF8, 'trace"id\n\\', '"trace\\"id\\n\\\\"'), + (UNDERSCORES, 'trace_id', 'trace_id'), +]) +def test_exemplar_label_name_escaping(kind, version, escaping, label_name, expected_name): + registry = CollectorRegistry() + labels = {label_name: 'abc'} + if kind == 'counter': + Counter('requests', 'Requests', registry=registry).inc(exemplar=labels) + elif kind == 'histogram': + Histogram('latency', 'Latency', registry=registry).observe(0.5, exemplar=labels) + else: + metric = HistogramMetricFamily('latency', 'Latency') + metric.add_sample('latency', {}, None, native_histogram=NativeHistogram( + 1, 0.5, 0, 0.001, 0, + (BucketSpan(0, 1),), None, (1,), None, + [Exemplar(labels, 0.5)], + )) + + class Collector: + def collect(self): + return [metric] + + registry.register(Collector()) + + output = generate_latest(registry, escaping=escaping, version=version).decode('utf-8') + exemplar_value = '1.0' if kind == 'counter' else '0.5' + assert '# {{{}="abc"}} {}'.format(expected_name, exemplar_value) in output + + class TestGenerateText(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry()