From db1420f200cb88ee3b9dc7a104001b95ea17ae11 Mon Sep 17 00:00:00 2001 From: spicysharkco <321699413+spicysharkco@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:34:03 +0000 Subject: [PATCH] Parse escaped quotes in OpenMetrics exemplar label values The exemplar scanner in _parse_remaining_text flipped its in-quotes state on every double quote, so a label value like x"y, which the writer emits as x\"y, made the whole line fail with "Invalid line". Ignore quotes preceded by an odd number of backslashes. Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com> --- prometheus_client/openmetrics/parser.py | 5 ++--- tests/openmetrics/test_parser.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index 0c5c9c41..adb3a388 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -154,10 +154,9 @@ def _parse_remaining_text(text): state = 'timestamp' text = split_text[1] - it = iter(text) in_quotes = False - for char in it: - if char == '"': + for i, char in enumerate(text): + if char == '"' and not _is_character_escaped(text, i): in_quotes = not in_quotes if in_quotes: continue diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py index 79a2158c..684d9a82 100644 --- a/tests/openmetrics/test_parser.py +++ b/tests/openmetrics/test_parser.py @@ -570,6 +570,17 @@ def test_label_escaping(self): metric_family.add_metric([unescaped_val, "baz"], 1) self.assertEqual([metric_family], list(families)) + def test_exemplar_label_escaping(self): + for escaped_val, unescaped_val in [('\\"', '"'), ('\\\\', '\\'), ('\\\\\\"', '\\"')]: + families = list(text_string_to_metric_families("""# TYPE a counter +# HELP a help +a_total 1 # {foo="%s"} 1 +# EOF +""" % escaped_val)) + metric_family = CounterMetricFamily("a", "help") + metric_family.add_sample("a_total", {}, 1.0, None, Exemplar({"foo": unescaped_val}, 1)) + self.assertEqual([metric_family], list(families)) + def test_help_escaping(self): for escaped_val, unescaped_val in [ ('foo', 'foo'),