-
Notifications
You must be signed in to change notification settings - Fork 811
OM text exposition for NH #1087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
1940648
c4d24f9
928b680
0e54c44
00427cc
28028f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -30,24 +30,24 @@ 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) | ||||||||
if s.labels: | ||||||||
labelstr += ', ' | ||||||||
else: | ||||||||
labelstr = '' | ||||||||
|
||||||||
if s.labels: | ||||||||
items = sorted(s.labels.items()) | ||||||||
items = s.labels.items() | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OM does not require ordering of labels, still I wonder why change this, doesn't seem part of the scope of the PR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just wanted to use the exact same strings I used in the parser tests (there the labels are not sorted). Since I knew that ordering is not required, I removed the sorting, and once I saw that the pre-existing exposition tests don’t fail, I simply thought I’d kill two birds with one stone, i.e. keeping the same test strings and removing the sorting performance overhead (which I admit I haven’t quantified btw). Having said that, I realize this change is out of the scope of this PR, so of course I will revert it, if you wish. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep the sorting. I'd have to go and make sure that it doesn't cause an issue anywhere not tested. Probably ok, but for example the sorting is important for multiprocess as that is how we match keys. |
||||||||
labelstr += ','.join( | ||||||||
['{}="{}"'.format( | ||||||||
escape_label_name(k), _escape(v)) | ||||||||
for k, v in items]) | ||||||||
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") | ||||||||
|
@@ -67,24 +67,87 @@ 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 = '' | ||||||||
pos = False | ||||||||
neg = False | ||||||||
|
||||||||
if s.native_histogram: | ||||||||
# 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 | ||||||||
Comment on lines
+96
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: these just shadow |
||||||||
|
||||||||
# 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: | ||||||||
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) | ||||||||
neg = True | ||||||||
|
||||||||
# Append pos deltas if pos spans were added | ||||||||
if pos: | ||||||||
nh_sample_template += ',positive_deltas:[{}]' | ||||||||
args.append(positive_deltas) | ||||||||
|
||||||||
# Append neg deltas if neg spans were added | ||||||||
if neg: | ||||||||
nh_sample_template += ',negative_deltas:[{}]' | ||||||||
args.append(negative_deltas) | ||||||||
Comment on lines
+116
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't these just be included in the already present if statements above and then there would be no need for the |
||||||||
|
||||||||
# Add closing brace | ||||||||
nh_sample_template += '}}' | ||||||||
|
||||||||
# Format the template with the args | ||||||||
native_histogram = nh_sample_template.format(*args) | ||||||||
|
||||||||
value = '' | ||||||||
if s.value is not None or not s.native_histogram: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should be enough to write
Suggested change
At least that's what we do in Prometheus. Type checks should prevent having None in the value. |
||||||||
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 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this must be placed immediately after There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd actually say we should just store native histogram into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Storing native histograms in I’ll be happy to open a new PR on the parser first in order to support exemplars and address your change requests, but I can’t do it without reaching a consensus on how class
and change things accordingly elsewhere. Which one shall I opt for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Go we opted for separate sample types for floats , and native histograms (actually there's two native histogram sample types in Go, but I digress) due to performance issues negatively affecting the float sample values. I don't know how much of a concern it is in Python, I suspect it's not a big issue. So my vote is: Let's try to implement option 2 in a separate PR . I wish we could unify the types in Go as well... I don't understand option 3, my comments are about the printing order , not the order in the tuple which I don't care about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't thinking about changing types elsewhere, just in this small bit of code when doing the formatting. E.g. just above this line do: value = ''
if s.native_histogram:
value = native_histogram
elif s.value is not None
value = floatToGoString(s.value) |
||||||||
)) | ||||||||
else: | ||||||||
output.append('{} {}{}{}\n'.format( | ||||||||
output.append('{} {}{}{}{}\n'.format( | ||||||||
labelstr, | ||||||||
floatToGoString(s.value), | ||||||||
value, | ||||||||
timestamp, | ||||||||
exemplarstr, | ||||||||
native_histogram | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this must be placed immediately after |
||||||||
)) | ||||||||
except Exception as exception: | ||||||||
exception.args = (exception.args or ('',)) + (metric,) | ||||||||
|
@@ -115,3 +178,6 @@ 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'\"') | ||||||||
|
||||||||
|
||||||||
|
||||||||
Comment on lines
+181
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,7 +5,8 @@ | |||||
CollectorRegistry, Counter, Enum, Gauge, Histogram, Info, Metric, Summary, | ||||||
) | ||||||
from prometheus_client.core import ( | ||||||
Exemplar, GaugeHistogramMetricFamily, Timestamp, | ||||||
BucketSpan, Exemplar, GaugeHistogramMetricFamily, HistogramMetricFamily, | ||||||
NativeHistogram, Timestamp, | ||||||
) | ||||||
from prometheus_client.openmetrics.exposition import generate_latest | ||||||
|
||||||
|
@@ -94,6 +95,125 @@ def test_histogram(self): | |||||
# EOF | ||||||
""", generate_latest(self.registry)) | ||||||
|
||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please also add test for exemplars (note that native histograms may have multiple exemplars) see example in https://github.com/prometheus/proposals/blob/main/proposals/2024-01-29_native_histograms_text_format.md |
||||||
def test_native_histogram(self): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
To get type checks. Also for all the other tests here. |
||||||
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))) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type annotation of add_sample says The type annotation of add_sample says
Suggested change
The reason why |
||||||
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]} | ||||||
# 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_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) | ||||||
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) | ||||||
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): | ||||||
s = Histogram('hh', 'A histogram', buckets=[-1, -0.5, 0, 0.5, 1], registry=self.registry) | ||||||
s.observe(-0.5) | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove extra spaces from end of lines. Also reduces the diff.