Repository: prometheus/client_python
Pull request #434 (2019-07-03 14:28:50)

Add support for exemplars on counters.
@SuperQ 


Patch:
diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py
index 9fca8e6..3e28509 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 text_fd_to_metric_families(fd):
                                            '_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)


Test patch:
diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py
index 815f749..8187338 100644
--- a/tests/openmetrics/test_parser.py
+++ b/tests/openmetrics/test_parser.py
@@ -148,6 +148,16 @@ a_bucket{le="+Inf"} 3 123 # {a="d"} 4 123
         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 @@ foo_created 1.520430000123e+09
             ('# 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'),