diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index 9fca8e6b..3e285093 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -238,10 +238,11 @@ def _parse_labels(text): def _parse_sample(text): + seperator = " # " # Detect the labels in the text label_start = text.find("{") - if label_start == -1: - # We don't have labels + if label_start == -1 or seperator in text[:label_start]: + # We don't have labels, but there could be an exemplar. name_end = text.index(" ") name = text[:name_end] # Parse the remaining text after the name @@ -250,8 +251,7 @@ def _parse_sample(text): return Sample(name, {}, value, timestamp, exemplar) # The name is before the labels name = text[:label_start] - seperator = " # " - if text.count(seperator) == 0: + if seperator not in text: # Line doesn't contain an exemplar # We can use `rindex` to find `label_end` label_end = text.rindex("}") @@ -261,7 +261,7 @@ def _parse_sample(text): # Line potentially contains an exemplar # Fallback to parsing labels with a state machine labels, labels_len = _parse_labels_with_state_machine(text[label_start + 1:]) - label_end = labels_len + len(name) + label_end = labels_len + len(name) # Parsing labels succeeded, continue parsing the remaining text remaining_text = text[label_end + 2:] value, timestamp, exemplar = _parse_remaining_text(remaining_text) @@ -564,9 +564,9 @@ def build_metric(name, documentation, typ, unit, samples): '_gsum'] and sample.value < 0: raise ValueError("Counter-like samples cannot be negative: " + line) if sample.exemplar and not ( - typ in ['histogram', 'gaugehistogram'] - and sample.name.endswith('_bucket')): - raise ValueError("Invalid line only histogram/gaugehistogram buckets can have exemplars: " + line) + (typ in ['histogram', 'gaugehistogram'] and sample.name.endswith('_bucket')) + or (typ in ['counter'] and sample.name.endswith('_total'))): + raise ValueError("Invalid line only histogram/gaugehistogram buckets and counters can have exemplars: " + line) if name is not None: yield build_metric(name, documentation, typ, unit, samples) diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py index 815f749f..81873388 100644 --- a/tests/openmetrics/test_parser.py +++ b/tests/openmetrics/test_parser.py @@ -148,6 +148,16 @@ def test_gaugehistogram_exemplars(self): hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, Timestamp(123, 0), Exemplar({"a": "d"}, 4, Timestamp(123, 0))) self.assertEqual([hfm], list(families)) + def test_counter_exemplars(self): + families = text_string_to_metric_families("""# TYPE a counter +# HELP a help +a_total 0 123 # {a="b"} 0.5 +# EOF +""") + cfm = CounterMetricFamily("a", "help") + cfm.add_sample("a_total", {}, 0.0, Timestamp(123, 0), Exemplar({"a": "b"}, 0.5)) + self.assertEqual([cfm], list(families)) + def test_simple_info(self): families = text_string_to_metric_families("""# TYPE a info # HELP a help @@ -616,9 +626,11 @@ def test_invalid_input(self): ('# TYPE a histogram\na_sum 1 # {a="b"} 0.5\n# EOF\n'), ('# TYPE a gaugehistogram\na_sum 1 # {a="b"} 0.5\n# EOF\n'), ('# TYPE a_bucket gauge\na_bucket 1 # {a="b"} 0.5\n# EOF\n'), + ('# TYPE a counter\na_created 1 # {a="b"} 0.5\n# EOF\n'), # Exemplars on unallowed metric types. - ('# TYPE a counter\na_total 1 # {a="b"} 1\n# EOF\n'), ('# TYPE a gauge\na 1 # {a="b"} 1\n# EOF\n'), + ('# TYPE a info\na_info 1 # {a="b"} 1\n# EOF\n'), + ('# TYPE a stateset\na{a="b"} 1 # {c="d"} 1\n# EOF\n'), # Bad stateset/info values. ('# TYPE a stateset\na 2\n# EOF\n'), ('# TYPE a info\na 2\n# EOF\n'),