Skip to content

Commit f48aea4

Browse files
vesaricsmarchbanks
andauthored
OM text exposition for NH (#1087)
* Start implement OM text exposition for nh, add first no obs test * Correct template for nh sample spans, add test * Correct templating and appending for deltas, add longer spans test * Add tests for nh with labels, remove labels sorting * Break down logic classic vs nh samples, add tests for classic-native histograms cohabitation * Move classic sample logic back to where it belongs * Assign nh to value, correct nil values in tests, clean up white spaces * Add logic for exposing nh exemplars * Please linters * Assign nh_exemplars to exemplarstr * Add Any type to metric_family in OM exposition test * Change printing order of nh spans and deltas according to OM 2.0 proposal * Shorten name of spans and deltas as per OM 2.0 proposal * Adapt nh with UTF-8 tests to new testing framework * Update prometheus_client/openmetrics/exposition.py * Update prometheus_client/openmetrics/exposition.py * Eliminate erroneous abbreviation for spans and deltas --------- Signed-off-by: Arianna Vespri <[email protected]> Signed-off-by: Arianna Vespri <[email protected]> Co-authored-by: Chris Marchbanks <[email protected]>
1 parent 7368028 commit f48aea4

File tree

3 files changed

+247
-57
lines changed

3 files changed

+247
-57
lines changed

prometheus_client/openmetrics/exposition.py

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ def _is_valid_exemplar_metric(metric, sample):
3030
return False
3131

3232

33+
def _compose_exemplar_string(metric, sample, exemplar):
34+
"""Constructs an exemplar string."""
35+
if not _is_valid_exemplar_metric(metric, sample):
36+
raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter")
37+
labels = '{{{0}}}'.format(','.join(
38+
['{}="{}"'.format(
39+
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
40+
for k, v in sorted(exemplar.labels.items())]))
41+
if exemplar.timestamp is not None:
42+
exemplarstr = ' # {} {} {}'.format(
43+
labels,
44+
floatToGoString(exemplar.value),
45+
exemplar.timestamp,
46+
)
47+
else:
48+
exemplarstr = ' # {} {}'.format(
49+
labels,
50+
floatToGoString(exemplar.value),
51+
)
52+
53+
return exemplarstr
54+
55+
3356
def generate_latest(registry, escaping=UNDERSCORES):
3457
'''Returns the metrics from the registry in latest text format as a string.'''
3558
output = []
@@ -58,44 +81,80 @@ def generate_latest(registry, escaping=UNDERSCORES):
5881
for k, v in items])
5982
if labelstr:
6083
labelstr = "{" + labelstr + "}"
61-
6284
if s.exemplar:
63-
if not _is_valid_exemplar_metric(metric, s):
64-
raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter")
65-
labels = '{{{0}}}'.format(','.join(
66-
['{}="{}"'.format(
67-
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
68-
for k, v in sorted(s.exemplar.labels.items())]))
69-
if s.exemplar.timestamp is not None:
70-
exemplarstr = ' # {} {} {}'.format(
71-
labels,
72-
floatToGoString(s.exemplar.value),
73-
s.exemplar.timestamp,
74-
)
75-
else:
76-
exemplarstr = ' # {} {}'.format(
77-
labels,
78-
floatToGoString(s.exemplar.value),
79-
)
85+
exemplarstr = _compose_exemplar_string(metric, s, s.exemplar)
8086
else:
8187
exemplarstr = ''
8288
timestamp = ''
8389
if s.timestamp is not None:
8490
timestamp = f' {s.timestamp}'
91+
92+
native_histogram = ''
93+
negative_spans = ''
94+
negative_deltas = ''
95+
positive_spans = ''
96+
positive_deltas = ''
97+
98+
if s.native_histogram:
99+
# Initialize basic nh template
100+
nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}'
101+
102+
args = [
103+
s.native_histogram.count_value,
104+
s.native_histogram.sum_value,
105+
s.native_histogram.schema,
106+
s.native_histogram.zero_threshold,
107+
s.native_histogram.zero_count,
108+
]
109+
110+
# If there are neg spans, append them and the neg deltas to the template and args
111+
if s.native_histogram.neg_spans:
112+
negative_spans = ','.join([f'{ns[0]}:{ns[1]}' for ns in s.native_histogram.neg_spans])
113+
negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas)
114+
nh_sample_template += ',negative_spans:[{}]'
115+
args.append(negative_spans)
116+
nh_sample_template += ',negative_deltas:[{}]'
117+
args.append(negative_deltas)
118+
119+
# If there are pos spans, append them and the pos spans to the template and args
120+
if s.native_histogram.pos_spans:
121+
positive_spans = ','.join([f'{ps[0]}:{ps[1]}' for ps in s.native_histogram.pos_spans])
122+
positive_deltas = ','.join(f'{pd}' for pd in s.native_histogram.pos_deltas)
123+
nh_sample_template += ',positive_spans:[{}]'
124+
args.append(positive_spans)
125+
nh_sample_template += ',positive_deltas:[{}]'
126+
args.append(positive_deltas)
127+
128+
# Add closing brace
129+
nh_sample_template += '}}'
130+
131+
# Format the template with the args
132+
native_histogram = nh_sample_template.format(*args)
133+
134+
if s.native_histogram.nh_exemplars:
135+
for nh_ex in s.native_histogram.nh_exemplars:
136+
nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex)
137+
exemplarstr += nh_exemplarstr
138+
139+
value = ''
140+
if s.native_histogram:
141+
value = native_histogram
142+
elif s.value is not None:
143+
value = floatToGoString(s.value)
85144
if (escaping != ALLOWUTF8) or _is_valid_legacy_metric_name(s.name):
86145
output.append('{}{} {}{}{}\n'.format(
87146
_escape(s.name, escaping, _is_legacy_labelname_rune),
88147
labelstr,
89-
floatToGoString(s.value),
148+
value,
90149
timestamp,
91-
exemplarstr,
150+
exemplarstr
92151
))
93152
else:
94153
output.append('{} {}{}{}\n'.format(
95154
labelstr,
96-
floatToGoString(s.value),
155+
value,
97156
timestamp,
98-
exemplarstr,
157+
exemplarstr
99158
))
100159
except Exception as exception:
101160
exception.args = (exception.args or ('',)) + (metric,)

