Repository: prometheus/client_python
Pull request #538 (2020-04-22 12:11:01)

Update canonical floats per openmetrics latest.
We now only require a specific set of very common le/quantile
labels to be in a canonical form.


brian-brazil: @SuperQ 
Test failure is unrelated.

Patch:
diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py
index 634066f..191e745 100644
--- a/prometheus_client/exposition.py
+++ b/prometheus_client/exposition.py
@@ -362,7 +362,10 @@ def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler)
 
 
 def _escape_grouping_key(k, v):
-    if '/' in v:
+    if v == "" :
+        # Per https://github.com/prometheus/pushgateway/pull/346.
+        return k + "@base64", "="
+    elif '/' in v:
         # Added in Pushgateway 0.9.0.
         return k + "@base64", base64.urlsafe_b64encode(v.encode("utf-8")).decode("utf-8")
     else:
diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py
index b34ca81..95770eb 100644
--- a/prometheus_client/metrics.py
+++ b/prometheus_client/metrics.py
@@ -27,12 +27,12 @@ def _build_full_name(metric_type, name, namespace, subsystem, unit):
     if subsystem:
         full_name += subsystem + '_'
     full_name += name
+    if metric_type == 'counter' and full_name.endswith('_total'):
+        full_name = full_name[:-6]  # Munge to OpenMetrics.
     if unit and not full_name.endswith("_" + unit):
         full_name += "_" + unit
     if unit and metric_type in ('info', 'stateset'):
         raise ValueError('Metric name is of a type that cannot have a unit: ' + full_name)
-    if metric_type == 'counter' and full_name.endswith('_total'):
-        full_name = full_name[:-6]  # Munge to OpenMetrics.
     return full_name
 
 
diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py
index a167ae6..6235eda 100644
--- a/prometheus_client/openmetrics/parser.py
+++ b/prometheus_client/openmetrics/parser.py
@@ -15,7 +15,6 @@ except ImportError:
     # Python 3
     import io as StringIO
 
-
 def text_string_to_metric_families(text):
     """Parse Openmetrics text format from a unicode string.
 
@@ -25,6 +24,16 @@ def text_string_to_metric_families(text):
         yield metric_family
 
 
+_CANONICAL_NUMBERS = set([i / 1000.0 for i in range(10000)] + [10.0**i for i in range(-10, 11)] + [float("inf")])
+
+
+def _isUncanonicalNumber(s):
+    f = float(s)
+    if f not in _CANONICAL_NUMBERS:
+        return False  # Only the canonical numbers are required to be canonical.
+    return s != floatToGoString(f)
+
+
 ESCAPE_SEQUENCES = {
     '\\\\': '\\',
     '\\n': '\n',
@@ -544,11 +553,11 @@ def text_fd_to_metric_families(fd):
                 raise ValueError("Stateset missing label: " + line)
             if (typ in ['histogram', 'gaugehistogram'] and name + '_bucket' == sample.name
                     and (sample.labels.get('le', "NaN") == "NaN"
-                         or sample.labels['le'] != floatToGoString(sample.labels['le']))):
+                         or _isUncanonicalNumber(sample.labels['le']))):
                 raise ValueError("Invalid le label: " + line)
             if (typ == 'summary' and name == sample.name
                     and (not (0 <= float(sample.labels.get('quantile', -1)) <= 1)
-                         or sample.labels['quantile'] != floatToGoString(sample.labels['quantile']))):
+                          or _isUncanonicalNumber(sample.labels['quantile']))):
                 raise ValueError("Invalid quantile label: " + line)
 
             g = tuple(sorted(_group_for_sample(sample, name, typ).items()))


Test patch:
diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py
index abdf181..81525d3 100644
--- a/tests/openmetrics/test_parser.py
+++ b/tests/openmetrics/test_parser.py
@@ -122,6 +122,20 @@ a_sum 2
         self.assertEqual([HistogramMetricFamily("a", "help", sum_value=2, buckets=[("1.0", 0.0), ("+Inf", 3.0)])],
                          list(families))
 
+    def test_histogram_noncanonical(self):
+        families = text_string_to_metric_families("""# TYPE a histogram
+# HELP a help
+a_bucket{le="0.00000000001"} 0
+a_bucket{le="1.1e-4"} 0
+a_bucket{le="1.1e-3"} 0
+a_bucket{le="100000000000.0"} 0
+a_bucket{le="+Inf"} 3
+a_count 3
+a_sum 2
+# EOF
+""")
+        list(families)
+
     def test_negative_bucket_histogram(self):
         families = text_string_to_metric_families("""# TYPE a histogram
 # HELP a help
