diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index a167ae6e..6235eda5 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -15,7 +15,6 @@ # 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 build_metric(name, documentation, typ, unit, samples): 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())) diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py index abdf1816..81525d34 100644 --- a/tests/openmetrics/test_parser.py +++ b/tests/openmetrics/test_parser.py @@ -122,6 +122,20 @@ def test_simple_histogram(self): 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 @@ def test_invalid_input(self): ('# 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 @@ def test_invalid_input(self): 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()