prometheus_client/samples.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ class BucketSpan(NamedTuple):
4040
length: int
4141

4242

43+
# Timestamp and exemplar are optional.
44+
# Value can be an int or a float.
45+
# Timestamp can be a float containing a unixtime in seconds,
46+
# a Timestamp object, or None.
47+
# Exemplar can be an Exemplar object, or None.
48+
class Exemplar(NamedTuple):
49+
labels: Dict[str, str]
50+
value: float
51+
timestamp: Optional[Union[float, Timestamp]] = None
52+
53+
4354
# NativeHistogram is experimental and subject to change at any time.
4455
class NativeHistogram(NamedTuple):
4556
count_value: float
@@ -51,17 +62,7 @@ class NativeHistogram(NamedTuple):
5162
neg_spans: Optional[Sequence[BucketSpan]] = None
5263
pos_deltas: Optional[Sequence[int]] = None
5364
neg_deltas: Optional[Sequence[int]] = None
54-
55-
56-
# Timestamp and exemplar are optional.
57-
# Value can be an int or a float.
58-
# Timestamp can be a float containing a unixtime in seconds,
59-
# a Timestamp object, or None.
60-
# Exemplar can be an Exemplar object, or None.
61-
class Exemplar(NamedTuple):
62-
labels: Dict[str, str]
63-
value: float
64-
timestamp: Optional[Union[float, Timestamp]] = None
65+
nh_exemplars: Optional[Sequence[Exemplar]] = None
6566

6667

6768
class Sample(NamedTuple):

0 commit comments

Comments
 (0)