@@ -731,9 +745,13 @@ foo_created 1.520430000123e+09
             ('# TYPE a gaugehistogram\na_gsum 1\n# EOF\n'),
             ('# TYPE a histogram\na_count 1\na_bucket{le="+Inf"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="+Inf"} 0\na_count 1\n# EOF\n'),
+            ('# TYPE a histogram\na_bucket{le="0"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="1"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
+            ('# TYPE a histogram\na_bucket{le="0.0000000001"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
+            ('# TYPE a histogram\na_bucket{le="1.1e-2"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="1e-04"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="1e+05"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
+            ('# TYPE a histogram\na_bucket{le="10000000000"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="+INF"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="2"} 0\na_bucket{le="1"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
             ('# TYPE a histogram\na_bucket{le="1"} 1\na_bucket{le="2"} 1\na_bucket{le="+Inf"} 0\n# EOF\n'),
@@ -754,16 +772,6 @@ foo_created 1.520430000123e+09
             with self.assertRaises(ValueError, msg=case):
                 list(text_string_to_metric_families(case))
 
-    @unittest.skipIf(sys.version_info < (2, 7), "float repr changed from 2.6 to 2.7")
-    def test_invalid_float_input(self):
-        for case in [
-            # Bad histograms.
-            ('# TYPE a histogram\na_bucket{le="9.999999999999999e+22"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
-            ('# TYPE a histogram\na_bucket{le="1.5555555555555201e+06"} 0\na_bucket{le="+Inf"} 0\n# EOF\n'),
-        ]:
-            with self.assertRaises(ValueError):
-                list(text_string_to_metric_families(case))
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_core.py b/tests/test_core.py
index f0b856a..b14e340 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -527,6 +527,11 @@ class TestMetricWrapper(unittest.TestCase):
         self.assertRaises(ValueError, Info, 'foo', 'help', unit="x")
         self.assertRaises(ValueError, Enum, 'foo', 'help', unit="x")
 
+    def test_name_cleanup_before_unit_append(self):
+        self.assertEqual(self.counter._name, 'c')
+        self.c = Counter('c_total', 'help', unit="total", labelnames=['l'], registry=self.registry)
+        self.assertEqual(self.c._name, 'c_total')
+
 
 class TestMetricFamilies(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_exposition.py b/tests/test_exposition.py
index 47c200f..25360b3 100644
--- a/tests/test_exposition.py
+++ b/tests/test_exposition.py
@@ -59,6 +59,17 @@ cc_total 1.0
 # HELP cc_created A counter
 # TYPE cc_created gauge
 cc_created 123.456
+""", generate_latest(self.registry))
+
+    def test_counter_name_unit_append(self):
+        c = Counter('requests', 'Request counter', unit="total", registry=self.registry)
+        c.inc()
+        self.assertEqual(b"""# HELP requests_total_total Request counter
+# TYPE requests_total_total counter
+requests_total_total 1.0
+# HELP requests_total_created Request counter
+# TYPE requests_total_created gauge
+requests_total_created 123.456
 """, generate_latest(self.registry))
 
     def test_counter_total(self):
@@ -249,6 +260,13 @@ class TestPushGateway(unittest.TestCase):
         self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST)
         self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n')
 
+    def test_push_with_groupingkey_empty_label(self):
+        push_to_gateway(self.address, "my_job", self.registry, {'a': ''})
+        self.assertEqual(self.requests[0][0].command, 'PUT')
+        self.assertEqual(self.requests[0][0].path, '/metrics/job/my_job/a@base64/=')
+        self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST)
+        self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n')
+
     def test_push_with_complex_groupingkey(self):
         push_to_gateway(self.address, "my_job", self.registry, {'a': 9, 'b': 'a/ z'})
         self.assertEqual(self.requests[0][0].command, 'PUT')