From 1940648ff69e50680e8b07fcfb79f08623e5b8cf Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Thu, 23 Jan 2025 13:30:57 +0100 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 + From bd8072cff85e0711a62ec0118386e338a386aa95 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Wed, 14 May 2025 16:37:58 +0200 Subject: [PATCH 07/14] Assign nh to value, correct nil values in tests, clean up white spaces Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 33 ++++---- tests/openmetrics/test_exposition.py | 86 ++++++++++----------- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index a1c930b2..fb8e2a0b 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -30,7 +30,7 @@ def generate_latest(registry): 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) @@ -40,11 +40,11 @@ def generate_latest(registry): labelstr = '' if s.labels: - items = s.labels.items() + items = sorted(s.labels.items()) labelstr += ','.join( ['{}="{}"'.format( escape_label_name(k), _escape(v)) - for k, v in items]) + for k, v in items]) if labelstr: labelstr = "{" + labelstr + "}" @@ -67,7 +67,7 @@ def generate_latest(registry): floatToGoString(s.exemplar.value), ) else: - exemplarstr = '' + exemplarstr = '' timestamp = '' if s.timestamp is not None: @@ -81,7 +81,7 @@ def generate_latest(registry): pos = False neg = False - if s.native_histogram: + if s.native_histogram: # Initialize basic nh template nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}' @@ -127,27 +127,27 @@ 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) - value = '' - if s.value is not None or not s.native_histogram: - value = floatToGoString(s.value) + value = '' + if s.native_histogram: + value = native_histogram + elif s.value is not None: + value = floatToGoString(s.value) if _is_valid_legacy_metric_name(s.name): - output.append('{}{} {}{}{}{}\n'.format( + output.append('{}{} {}{}{}\n'.format( s.name, labelstr, value, timestamp, exemplarstr, - native_histogram )) else: - output.append('{} {}{}{}{}\n'.format( + output.append('{} {}{}{}\n'.format( labelstr, value, timestamp, - exemplarstr, - native_histogram + exemplarstr )) except Exception as exception: exception.args = (exception.args or ('',)) + (metric,) @@ -177,7 +177,4 @@ 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'\"') - - - + return s.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"') \ No newline at end of file diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 0f421405..f96e568b 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -22,43 +22,43 @@ def setUp(self): def tearDown(self): time.time = self.old_time - def custom_collector(self, metric_family): + def custom_collector(self, metric_family) -> None: class CustomCollector: def collect(self): return [metric_family] self.registry.register(CustomCollector()) - def test_counter(self): + def test_counter(self) -> None: c = Counter('cc', 'A counter', registry=self.registry) c.inc() self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc_total 1.0\ncc_created 123.456\n# EOF\n', generate_latest(self.registry)) - def test_counter_utf8(self): + def test_counter_utf8(self) -> None: c = Counter('cc.with.dots', 'A counter', registry=self.registry) c.inc() self.assertEqual(b'# HELP "cc.with.dots" A counter\n# TYPE "cc.with.dots" counter\n{"cc.with.dots_total"} 1.0\n{"cc.with.dots_created"} 123.456\n# EOF\n', generate_latest(self.registry)) - def test_counter_total(self): + def test_counter_total(self) -> None: c = Counter('cc_total', 'A counter', registry=self.registry) c.inc() self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc_total 1.0\ncc_created 123.456\n# EOF\n', generate_latest(self.registry)) - def test_counter_unit(self): + def test_counter_unit(self) -> None: c = Counter('cc_seconds', 'A counter', registry=self.registry, unit="seconds") c.inc() self.assertEqual(b'# HELP cc_seconds A counter\n# TYPE cc_seconds counter\n# UNIT cc_seconds seconds\ncc_seconds_total 1.0\ncc_seconds_created 123.456\n# EOF\n', generate_latest(self.registry)) - def test_gauge(self): + def test_gauge(self) -> None: g = Gauge('gg', 'A gauge', registry=self.registry) g.set(17) self.assertEqual(b'# HELP gg A gauge\n# TYPE gg gauge\ngg 17.0\n# EOF\n', generate_latest(self.registry)) - def test_summary(self): + def test_summary(self) -> None: s = Summary('ss', 'A summary', ['a', 'b'], registry=self.registry) s.labels('c', 'd').observe(17) self.assertEqual(b"""# HELP ss A summary @@ -69,7 +69,7 @@ def test_summary(self): # EOF """, generate_latest(self.registry)) - def test_histogram(self): + def test_histogram(self) -> None: s = Histogram('hh', 'A histogram', registry=self.registry) s.observe(0.05) self.assertEqual(b"""# HELP hh A histogram @@ -96,9 +96,9 @@ def test_histogram(self): """, generate_latest(self.registry)) - def test_native_histogram(self): + def test_native_histogram(self) -> None: 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))) + hfm.add_sample("nh", {}, 0, 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 nh nh # TYPE nh histogram @@ -107,9 +107,9 @@ def test_native_histogram(self): """, generate_latest(self.registry)) - def test_nh_no_observation(self): + def test_nh_no_observation(self) -> None: hfm = HistogramMetricFamily("nhnoobs", "nhnoobs") - hfm.add_sample("nhnoobs", None, None, None, None, NativeHistogram(0, 0, 3, 2.938735877055719e-39, 0)) + hfm.add_sample("nhnoobs", {}, 0, None, None, NativeHistogram(0, 0, 3, 2.938735877055719e-39, 0)) self.custom_collector(hfm) self.assertEqual(b"""# HELP nhnoobs nhnoobs # TYPE nhnoobs histogram @@ -118,9 +118,9 @@ def test_nh_no_observation(self): """, generate_latest(self.registry)) - def test_nh_longer_spans(self): + def test_nh_longer_spans(self) -> None: 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)) + hfm.add_sample("nhsp", {}, 0, 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) self.assertEqual(b"""# HELP nhsp Is a basic example of a native histogram with three spans # TYPE nhsp histogram @@ -128,9 +128,9 @@ def test_nh_longer_spans(self): # EOF """, generate_latest(self.registry)) - def test_native_histogram_utf8(self): + def test_native_histogram_utf8(self) -> None: 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))) + hfm.add_sample("native{histogram", {}, 0, 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 @@ -138,9 +138,9 @@ def test_native_histogram_utf8(self): # EOF """, generate_latest(self.registry)) - def test_native_histogram_utf8_stress(self): + def test_native_histogram_utf8_stress(self) -> None: 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))) + hfm.add_sample("native{histogram", {'xx{} # {}': ' EOF # {}}}'}, 0, 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 @@ -148,29 +148,29 @@ def test_native_histogram_utf8_stress(self): # EOF """, generate_latest(self.registry)) - def test_native_histogram_with_labels(self): + def test_native_histogram_with_labels(self) -> None: 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))) + hfm.add_sample("hist_w_labels", {"foo": "bar", "baz": "qux"}, 0, 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]} +hist_w_labels{baz="qux",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]} # EOF """, generate_latest(self.registry)) - def test_native_histogram_with_labels_utf8(self): + def test_native_histogram_with_labels_utf8(self) -> None: 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))) + hfm.add_sample("hist.w.labels", {"foo": "bar", "baz": "qux"}, 0, 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]} +{"hist.w.labels", baz="qux",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]} # EOF """, generate_latest(self.registry)) - def test_native_histogram_with_classic_histogram(self): + def test_native_histogram_with_classic_histogram(self) -> None: 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", {"foo": "bar"}, 0, 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) @@ -186,14 +186,14 @@ def test_native_histogram_with_classic_histogram(self): # EOF """, generate_latest(self.registry)) - def test_native_plus_classic_histogram_two_labelsets(self): + def test_native_plus_classic_histogram_two_labelsets(self) -> None: 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", {"foo": "bar"}, 0, 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", {"foo": "baz"}, 0, 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) @@ -214,7 +214,7 @@ def test_native_plus_classic_histogram_two_labelsets(self): # EOF """, generate_latest(self.registry)) - def test_histogram_negative_buckets(self): + def test_histogram_negative_buckets(self) -> None: s = Histogram('hh', 'A histogram', buckets=[-1, -0.5, 0, 0.5, 1], registry=self.registry) s.observe(-0.5) self.assertEqual(b"""# HELP hh A histogram @@ -230,7 +230,7 @@ def test_histogram_negative_buckets(self): # EOF """, generate_latest(self.registry)) - def test_histogram_exemplar(self): + def test_histogram_exemplar(self) -> None: s = Histogram('hh', 'A histogram', buckets=[1, 2, 3, 4], registry=self.registry) s.observe(0.5, {'a': 'b'}) s.observe(1.5, {'le': '7'}) @@ -250,7 +250,7 @@ def test_histogram_exemplar(self): # EOF """, generate_latest(self.registry)) - def test_counter_exemplar(self): + def test_counter_exemplar(self) -> None: c = Counter('cc', 'A counter', registry=self.registry) c.inc(exemplar={'a': 'b'}) self.assertEqual(b"""# HELP cc A counter @@ -260,7 +260,7 @@ def test_counter_exemplar(self): # EOF """, generate_latest(self.registry)) - def test_untyped_exemplar(self): + def test_untyped_exemplar(self) -> None: class MyCollector: def collect(self): metric = Metric("hh", "help", 'untyped') @@ -272,7 +272,7 @@ def collect(self): with self.assertRaises(ValueError): generate_latest(self.registry) - def test_histogram_non_bucket_exemplar(self): + def test_histogram_non_bucket_exemplar(self) -> None: class MyCollector: def collect(self): metric = Metric("hh", "help", 'histogram') @@ -284,7 +284,7 @@ def collect(self): with self.assertRaises(ValueError): generate_latest(self.registry) - def test_counter_non_total_exemplar(self): + def test_counter_non_total_exemplar(self) -> None: class MyCollector: def collect(self): metric = Metric("cc", "A counter", 'counter') @@ -296,7 +296,7 @@ def collect(self): with self.assertRaises(ValueError): generate_latest(self.registry) - def test_gaugehistogram(self): + def test_gaugehistogram(self) -> None: self.custom_collector( GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))], gsum_value=7)) self.assertEqual(b"""# HELP gh help @@ -308,7 +308,7 @@ def test_gaugehistogram(self): # EOF """, generate_latest(self.registry)) - def test_gaugehistogram_negative_buckets(self): + def test_gaugehistogram_negative_buckets(self) -> None: self.custom_collector( GaugeHistogramMetricFamily('gh', 'help', buckets=[('-1.0', 4), ('+Inf', (5))], gsum_value=-7)) self.assertEqual(b"""# HELP gh help @@ -320,7 +320,7 @@ def test_gaugehistogram_negative_buckets(self): # EOF """, generate_latest(self.registry)) - def test_info(self): + def test_info(self) -> None: i = Info('ii', 'A info', ['a', 'b'], registry=self.registry) i.labels('c', 'd').info({'foo': 'bar'}) self.assertEqual(b"""# HELP ii A info @@ -329,7 +329,7 @@ def test_info(self): # EOF """, generate_latest(self.registry)) - def test_enum(self): + def test_enum(self) -> None: i = Enum('ee', 'An enum', ['a', 'b'], registry=self.registry, states=['foo', 'bar']) i.labels('c', 'd').state('bar') self.assertEqual(b"""# HELP ee An enum @@ -339,7 +339,7 @@ def test_enum(self): # EOF """, generate_latest(self.registry)) - def test_unicode(self): + def test_unicode(self) -> None: c = Counter('cc', '\u4500', ['l'], registry=self.registry) c.labels('\u4500').inc() self.assertEqual(b"""# HELP cc \xe4\x94\x80 @@ -349,7 +349,7 @@ def test_unicode(self): # EOF """, generate_latest(self.registry)) - def test_escaping(self): + def test_escaping(self) -> None: c = Counter('cc', 'A\ncount\\er\"', ['a'], registry=self.registry) c.labels('\\x\n"').inc(1) self.assertEqual(b"""# HELP cc A\\ncount\\\\er\\" @@ -359,7 +359,7 @@ def test_escaping(self): # EOF """, generate_latest(self.registry)) - def test_nonnumber(self): + def test_nonnumber(self) -> None: class MyNumber: def __repr__(self): return "MyNumber(123)" @@ -377,7 +377,7 @@ def collect(self): self.assertEqual(b'# HELP nonnumber Non number\n# TYPE nonnumber unknown\nnonnumber 123.0\n# EOF\n', generate_latest(self.registry)) - def test_timestamp(self): + def test_timestamp(self) -> None: class MyCollector: def collect(self): metric = Metric("ts", "help", 'unknown') From 2fdb24cf08a6b7e7e4885afcc3f9745635193b3f Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Thu, 15 May 2025 18:23:59 +0200 Subject: [PATCH 08/14] Add logic for exposing nh exemplars Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 64 +++++++++++---------- prometheus_client/samples.py | 23 ++++---- tests/openmetrics/test_exposition.py | 10 ++++ 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index fb8e2a0b..28dd71d8 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -49,23 +49,7 @@ def generate_latest(registry): 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), - ) + exemplarstr = _compose_exemplar_string(metric, s, s.exemplar) else: exemplarstr = '' @@ -93,17 +77,12 @@ def generate_latest(registry): 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(f'{pd}' for pd in s.native_histogram.pos_deltas) nh_sample_template += ',positive_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: @@ -111,36 +90,40 @@ def generate_latest(registry): negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas) nh_sample_template += ',negative_spans:[{}]' args.append(negative_spans) - neg = True # Append pos deltas if pos spans were added - if pos: + if s.native_histogram.pos_spans: nh_sample_template += ',positive_deltas:[{}]' args.append(positive_deltas) # Append neg deltas if neg spans were added - if neg: + if s.native_histogram.neg_spans: nh_sample_template += ',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) + + if s.native_histogram.nh_exemplars: + for nh_ex in s.native_histogram.nh_exemplars: + nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex) + native_histogram += nh_exemplarstr value = '' if s.native_histogram: value = native_histogram elif s.value is not None: - value = floatToGoString(s.value) + value = floatToGoString(s.value) if _is_valid_legacy_metric_name(s.name): output.append('{}{} {}{}{}\n'.format( s.name, labelstr, value, timestamp, - exemplarstr, + exemplarstr )) else: output.append('{} {}{}{}\n'.format( @@ -177,4 +160,27 @@ 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'\"') \ No newline at end of file + return s.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"') + + +def _compose_exemplar_string(metric, sample, exemplar) -> str: + """Constructs an exemplar string.""" + 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(exemplar.labels.items())])) + if exemplar.timestamp is not None: + exemplarstr = ' # {} {} {}'.format( + labels, + floatToGoString(exemplar.value), + exemplar.timestamp, + ) + else: + exemplarstr = ' # {} {}'.format( + labels, + floatToGoString(exemplar.value), + ) + + return exemplarstr diff --git a/prometheus_client/samples.py b/prometheus_client/samples.py index 16e03c04..994d1281 100644 --- a/prometheus_client/samples.py +++ b/prometheus_client/samples.py @@ -40,6 +40,17 @@ class BucketSpan(NamedTuple): length: int +# Timestamp and exemplar are optional. +# Value can be an int or a float. +# Timestamp can be a float containing a unixtime in seconds, +# a Timestamp object, or None. +# Exemplar can be an Exemplar object, or None. +class Exemplar(NamedTuple): + labels: Dict[str, str] + value: float + timestamp: Optional[Union[float, Timestamp]] = None + + # NativeHistogram is experimental and subject to change at any time. class NativeHistogram(NamedTuple): count_value: float @@ -51,17 +62,7 @@ class NativeHistogram(NamedTuple): neg_spans: Optional[Sequence[BucketSpan]] = None pos_deltas: Optional[Sequence[int]] = None neg_deltas: Optional[Sequence[int]] = None - - -# Timestamp and exemplar are optional. -# Value can be an int or a float. -# Timestamp can be a float containing a unixtime in seconds, -# a Timestamp object, or None. -# Exemplar can be an Exemplar object, or None. -class Exemplar(NamedTuple): - labels: Dict[str, str] - value: float - timestamp: Optional[Union[float, Timestamp]] = None + nh_exemplars: Optional[Sequence[Exemplar]] = None class Sample(NamedTuple): diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index f96e568b..1d86de66 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -106,6 +106,16 @@ def test_native_histogram(self) -> None: # EOF """, generate_latest(self.registry)) + + def test_nh_histogram_with_exemplars(self) -> None: + hfm = HistogramMetricFamily("nh", "nh") + hfm.add_sample("nh", {}, 0, 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), (Exemplar({"trace_id": "KOO5S4vxi0o"}, 0.67), Exemplar({"trace_id": "oHg5SJYRHA0"}, 9.8, float(Timestamp(1520879607, 0.789 * 1e9)))))) + self.custom_collector(hfm) + 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]} # {trace_id="KOO5S4vxi0o"} 0.67 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789 +# EOF +""", generate_latest(self.registry)) def test_nh_no_observation(self) -> None: hfm = HistogramMetricFamily("nhnoobs", "nhnoobs") From 11a6e874c65b27e9581cb3b4c6f3665efece040a Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Fri, 23 May 2025 17:07:44 +0200 Subject: [PATCH 09/14] Please linters Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 28dd71d8..358d8c84 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -20,6 +20,29 @@ def _is_valid_exemplar_metric(metric, sample): return False +def _compose_exemplar_string(metric, sample, exemplar): + """Constructs an exemplar string.""" + 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(exemplar.labels.items())])) + if exemplar.timestamp is not None: + exemplarstr = ' # {} {} {}'.format( + labels, + floatToGoString(exemplar.value), + exemplar.timestamp, + ) + else: + exemplarstr = ' # {} {}'.format( + labels, + floatToGoString(exemplar.value), + ) + + return exemplarstr + + def generate_latest(registry): '''Returns the metrics from the registry in latest text format as a string.''' output = [] @@ -109,12 +132,12 @@ def generate_latest(registry): if s.native_histogram.nh_exemplars: for nh_ex in s.native_histogram.nh_exemplars: - nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex) - native_histogram += nh_exemplarstr + nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex) + native_histogram += nh_exemplarstr value = '' if s.native_histogram: - value = native_histogram + value = native_histogram elif s.value is not None: value = floatToGoString(s.value) if _is_valid_legacy_metric_name(s.name): @@ -161,26 +184,3 @@ 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 _compose_exemplar_string(metric, sample, exemplar) -> str: - """Constructs an exemplar string.""" - 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(exemplar.labels.items())])) - if exemplar.timestamp is not None: - exemplarstr = ' # {} {} {}'.format( - labels, - floatToGoString(exemplar.value), - exemplar.timestamp, - ) - else: - exemplarstr = ' # {} {}'.format( - labels, - floatToGoString(exemplar.value), - ) - - return exemplarstr From b2f5afb0899a6a3f01e984324344d892837a7c15 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Fri, 23 May 2025 17:23:27 +0200 Subject: [PATCH 10/14] Assign nh_exemplars to exemplarstr Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 358d8c84..3a57d796 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -133,7 +133,7 @@ def generate_latest(registry): if s.native_histogram.nh_exemplars: for nh_ex in s.native_histogram.nh_exemplars: nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex) - native_histogram += nh_exemplarstr + exemplarstr += nh_exemplarstr value = '' if s.native_histogram: From 3103c03d0f35ee602db06ff9fe6f606c82cdb220 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Mon, 2 Jun 2025 16:18:48 +0200 Subject: [PATCH 11/14] Add Any type to metric_family in OM exposition test Signed-off-by: Arianna Vespri --- tests/openmetrics/test_exposition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 1d86de66..235c6d89 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -1,4 +1,5 @@ import time +from typing import Any import unittest from prometheus_client import ( @@ -22,7 +23,7 @@ def setUp(self): def tearDown(self): time.time = self.old_time - def custom_collector(self, metric_family) -> None: + def custom_collector(self, metric_family: Any) -> None: class CustomCollector: def collect(self): return [metric_family] From 9f00c9f6f0fd3ed351c416ac225a9c000d5caae9 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Tue, 10 Jun 2025 17:50:42 +0200 Subject: [PATCH 12/14] Change printing order of nh spans and deltas according to OM 2.0 proposal Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 34 ++++++++------------- tests/openmetrics/test_exposition.py | 19 ++++++------ 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 3a57d796..2207a7cc 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -81,12 +81,10 @@ def generate_latest(registry): timestamp = f' {s.timestamp}' native_histogram = '' - positive_spans = '' - positive_deltas = '' negative_spans = '' negative_deltas = '' - pos = False - neg = False + positive_spans = '' + positive_deltas = '' if s.native_histogram: # Initialize basic nh template @@ -99,31 +97,25 @@ def generate_latest(registry): s.native_histogram.zero_threshold, s.native_histogram.zero_count, ] - - # 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(f'{pd}' for pd in s.native_histogram.pos_deltas) - nh_sample_template += ',positive_spans:[{}]' - args.append(positive_spans) - - # If there are neg spans exist, append them to the template and args + + # If there are neg spans, append them and the neg deltas 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:[{}]' args.append(negative_spans) + nh_sample_template += ',negative_deltas:[{}]' + args.append(negative_deltas) - # Append pos deltas if pos spans were added + # If there are pos spans, append them and the pos spans 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(f'{pd}' for pd in s.native_histogram.pos_deltas) + nh_sample_template += ',positive_spans:[{}]' + args.append(positive_spans) nh_sample_template += ',positive_deltas:[{}]' - args.append(positive_deltas) - - # Append neg deltas if neg spans were added - if s.native_histogram.neg_spans: - nh_sample_template += ',negative_deltas:[{}]' - args.append(negative_deltas) - + args.append(positive_deltas) + # Add closing brace nh_sample_template += '}}' diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 235c6d89..3cd6d05f 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -103,10 +103,9 @@ def test_native_histogram(self) -> None: self.custom_collector(hfm) 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]} +nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) - def test_nh_histogram_with_exemplars(self) -> None: hfm = HistogramMetricFamily("nh", "nh") @@ -114,7 +113,7 @@ def test_nh_histogram_with_exemplars(self) -> None: self.custom_collector(hfm) 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]} # {trace_id="KOO5S4vxi0o"} 0.67 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789 +nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # {trace_id="KOO5S4vxi0o"} 0.67 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789 # EOF """, generate_latest(self.registry)) @@ -145,7 +144,7 @@ def test_native_histogram_utf8(self) -> None: 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]} +{"native{histogram"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -155,7 +154,7 @@ def test_native_histogram_utf8_stress(self) -> None: 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]} +{"native{histogram", "xx{} # {}"=" EOF # {}}}"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -165,7 +164,7 @@ def test_native_histogram_with_labels(self) -> None: 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{baz="qux",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_labels{baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -175,7 +174,7 @@ def test_native_histogram_with_labels_utf8(self) -> None: 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", baz="qux",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.labels", baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -189,7 +188,7 @@ def test_native_histogram_with_classic_histogram(self) -> 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{foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,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 @@ -212,12 +211,12 @@ def test_native_plus_classic_histogram_two_labelsets(self) -> 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{foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,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{foo="baz"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,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 From 91fa9881e0979f5444593db7d8a6991e20128cd6 Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Thu, 12 Jun 2025 17:11:16 +0200 Subject: [PATCH 13/14] Shorten name of spans and deltas as per OM 2.0 proposal Signed-off-by: Arianna Vespri --- prometheus_client/openmetrics/exposition.py | 8 ++++---- tests/openmetrics/test_exposition.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 2207a7cc..2f9a73cb 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -102,18 +102,18 @@ def generate_latest(registry): 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:[{}]' + nh_sample_template += ',n_spans:[{}]' args.append(negative_spans) - nh_sample_template += ',negative_deltas:[{}]' + nh_sample_template += ',n_deltas:[{}]' args.append(negative_deltas) # If there are pos spans, append them and the pos spans 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(f'{pd}' for pd in s.native_histogram.pos_deltas) - nh_sample_template += ',positive_spans:[{}]' + nh_sample_template += ',p_spans:[{}]' args.append(positive_spans) - nh_sample_template += ',positive_deltas:[{}]' + nh_sample_template += ',p_deltas:[{}]' args.append(positive_deltas) # Add closing brace diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 3cd6d05f..451c7a11 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -103,7 +103,7 @@ def test_native_histogram(self) -> None: self.custom_collector(hfm) self.assertEqual(b"""# HELP nh nh # TYPE nh histogram -nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -113,7 +113,7 @@ def test_nh_histogram_with_exemplars(self) -> None: self.custom_collector(hfm) self.assertEqual(b"""# HELP nh nh # TYPE nh histogram -nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} # {trace_id="KOO5S4vxi0o"} 0.67 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789 +nh {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # {trace_id="KOO5S4vxi0o"} 0.67 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789 # EOF """, generate_latest(self.registry)) @@ -134,7 +134,7 @@ def test_nh_longer_spans(self) -> None: self.custom_collector(hfm) 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]} +nhsp {count:4,sum:6,schema:3,zero_threshold:2.938735877055719e-39,zero_count:1,p_spans:[0:1,7:1,4:1],p_deltas:[1,0,0]} # EOF """, generate_latest(self.registry)) @@ -144,7 +144,7 @@ def test_native_histogram_utf8(self) -> None: 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,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +{"native{histogram"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -154,7 +154,7 @@ def test_native_histogram_utf8_stress(self) -> None: 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,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +{"native{histogram", "xx{} # {}"=" EOF # {}}}"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -164,7 +164,7 @@ def test_native_histogram_with_labels(self) -> None: 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{baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +hist_w_labels{baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -174,7 +174,7 @@ def test_native_histogram_with_labels_utf8(self) -> None: 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", baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +{"hist.w.labels", baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF """, generate_latest(self.registry)) @@ -188,7 +188,7 @@ def test_native_histogram_with_classic_histogram(self) -> 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,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +hist_w_classic{foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,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 @@ -211,12 +211,12 @@ def test_native_plus_classic_histogram_two_labelsets(self) -> 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,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +hist_w_classic_two_sets{foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,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,negative_spans:[0:2,1:2],negative_deltas:[2,1,-2,3],positive_spans:[0:2,1:2],positive_deltas:[2,1,-3,3]} +hist_w_classic_two_sets{foo="baz"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,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 From db46de684567240a908afbf7e049b59d3fadc60d Mon Sep 17 00:00:00 2001 From: Arianna Vespri Date: Fri, 13 Jun 2025 17:02:35 +0200 Subject: [PATCH 14/14] Adapt nh with UTF-8 tests to new testing framework Signed-off-by: Arianna Vespri --- tests/openmetrics/test_exposition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 27af33b9..17584487 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -161,7 +161,7 @@ def test_native_histogram_utf8(self) -> None: # TYPE "native{histogram" histogram {"native{histogram"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF -""", generate_latest(self.registry)) +""", generate_latest(self.registry, ALLOWUTF8)) def test_native_histogram_utf8_stress(self) -> None: hfm = HistogramMetricFamily("native{histogram", "Is a basic example of a native histogram") @@ -171,7 +171,7 @@ def test_native_histogram_utf8_stress(self) -> None: # TYPE "native{histogram" histogram {"native{histogram", "xx{} # {}"=" EOF # {}}}"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF -""", generate_latest(self.registry)) +""", generate_latest(self.registry, ALLOWUTF8)) def test_native_histogram_with_labels(self) -> None: hfm = HistogramMetricFamily("hist_w_labels", "Is a basic example of a native histogram with labels") @@ -191,7 +191,7 @@ def test_native_histogram_with_labels_utf8(self) -> None: # TYPE "hist.w.labels" histogram {"hist.w.labels", baz="qux",foo="bar"} {count:24,sum:100,schema:0,zero_threshold:0.001,zero_count:4,n_spans:[0:2,1:2],n_deltas:[2,1,-2,3],p_spans:[0:2,1:2],p_deltas:[2,1,-3,3]} # EOF -""", generate_latest(self.registry)) +""", generate_latest(self.registry, ALLOWUTF8)) def test_native_histogram_with_classic_histogram(self) -> None: hfm = HistogramMetricFamily("hist_w_classic", "Is a basic example of a native histogram coexisting with a classic histogram")