From 1972af273150a16bde578de1bbab652d02daa2ae Mon Sep 17 00:00:00 2001 From: John Saurabh Date: Wed, 3 Jun 2026 22:03:38 -0700 Subject: [PATCH 1/3] fix: escape colons in metric names under UNDERSCORES mode In UNDERSCORES escaping mode, escape_metric_name was short-circuiting for names that matched the legacy Prometheus metric name regex, which allows colons. This caused # HELP and # TYPE lines to emit the raw name (e.g. sglang:token_usage) while sample lines, which use _is_legacy_labelname_rune, correctly replaced the colon with an underscore (e.g. sglang_token_usage). The mismatch violates the OpenMetrics standard and breaks strict parsers. Fix by requiring the name to also be colon-free before taking the no-op path, and switching the fallback _escape call to use _is_legacy_labelname_rune so colons are replaced consistently. Fixes #1177 Signed-off-by: John Saurabh --- prometheus_client/openmetrics/exposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 5e69e463..57e18fe6 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -181,9 +181,9 @@ def escape_metric_name(s: str, escaping: str = UNDERSCORES) -> str: return '"{}"'.format(_escape(s, escaping, _is_legacy_metric_rune)) return _escape(s, escaping, _is_legacy_metric_rune) elif escaping == UNDERSCORES: - if _is_valid_legacy_metric_name(s): + if _is_valid_legacy_metric_name(s) and ':' not in s: return s - return _escape(s, escaping, _is_legacy_metric_rune) + return _escape(s, escaping, _is_legacy_labelname_rune) elif escaping == DOTS: return _escape(s, escaping, _is_legacy_metric_rune) elif escaping == VALUES: From 45601c17957ae484a95647040705f290f1b839e6 Mon Sep 17 00:00:00 2001 From: John Saurabh Date: Wed, 3 Jun 2026 22:03:48 -0700 Subject: [PATCH 2/3] test: add colon escaping test and update stale expectations Add test_gauge_colon_in_name_escaped_underscores to verify that a gauge named sglang:token_usage produces consistent # HELP, # TYPE, and sample lines when escaping=underscores is in effect. Update two parametrized scenario expectations that documented the old buggy behaviour (colon preserved in UNDERSCORES mode): - "legacy valid metric name": no:escaping_required -> no_escaping_required - "metric name with dots and colon": http_status:sum -> http_status_sum Signed-off-by: John Saurabh --- tests/openmetrics/test_exposition.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index a3ed0d6e..f67a7899 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -55,6 +55,15 @@ def test_counter_utf8_escaped_underscores(self): utf8_cc_total 1.0 utf8_cc_created 123.456 # EOF +""" == generate_latest(self.registry, UNDERSCORES) + + def test_gauge_colon_in_name_escaped_underscores(self): + g = Gauge('sglang:token_usage', 'Total token usage.', registry=self.registry) + g.set(42.0) + assert b"""# HELP sglang_token_usage Total token usage. +# TYPE sglang_token_usage gauge +sglang_token_usage 42.0 +# EOF """ == generate_latest(self.registry, UNDERSCORES) def test_counter_total(self) -> None: @@ -482,7 +491,7 @@ def test_native_histogram_version_comparison(self) -> None: { "name": "legacy valid metric name", "input": "no:escaping_required", - "expectedUnderscores": "no:escaping_required", + "expectedUnderscores": "no_escaping_required", "expectedDots": "no:escaping__required", "expectedValue": "no:escaping_required", }, @@ -503,7 +512,7 @@ def test_native_histogram_version_comparison(self) -> None: { "name": "metric name with dots and colon", "input": "http.status:sum", - "expectedUnderscores": "http_status:sum", + "expectedUnderscores": "http_status_sum", "expectedDots": "http_dot_status:sum", "expectedValue": "U__http_2e_status:sum", }, From 74393527516bac1d60c3e377dcad736f22f5dfa0 Mon Sep 17 00:00:00 2001 From: John Saurabh Date: Wed, 3 Jun 2026 22:03:54 -0700 Subject: [PATCH 3/3] test: update stale colon escaping expectations in test_exposition.py Mirror the same expectation corrections made to tests/openmetrics/test_exposition.py: under UNDERSCORES mode, colons in metric names are now replaced with underscores to match sample line output. Signed-off-by: John Saurabh --- tests/test_exposition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index a3c97820..2fe31891 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -644,7 +644,7 @@ def test_prom_no_version(self): { "name": "legacy valid metric name", "input": "no:escaping_required", - "expectedUnderscores": "no:escaping_required", + "expectedUnderscores": "no_escaping_required", "expectedDots": "no:escaping__required", "expectedValue": "no:escaping_required", }, @@ -665,7 +665,7 @@ def test_prom_no_version(self): { "name": "metric name with dots and colon", "input": "http.status:sum", - "expectedUnderscores": "http_status:sum", + "expectedUnderscores": "http_status_sum", "expectedDots": "http_dot_status:sum", "expectedValue": "U__http_2e_status:sum", },