From a6d7e60303a72b5f796031bd22f15f70f3e2ebf7 Mon Sep 17 00:00:00 2001 From: Brian Brazil Date: Tue, 2 Oct 2018 14:50:26 +0100 Subject: [PATCH] Check ordering within groups. Signed-off-by: Brian Brazil --- prometheus_client/core.py | 5 ++++ prometheus_client/openmetrics/parser.py | 15 ++++++++--- tests/openmetrics/test_parser.py | 33 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/prometheus_client/core.py b/prometheus_client/core.py index ccb41348..16ef4f0e 100644 --- a/prometheus_client/core.py +++ b/prometheus_client/core.py @@ -49,6 +49,8 @@ class Timestamp(object): def __init__(self, sec, nsec): if nsec < 0 or nsec >= 1e9: raise ValueError("Invalid value for nanoseconds in Timestamp: {}".format(nsec)) + if sec < 0: + nsec = -nsec self.sec = int(sec) self.nsec = int(nsec) @@ -64,6 +66,9 @@ def __float__(self): def __eq__(self, other): return type(self) == type(other) and self.sec == other.sec and self.nsec == other.nsec + def __gt__(self, other): + return self.sec > other.sec or self.nsec > other.nsec + Exemplar = namedtuple('Exemplar', ['labels', 'value', 'timestamp']) Exemplar.__new__.__defaults__ = (None, ) diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index df082b43..4bfb7a4b 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -264,6 +264,7 @@ def text_fd_to_metric_families(fd): unit = None group = None seen_groups = set() + group_timestamp = None samples = [] allowed_names = [] eof = False @@ -285,10 +286,8 @@ def build_metric(name, documentation, typ, unit, samples): raise ValueError("Units not allowed for this metric type: " + name) metric = core.Metric(name, documentation, typ, unit) # TODO: check labelvalues are valid utf8 - # TODO: check samples are appropriately ordered # TODO: Check histogram bucket rules being followed - # TODO: Check for dupliate samples - # TODO: Check for decresing timestamps + # TODO: Check for duplicate samples metric.samples = samples return metric @@ -317,6 +316,7 @@ def build_metric(name, documentation, typ, unit, samples): documentation = None group = None seen_groups = set() + group_timestamp = None samples = [] allowed_names = [parts[2]] @@ -359,6 +359,7 @@ def build_metric(name, documentation, typ, unit, samples): typ = 'unknown' samples = [sample] group = None + group_timestamp = None seen_groups = set() allowed_names = [sample.name] else: @@ -375,8 +376,14 @@ def build_metric(name, documentation, typ, unit, samples): g = tuple(sorted(_group_for_sample(sample, name, typ).items())) if group is not None and g != group and g in seen_groups: - raise ValueError("Invalid metric group ordering: " + line) + raise ValueError("Invalid metric grouping: " + line) + if group is not None and g == group: + if (sample.timestamp is None) != (group_timestamp is None): + raise ValueError("Mix of timestamp presence within a group: " + line) + if group_timestamp is not None and group_timestamp > sample.timestamp and typ != 'info': + raise ValueError("Timestamps went backwards within a group: " + line) group = g + group_timestamp = sample.timestamp seen_groups.add(g) if typ == 'stateset' and sample.value not in [0, 1]: diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py index 01a8f87d..24eb5236 100644 --- a/tests/openmetrics/test_parser.py +++ b/tests/openmetrics/test_parser.py @@ -121,13 +121,13 @@ def test_histogram_exemplars(self): families = text_string_to_metric_families("""# TYPE a histogram # HELP a help a_bucket{le="1"} 0 # {a="b"} 0.5 -a_bucket{le="2"} 2 123 # {a="c"} 0.5 +a_bucket{le="2"} 2 # {a="c"} 0.5 a_bucket{le="+Inf"} 3 # {a="1234567890123456789012345678901234567890123456789012345678"} 4 123 # EOF """) hfm = HistogramMetricFamily("a", "help") hfm.add_sample("a_bucket", {"le": "1"}, 0.0, None, Exemplar({"a": "b"}, 0.5)) - hfm.add_sample("a_bucket", {"le": "2"}, 2.0, Timestamp(123, 0), Exemplar({"a": "c"}, 0.5)), + hfm.add_sample("a_bucket", {"le": "2"}, 2.0, None, Exemplar({"a": "c"}, 0.5)), hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, None, Exemplar({"a": "1234567890123456789012345678901234567890123456789012345678"}, 4, Timestamp(123, 0))) self.assertEqual([hfm], list(families)) @@ -145,15 +145,15 @@ def test_simple_gaugehistogram(self): def test_gaugehistogram_exemplars(self): families = text_string_to_metric_families("""# TYPE a gaugehistogram # HELP a help -a_bucket{le="1"} 0 # {a="b"} 0.5 +a_bucket{le="1"} 0 123 # {a="b"} 0.5 a_bucket{le="2"} 2 123 # {a="c"} 0.5 -a_bucket{le="+Inf"} 3 # {a="d"} 4 123 +a_bucket{le="+Inf"} 3 123 # {a="d"} 4 123 # EOF """) hfm = GaugeHistogramMetricFamily("a", "help") - hfm.add_sample("a_bucket", {"le": "1"}, 0.0, None, Exemplar({"a": "b"}, 0.5)) + hfm.add_sample("a_bucket", {"le": "1"}, 0.0, Timestamp(123, 0), Exemplar({"a": "b"}, 0.5)) hfm.add_sample("a_bucket", {"le": "2"}, 2.0, Timestamp(123, 0), Exemplar({"a": "c"}, 0.5)), - hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, None, Exemplar({"a": "d"}, 4, Timestamp(123, 0))) + 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_simple_info(self): @@ -164,6 +164,18 @@ def test_simple_info(self): """) self.assertEqual([InfoMetricFamily("a", "help", {'foo': 'bar'})], list(families)) + def test_info_timestamps(self): + families = text_string_to_metric_families("""# TYPE a info +# HELP a help +a_info{a="1",foo="bar"} 1 1 +a_info{a="2",foo="bar"} 1 0 +# EOF +""") + imf = InfoMetricFamily("a", "help") + imf.add_sample("a_info", {"a": "1", "foo": "bar"}, 1, Timestamp(1, 0)) + imf.add_sample("a_info", {"a": "2", "foo": "bar"}, 1, Timestamp(0, 0)) + self.assertEqual([imf], list(families)) + def test_simple_stateset(self): families = text_string_to_metric_families("""# TYPE a stateset # HELP a help @@ -516,11 +528,18 @@ def test_invalid_input(self): ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} NaN\n# EOF\n'), ('# TYPE a summary\na_sum NaN\n# EOF\n'), ('# TYPE a summary\na_count NaN\n# EOF\n'), - # Bad grouping. + # Bad grouping or ordering. ('# TYPE a histogram\na_sum{a="1"} 0\na_sum{a="2"} 0\na_count{a="1"} 0\n# EOF\n'), ('# TYPE a histogram\na_bucket{a="1",le="1"} 0\na_bucket{a="2",le="+Inf""} 0\na_bucket{a="1",le="+Inf"} 0\n# EOF\n'), ('# TYPE a gaugehistogram\na_gsum{a="1"} 0\na_gsum{a="2"} 0\na_gcount{a="1"} 0\n# EOF\n'), ('# TYPE a summary\nquantile{quantile="0"} 0\na_sum{a="1"} 0\nquantile{quantile="1"} 0\n# EOF\n'), + ('# TYPE a gauge\na 0 -1\na 0 -2\n# EOF\n'), + ('# TYPE a gauge\na 0 -1\na 0 -1.1\n# EOF\n'), + ('# TYPE a gauge\na 0 1\na 0 -1\n# EOF\n'), + ('# TYPE a gauge\na 0 1.1\na 0 1\n# EOF\n'), + ('# TYPE a gauge\na 0 1\na 0 0\n# EOF\n'), + ('# TYPE a gauge\na 0\na 0 0\n# EOF\n'), + ('# TYPE a gauge\na 0 0\na 0\n# EOF\n'), ]: with self.assertRaises(ValueError): list(text_string_to_metric_families(case))