From 1940648ff69e50680e8b07fcfb79f08623e5b8cf Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Thu, 23 Jan 2025 13:30:57 +0100 Subject: [PATCH 1/6] Start implement OM text exposition for nh, add first no obs test Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 59 +++++++++++++++++++-- tests/openmetrics/test_exposition.py | 26 ++++++++- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 84600605..0e90d492 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -26,6 +26,7 @@ def generate_latest(registry): for metric in registry.collect(): try: mname = metric.name + # (Vesari): TODO: this is wrong. TYPE should come before HELP!!!! output.append('# HELP {} {}\n'.format( escape_metric_name(mname), _escape(metric.documentation))) output.append(f'# TYPE {escape_metric_name(mname)} {metric.type}\n') @@ -67,24 +68,72 @@ def generate_latest(registry): floatToGoString(s.exemplar.value), ) else: - exemplarstr = '' + exemplarstr = '' + timestamp = '' if s.timestamp is not None: timestamp = f' {s.timestamp}' + + native_histogram = '' + positive_spans = '' + positive_deltas = '' + negative_spans = '' + negative_deltas = '' + if s.native_histogram: + if s.name is not metric.name: + raise ValueError(f"Metric {metric.name} is native histogram, but sample name is not valid") + if s.native_histogram.pos_spans: + positive_spans = ','.join([f'{ps[0]}:{ps[1]}' for ps in s.native_histogram.pos_spans]) + positive_deltas = ','.join(str(pd) for pd in s.native_histogram.pos_deltas) + if s.native_histogram.neg_spans: + negative_spans = ','.join([f'{ns[0]}:{ns[1]}' for ns in s.native_histogram.neg_spans]) + negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas) + + nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}}}' + if positive_spans != '': + nh_sample_template += ',positive_spans:[{{{}}}]' + if negative_spans != '': + nh_sample_template += ',negative_spans:[{{{}}}]' + if positive_deltas != '': + nh_sample_template += ',positive_deltas:[{{{}}}]' + if negative_deltas == '': + nh_sample_template += '}}' + if negative_deltas != '': + nh_sample_template += ',negative_deltas:[{{{}}}]' + nh_sample_template += '}}' + + native_histogram = nh_sample_template.format( + s.native_histogram.count_value, + s.native_histogram.sum_value, + s.native_histogram.schema, + s.native_histogram.zero_threshold, + s.native_histogram.zero_count, + positive_spans, + negative_spans, + positive_deltas, + negative_deltas, + ) + + value = '' + if s.value is not None or not s.native_histogram: + value = floatToGoString(s.value) if _is_valid_legacy_metric_name(s.name): - output.append('{}{} {}{}{}\n'.format( + output.append('{}{} {}{}{}{}\n'.format( s.name, labelstr, - floatToGoString(s.value), + value, timestamp, exemplarstr, + native_histogram )) + else: - output.append('{} {}{}{}\n'.format( + output.append('{} {}{}{}{}\n'.format( labelstr, - floatToGoString(s.value), + value, timestamp, exemplarstr, + native_histogram )) except Exception as exception: exception.args = (exception.args or ('',)) + (metric,) diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 124e55e9..07b5da46 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -5,7 +5,7 @@ CollectorRegistry, Counter, Enum, Gauge, Histogram, Info, Metric, Summary, ) from prometheus_client.core import ( - Exemplar, GaugeHistogramMetricFamily, Timestamp, + BucketSpan, Exemplar, GaugeHistogramMetricFamily, NativeHistogram, HistogramMetricFamily, Sample, Timestamp, ) from prometheus_client.openmetrics.exposition import generate_latest @@ -94,6 +94,30 @@ def test_histogram(self): # EOF """, generate_latest(self.registry)) + + '''def test_native_histogram(self): + hfm = HistogramMetricFamily("nativehistogram", "Is a basic example of a native histogram") + hfm.add_sample("nativehistogram", None, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + # print(hfm) + self.custom_collector(hfm) + print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE + self.assertEqual(b"""# HELP nativehistogram Is a basic example of a native histogram +# TYPE nativehistogram histogram +nativehistogram {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +# EOF +""", generate_latest(self.registry))''' + + def test_nh_no_observation(self): + hfm = HistogramMetricFamily("nhnoobs", "nhnoobs") + hfm.add_sample("nhnoobs", None, None, None, None, NativeHistogram(0, 0, 3, 2.938735877055719e-39, 0)) + self.custom_collector(hfm) + self.assertEqual(b"""# HELP nhnoobs nhnoobs +# TYPE nhnoobs histogram +nhnoobs {count:0,sum:0,schema:3,zero_threshold:2.938735877055719e-39,zero_count:0} +# EOF +""", generate_latest(self.registry)) + + def test_histogram_negative_buckets(self): s = Histogram('hh', 'A histogram', buckets=[-1, -0.5, 0, 0.5, 1], registry=self.registry) s.observe(-0.5) From c4d24f9d1affa501670fa1106ab3d78d1cb46e3c Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Thu, 23 Jan 2025 14:19:57 +0100 Subject: [PATCH 2/6] Correct template for nh sample spans, add test Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 18 ++++++++++-------- tests/openmetrics/test_exposition.py | 14 +++++++------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 0e90d492..e2b83f2c 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -89,18 +89,20 @@ def generate_latest(registry): negative_spans = ','.join([f'{ns[0]}:{ns[1]}' for ns in s.native_histogram.neg_spans]) negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas) - nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}}}' + nh_sample_template = '{{' + 'count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}' if positive_spans != '': - nh_sample_template += ',positive_spans:[{{{}}}]' + nh_sample_template += ',positive_spans:[{}]' if negative_spans != '': - nh_sample_template += ',negative_spans:[{{{}}}]' + nh_sample_template += ',negative_spans:[{}]' if positive_deltas != '': - nh_sample_template += ',positive_deltas:[{{{}}}]' - if negative_deltas == '': - nh_sample_template += '}}' + nh_sample_template += ',positive_deltas:[{}]' + if negative_deltas == '': + nh_sample_template += '}}' if negative_deltas != '': - nh_sample_template += ',negative_deltas:[{{{}}}]' - nh_sample_template += '}}' + nh_sample_template += ',negative_deltas:[{}]' + nh_sample_template += '}}' + else: + nh_sample_template += '}}' native_histogram = nh_sample_template.format( s.native_histogram.count_value, diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 07b5da46..650e5280 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -95,17 +95,17 @@ def test_histogram(self): """, generate_latest(self.registry)) - '''def test_native_histogram(self): - hfm = HistogramMetricFamily("nativehistogram", "Is a basic example of a native histogram") - hfm.add_sample("nativehistogram", None, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + def test_native_histogram(self): + hfm = HistogramMetricFamily("nh", "nh") + hfm.add_sample("nh", None, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) # print(hfm) self.custom_collector(hfm) print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE - self.assertEqual(b"""# HELP nativehistogram Is a basic example of a native histogram -# TYPE nativehistogram histogram -nativehistogram {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} + self.assertEqual(b"""# HELP nh nh +# TYPE nh histogram +nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} # EOF -""", generate_latest(self.registry))''' +""", generate_latest(self.registry)) def test_nh_no_observation(self): hfm = HistogramMetricFamily("nhnoobs", "nhnoobs") From 928b6808ba5c9139fe4fd99eaee8968f05eea2c8 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Fri, 24 Jan 2025 17:40:03 +0100 Subject: [PATCH 3/6] Correct templating and appending for deltas, add longer spans test Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 77 +++++++++++++-------- tests/openmetrics/test_exposition.py | 16 ++++- 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index e2b83f2c..34296c9a 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -78,44 +78,61 @@ def generate_latest(registry): positive_spans = '' positive_deltas = '' negative_spans = '' - negative_deltas = '' + negative_deltas = '' + pos = False + neg = False if s.native_histogram: if s.name is not metric.name: - raise ValueError(f"Metric {metric.name} is native histogram, but sample name is not valid") + raise ValueError(f"Metric {metric.name} is native histogram, but sample name is not valid") + # Initialize basic nh template + nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}' + + args = [ + s.native_histogram.count_value, + s.native_histogram.sum_value, + s.native_histogram.schema, + s.native_histogram.zero_threshold, + s.native_histogram.zero_count, + ] + + # Signal presence for pos/neg spans/deltas + pos = False + neg = False + + # If there are pos spans, append them to the template and args if s.native_histogram.pos_spans: positive_spans = ','.join([f'{ps[0]}:{ps[1]}' for ps in s.native_histogram.pos_spans]) - positive_deltas = ','.join(str(pd) for pd in s.native_histogram.pos_deltas) - if s.native_histogram.neg_spans: - negative_spans = ','.join([f'{ns[0]}:{ns[1]}' for ns in s.native_histogram.neg_spans]) - negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas) - - nh_sample_template = '{{' + 'count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}' - if positive_spans != '': + positive_deltas = ','.join(f'{pd}' for pd in s.native_histogram.pos_deltas) nh_sample_template += ',positive_spans:[{}]' - if negative_spans != '': + args.append(positive_spans) + pos = True + + # If there are neg spans exist, append them to the template and args + if s.native_histogram.neg_spans: + negative_spans = ','.join([f'{ns[0]}:{ns[1]}' for ns in s.native_histogram.neg_spans]) + negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas) nh_sample_template += ',negative_spans:[{}]' - if positive_deltas != '': + args.append(negative_spans) + neg = True + + # Append pos deltas if pos spans were added + if pos: nh_sample_template += ',positive_deltas:[{}]' - if negative_deltas == '': - nh_sample_template += '}}' - if negative_deltas != '': + args.append(positive_deltas) + + # Append neg deltas if neg spans were added + if neg: nh_sample_template += ',negative_deltas:[{}]' - nh_sample_template += '}}' - else: - nh_sample_template += '}}' - - native_histogram = nh_sample_template.format( - s.native_histogram.count_value, - s.native_histogram.sum_value, - s.native_histogram.schema, - s.native_histogram.zero_threshold, - s.native_histogram.zero_count, - positive_spans, - negative_spans, - positive_deltas, - negative_deltas, - ) - + args.append(negative_deltas) + + # Add closing brace + nh_sample_template += '}}' + + # Format the template with the args + native_histogram = nh_sample_template.format(*args) + + print("These are the pos deltas", positive_deltas) #DEBUGGING LINE + print("The is the nh", native_histogram) #DEBUGGING LINE value = '' if s.value is not None or not s.native_histogram: value = floatToGoString(s.value) diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 650e5280..615b5011 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -98,15 +98,14 @@ def test_histogram(self): def test_native_histogram(self): hfm = HistogramMetricFamily("nh", "nh") hfm.add_sample("nh", None, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) - # print(hfm) self.custom_collector(hfm) - print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE self.assertEqual(b"""# HELP nh nh # TYPE nh histogram nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} # EOF """, generate_latest(self.registry)) + def test_nh_no_observation(self): hfm = HistogramMetricFamily("nhnoobs", "nhnoobs") hfm.add_sample("nhnoobs", None, None, None, None, NativeHistogram(0, 0, 3, 2.938735877055719e-39, 0)) @@ -118,6 +117,19 @@ def test_nh_no_observation(self): """, generate_latest(self.registry)) + def test_nh_longer_spans(self): + hfm = HistogramMetricFamily("nhsp", "Is a basic example of a native histogram with three spans") + hfm.add_sample("nhsp", None, None, None, None, NativeHistogram(4, 6, 3, 2.938735877055719e-39, 1, (BucketSpan(0, 1), BucketSpan(7, 1), BucketSpan(4, 1)), None, (1, 0, 0), None)) + self.custom_collector(hfm) + print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE + self.assertEqual(b"""# HELP nhsp Is a basic example of a native histogram with three spans +# TYPE nhsp histogram +nhsp {count:4,sum:6,schema:3,zero_threshold:2.938735877055719e-39,zero_count:1,positive_spans:[0:1,7:1,4:1],positive_deltas:[1,0,0]} +# EOF +""", generate_latest(self.registry)) + + + def test_histogram_negative_buckets(self): s = Histogram('hh', 'A histogram', buckets=[-1, -0.5, 0, 0.5, 1], registry=self.registry) s.observe(-0.5) From 0e54c442612afacf1c37511b559aca86e2b7e0e1 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Fri, 24 Jan 2025 18:19:11 +0100 Subject: [PATCH 4/6] Add tests for nh with labels, remove labels sorting Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 6 ++-- tests/openmetrics/test_exposition.py | 40 ++++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 34296c9a..1d04d91c 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -41,7 +41,7 @@ def generate_latest(registry): labelstr = '' if s.labels: - items = sorted(s.labels.items()) + items = s.labels.items() labelstr += ','.join( ['{}="{}"'.format( escape_label_name(k), _escape(v)) @@ -81,9 +81,7 @@ def generate_latest(registry): negative_deltas = '' pos = False neg = False - if s.native_histogram: - if s.name is not metric.name: - raise ValueError(f"Metric {metric.name} is native histogram, but sample name is not valid") + if s.native_histogram: # Initialize basic nh template nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}' diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 615b5011..d45a9596 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -121,14 +121,52 @@ def test_nh_longer_spans(self): hfm = HistogramMetricFamily("nhsp", "Is a basic example of a native histogram with three spans") hfm.add_sample("nhsp", None, None, None, None, NativeHistogram(4, 6, 3, 2.938735877055719e-39, 1, (BucketSpan(0, 1), BucketSpan(7, 1), BucketSpan(4, 1)), None, (1, 0, 0), None)) self.custom_collector(hfm) - print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE self.assertEqual(b"""# HELP nhsp Is a basic example of a native histogram with three spans # TYPE nhsp histogram nhsp {count:4,sum:6,schema:3,zero_threshold:2.938735877055719e-39,zero_count:1,positive_spans:[0:1,7:1,4:1],positive_deltas:[1,0,0]} # EOF +""", generate_latest(self.registry)) + + def test_native_histogram_utf8(self): + hfm = HistogramMetricFamily("native{histogram", "Is a basic example of a native histogram") + hfm.add_sample("native{histogram", None, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + self.custom_collector(hfm) + self.assertEqual(b"""# HELP "native{histogram" Is a basic example of a native histogram +# TYPE "native{histogram" histogram +{"native{histogram"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +# EOF +""", generate_latest(self.registry)) + + def test_native_histogram_utf8_stress(self): + hfm = HistogramMetricFamily("native{histogram", "Is a basic example of a native histogram") + hfm.add_sample("native{histogram", {'xx{} # {}': ' EOF # {}}}'}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + self.custom_collector(hfm) + self.assertEqual(b"""# HELP "native{histogram" Is a basic example of a native histogram +# TYPE "native{histogram" histogram +{"native{histogram", "xx{} # {}"=" EOF # {}}}"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +# EOF """, generate_latest(self.registry)) + def test_native_histogram_with_labels(self): + hfm = HistogramMetricFamily("hist_w_labels", "Is a basic example of a native histogram with labels") + hfm.add_sample("hist_w_labels", {"foo": "bar", "baz": "qux"}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + self.custom_collector(hfm) + self.assertEqual(b"""# HELP hist_w_labels Is a basic example of a native histogram with labels +# TYPE hist_w_labels histogram +hist_w_labels{foo="bar",baz="qux"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +# EOF +""", generate_latest(self.registry)) + def test_native_histogram_with_labels_utf8(self): + hfm = HistogramMetricFamily("hist.w.labels", "Is a basic example of a native histogram with labels") + hfm.add_sample("hist.w.labels", {"foo": "bar", "baz": "qux"}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + self.custom_collector(hfm) + print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE + self.assertEqual(b"""# HELP "hist.w.labels" Is a basic example of a native histogram with labels +# TYPE "hist.w.labels" histogram +{"hist.w.labels", foo="bar",baz="qux"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +# EOF +""", generate_latest(self.registry)) def test_histogram_negative_buckets(self): s = Histogram('hh', 'A histogram', buckets=[-1, -0.5, 0, 0.5, 1], registry=self.registry) From 00427cc80e30b0e1a7c8c8ebf84dc62ba4b20125 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Sun, 26 Jan 2025 17:51:41 +0100 Subject: [PATCH 5/6] Break down logic classic vs nh samples, add tests for classic-native histograms cohabitation Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 98 +++++++++++---------- tests/openmetrics/test_exposition.py | 50 ++++++++++- 2 files changed, 98 insertions(+), 50 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 1d04d91c..78ee3f11 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -26,61 +26,22 @@ def generate_latest(registry): for metric in registry.collect(): try: mname = metric.name - # (Vesari): TODO: this is wrong. TYPE should come before HELP!!!! output.append('# HELP {} {}\n'.format( escape_metric_name(mname), _escape(metric.documentation))) output.append(f'# TYPE {escape_metric_name(mname)} {metric.type}\n') if metric.unit: - output.append(f'# UNIT {escape_metric_name(mname)} {metric.unit}\n') + output.append(f'# UNIT {escape_metric_name(mname)} {metric.unit}\n') for s in metric.samples: - if not _is_valid_legacy_metric_name(s.name): - labelstr = escape_metric_name(s.name) - if s.labels: - labelstr += ', ' - else: - labelstr = '' - - if s.labels: - items = s.labels.items() - labelstr += ','.join( - ['{}="{}"'.format( - escape_label_name(k), _escape(v)) - for k, v in items]) - if labelstr: - labelstr = "{" + labelstr + "}" - - if s.exemplar: - if not _is_valid_exemplar_metric(metric, s): - raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter") - labels = '{{{0}}}'.format(','.join( - ['{}="{}"'.format( - k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) - for k, v in sorted(s.exemplar.labels.items())])) - if s.exemplar.timestamp is not None: - exemplarstr = ' # {} {} {}'.format( - labels, - floatToGoString(s.exemplar.value), - s.exemplar.timestamp, - ) - else: - exemplarstr = ' # {} {}'.format( - labels, - floatToGoString(s.exemplar.value), - ) - else: - exemplarstr = '' + labelstr, exemplarstr, timestamp = _expose_classic_metrics_sample(metric, s) - timestamp = '' - if s.timestamp is not None: - timestamp = f' {s.timestamp}' - native_histogram = '' positive_spans = '' positive_deltas = '' negative_spans = '' negative_deltas = '' pos = False - neg = False + neg = False + if s.native_histogram: # Initialize basic nh template nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}' @@ -127,13 +88,11 @@ def generate_latest(registry): nh_sample_template += '}}' # Format the template with the args - native_histogram = nh_sample_template.format(*args) + native_histogram = nh_sample_template.format(*args) - print("These are the pos deltas", positive_deltas) #DEBUGGING LINE - print("The is the nh", native_histogram) #DEBUGGING LINE value = '' if s.value is not None or not s.native_histogram: - value = floatToGoString(s.value) + value = floatToGoString(s.value) if _is_valid_legacy_metric_name(s.name): output.append('{}{} {}{}{}{}\n'.format( s.name, @@ -143,7 +102,6 @@ def generate_latest(registry): exemplarstr, native_histogram )) - else: output.append('{} {}{}{}{}\n'.format( labelstr, @@ -181,3 +139,47 @@ def escape_label_name(s: str) -> str: def _escape(s: str) -> str: """Performs backslash escaping on backslash, newline, and double-quote characters.""" return s.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"') + + +def _expose_classic_metrics_sample(metric, sample) -> tuple[str, str, str]: + if not _is_valid_legacy_metric_name(sample.name): + labelstr = escape_metric_name(sample.name) + if sample.labels: + labelstr += ', ' + else: + labelstr = '' + + if sample.labels: + items = sample.labels.items() + labelstr += ','.join( + ['{}="{}"'.format( + escape_label_name(k), _escape(v)) + for k, v in items]) + if labelstr: + labelstr = "{" + labelstr + "}" + + if sample.exemplar: + if not _is_valid_exemplar_metric(metric, sample): + raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter") + labels = '{{{0}}}'.format(','.join( + ['{}="{}"'.format( + k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) + for k, v in sorted(sample.exemplar.labels.items())])) + if sample.exemplar.timestamp is not None: + exemplarstr = ' # {} {} {}'.format( + labels, + floatToGoString(sample.exemplar.value), + sample.exemplar.timestamp, + ) + else: + exemplarstr = ' # {} {}'.format( + labels, + floatToGoString(sample.exemplar.value), + ) + else: + exemplarstr = '' + + timestamp = '' + if sample.timestamp is not None: + timestamp = f' {sample.timestamp}' + return labelstr, exemplarstr, timestamp diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index d45a9596..0f421405 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -5,7 +5,8 @@ CollectorRegistry, Counter, Enum, Gauge, Histogram, Info, Metric, Summary, ) from prometheus_client.core import ( - BucketSpan, Exemplar, GaugeHistogramMetricFamily, NativeHistogram, HistogramMetricFamily, Sample, Timestamp, + BucketSpan, Exemplar, GaugeHistogramMetricFamily, HistogramMetricFamily, + NativeHistogram, Timestamp, ) from prometheus_client.openmetrics.exposition import generate_latest @@ -161,11 +162,56 @@ def test_native_histogram_with_labels_utf8(self): hfm = HistogramMetricFamily("hist.w.labels", "Is a basic example of a native histogram with labels") hfm.add_sample("hist.w.labels", {"foo": "bar", "baz": "qux"}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) self.custom_collector(hfm) - print("this is the GOT", generate_latest(self.registry)) # DEBUGGING LINE self.assertEqual(b"""# HELP "hist.w.labels" Is a basic example of a native histogram with labels # TYPE "hist.w.labels" histogram {"hist.w.labels", foo="bar",baz="qux"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} # EOF +""", generate_latest(self.registry)) + + def test_native_histogram_with_classic_histogram(self): + hfm = HistogramMetricFamily("hist_w_classic", "Is a basic example of a native histogram coexisting with a classic histogram") + hfm.add_sample("hist_w_classic", {"foo": "bar"}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + hfm.add_sample("hist_w_classic_bucket", {"foo": "bar", "le": "0.001"}, 4.0, None, None, None) + hfm.add_sample("hist_w_classic_bucket", {"foo": "bar", "le": "+Inf"}, 24.0, None, None, None) + hfm.add_sample("hist_w_classic_count", {"foo": "bar"}, 24.0, None, None, None) + hfm.add_sample("hist_w_classic_sum", {"foo": "bar"}, 100.0, None, None, None) + self.custom_collector(hfm) + self.assertEqual(b"""# HELP hist_w_classic Is a basic example of a native histogram coexisting with a classic histogram +# TYPE hist_w_classic histogram +hist_w_classic{foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +hist_w_classic_bucket{foo="bar",le="0.001"} 4.0 +hist_w_classic_bucket{foo="bar",le="+Inf"} 24.0 +hist_w_classic_count{foo="bar"} 24.0 +hist_w_classic_sum{foo="bar"} 100.0 +# EOF +""", generate_latest(self.registry)) + + def test_native_plus_classic_histogram_two_labelsets(self): + hfm = HistogramMetricFamily("hist_w_classic_two_sets", "Is an example of a native histogram plus a classic histogram with two label sets") + hfm.add_sample("hist_w_classic_two_sets", {"foo": "bar"}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + hfm.add_sample("hist_w_classic_two_sets_bucket", {"foo": "bar", "le": "0.001"}, 4.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets_bucket", {"foo": "bar", "le": "+Inf"}, 24.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets_count", {"foo": "bar"}, 24.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets_sum", {"foo": "bar"}, 100.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets", {"foo": "baz"}, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3))) + hfm.add_sample("hist_w_classic_two_sets_bucket", {"foo": "baz", "le": "0.001"}, 4.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets_bucket", {"foo": "baz", "le": "+Inf"}, 24.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets_count", {"foo": "baz"}, 24.0, None, None, None) + hfm.add_sample("hist_w_classic_two_sets_sum", {"foo": "baz"}, 100.0, None, None, None) + self.custom_collector(hfm) + self.assertEqual(b"""# HELP hist_w_classic_two_sets Is an example of a native histogram plus a classic histogram with two label sets +# TYPE hist_w_classic_two_sets histogram +hist_w_classic_two_sets{foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +hist_w_classic_two_sets_bucket{foo="bar",le="0.001"} 4.0 +hist_w_classic_two_sets_bucket{foo="bar",le="+Inf"} 24.0 +hist_w_classic_two_sets_count{foo="bar"} 24.0 +hist_w_classic_two_sets_sum{foo="bar"} 100.0 +hist_w_classic_two_sets{foo="baz"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]} +hist_w_classic_two_sets_bucket{foo="baz",le="0.001"} 4.0 +hist_w_classic_two_sets_bucket{foo="baz",le="+Inf"} 24.0 +hist_w_classic_two_sets_count{foo="baz"} 24.0 +hist_w_classic_two_sets_sum{foo="baz"} 100.0 +# EOF """, generate_latest(self.registry)) def test_histogram_negative_buckets(self): From 28028f7de940cce4cad84a5e0cdf5f380f008fc8 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Sun, 26 Jan 2025 18:37:20 +0100 Subject: [PATCH 6/6] Move classic sample logic back to where it belongs Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 84 ++++++++++----------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 78ee3f11..a1c930b2 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -32,7 +32,46 @@ def generate_latest(registry): if metric.unit: output.append(f'# UNIT {escape_metric_name(mname)} {metric.unit}\n') for s in metric.samples: - labelstr, exemplarstr, timestamp = _expose_classic_metrics_sample(metric, s) + if not _is_valid_legacy_metric_name(s.name): + labelstr = escape_metric_name(s.name) + if s.labels: + labelstr += ', ' + else: + labelstr = '' + + if s.labels: + items = s.labels.items() + labelstr += ','.join( + ['{}="{}"'.format( + escape_label_name(k), _escape(v)) + for k, v in items]) + if labelstr: + labelstr = "{" + labelstr + "}" + + if s.exemplar: + if not _is_valid_exemplar_metric(metric, s): + raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter") + labels = '{{{0}}}'.format(','.join( + ['{}="{}"'.format( + k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) + for k, v in sorted(s.exemplar.labels.items())])) + if s.exemplar.timestamp is not None: + exemplarstr = ' # {} {} {}'.format( + labels, + floatToGoString(s.exemplar.value), + s.exemplar.timestamp, + ) + else: + exemplarstr = ' # {} {}'.format( + labels, + floatToGoString(s.exemplar.value), + ) + else: + exemplarstr = '' + + timestamp = '' + if s.timestamp is not None: + timestamp = f' {s.timestamp}' native_histogram = '' positive_spans = '' @@ -141,45 +180,4 @@ def _escape(s: str) -> str: return s.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"') -def _expose_classic_metrics_sample(metric, sample) -> tuple[str, str, str]: - if not _is_valid_legacy_metric_name(sample.name): - labelstr = escape_metric_name(sample.name) - if sample.labels: - labelstr += ', ' - else: - labelstr = '' - - if sample.labels: - items = sample.labels.items() - labelstr += ','.join( - ['{}="{}"'.format( - escape_label_name(k), _escape(v)) - for k, v in items]) - if labelstr: - labelstr = "{" + labelstr + "}" - - if sample.exemplar: - if not _is_valid_exemplar_metric(metric, sample): - raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter") - labels = '{{{0}}}'.format(','.join( - ['{}="{}"'.format( - k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) - for k, v in sorted(sample.exemplar.labels.items())])) - if sample.exemplar.timestamp is not None: - exemplarstr = ' # {} {} {}'.format( - labels, - floatToGoString(sample.exemplar.value), - sample.exemplar.timestamp, - ) - else: - exemplarstr = ' # {} {}'.format( - labels, - floatToGoString(sample.exemplar.value), - ) - else: - exemplarstr = '' - - timestamp = '' - if sample.timestamp is not None: - timestamp = f' {sample.timestamp}' - return labelstr, exemplarstr, timestamp +