10000 Add support for parsing exemplars · sxlstevengit/client_python@d619202 · GitHub
[go: up one dir, main page]

Skip to content

Commit d619202

Browse files
committed
Add support for parsing exemplars
Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent 0f668b6 commit d619202

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

prometheus_client/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def __float__(self):
6262
return float(self.sec) + float(self.nsec) / 1e9
6363

6464
def __eq__(self, other):
65-
return self.sec == other.sec and self.nsec == other.nsec
65+
return type(self) == type(other) and self.sec == other.sec and self.nsec == other.nsec
6666

6767

6868
Exemplar = namedtuple('Exemplar', ['labels', 'value', 'timestamp'])

prometheus_client/openmetrics/parser.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def _unescape_help(text):
4949

5050
def _parse_value(value):
5151
value = ''.join(value)
52+
if value != value.strip():
53+
raise ValueError("Invalid value: {0!r}".format(value))
5254
try:
5355
return int(value)
5456
except ValueError:
@@ -59,6 +61,8 @@ def _parse_timestamp(timestamp):
5961
timestamp = ''.join(timestamp)
6062
if not timestamp:
6163
return None
64+
if timestamp != timestamp.strip():
65+
raise ValueError("Invalid timestamp: {0!r}".format(timestamp))
6266
try:
6367
# Simple int.
6468
return core.Timestamp(int(timestamp), 0)
@@ -138,11 +142,13 @@ def _parse_sample(text):
138142
value = []
139143
timestamp = []
140144
labels = {}
145+
exemplar_value = []
146+
exemplar_timestamp = []
147+
exemplar_labels = None
141148

142149
state = 'name'
143150

144151
it = iter(text)
145-
146152
for char in it:
147153
if state == 'name':
148154
if char == '{':
@@ -159,23 +165,61 @@ def _parse_sample(text):
159165
else:
160166
value.append(char)
161167
elif state == 'timestamp':
162-
if char == ' ':
163-
# examplars are not supported, halt
164-
break
168+
if char == '#' and not timestamp:
169+
state = 'exemplarspace'
170+
elif char == ' ':
171+
state = 'exemplarhash'
165172
else:
166173
timestamp.append(char)
174+
elif state == 'exemplarhash':
175+
if char == '#':
176+
state = 'exemplarspace'
177+
else:
178+
raise ValueError("Invalid line: " + text)
179+
elif state == 'exemplarspace':
180+
if char == ' ':
181+
state = 'exemplarstartoflabels'
182+
else:
183+
raise ValueError("Invalid line: " + text)
184+
elif state == 'exemplarstartoflabels':
185+
if char == '{':
186+
exemplar_labels = _parse_labels(it, text)
187+
# Space has already been parsed.
188+
state = 'exemplarvalue'
189+
else:
190+
raise ValueError("Invalid line: " + text)
191+
elif state == 'exemplarvalue':
192+
if char == ' ':
193+
state = 'exemplartimestamp'
194+
else:
195+
exemplar_value.append(char)
196+
elif state == 'exemplartimestamp':
197+
exemplar_timestamp.append(char)
167198

168199
# Trailing space after value.
169200
if state == 'timestamp' and not timestamp:
170201
raise ValueError("Invalid line: " + text)
171202

203+
# Trailing space after value.
204+
if state == 'exemplartimestamp' and not exemplar_timestamp:
205+
raise ValueError("Invalid line: " + text)
206+
207+
# Incomplete exemplar.
208+
if state in ['exemplarhash', 'exemplarspace', 'exemplarstartoflabels']:
209+
raise ValueError("Invalid line: " + text)
210+
172211
if not value:
173212
raise ValueError("Invalid line: " + text)
174213
value = ''.join(value)
175214
val = _parse_value(value)
176215
ts = _parse_timestamp(timestamp)
216+
exemplar = None
217+
if exemplar_labels is not None:
218+
exemplar = core.Exemplar(exemplar_labels,
219+
_parse_value(exemplar_value),
220+
_parse_timestamp(exemplar_timestamp))
177221

178-
return core.Sample(''.join(name), labels, val, ts)
222+
return core.Sample(''.join(name), labels, val, ts, exemplar)
179223

180224

181225
def text_fd_to_metric_families(fd):
@@ -206,6 +250,7 @@ def build_metric(name, documentation, typ, unit, samples):
206250
# TODO: Info and stateset can't have units
207251
# TODO: check samples are appropriately grouped and ordered
208252
# TODO: check for metadata in middle of samples
253+
# TODO: Check histogram bucket rules being followed
209254
metric.samples = samples
210255
return metric
211256

tests/openmetrics/test_parser.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from prometheus_client.core import (
1313
CollectorRegistry,
1414
CounterMetricFamily,
15+
Exemplar,
1516
GaugeMetricFamily,
1617
HistogramMetricFamily,
1718
Metric,
@@ -105,6 +106,20 @@ def test_simple_histogram(self):
105106
""")
106107
self.assertEqual([HistogramMetricFamily("a", "help", sum_value=2, buckets=[("1", 0.0), ("+Inf", 3.0)])], list(families))
107108

109+
def test_histogram_exemplars(self):
110+
families = text_string_to_metric_families("""# TYPE a histogram
111+
# HELP a help
112+
a_bucket{le="1"} 0 # {a="b"} 0.5
113+
a_bucket{le="2"} 2 123 # {a="c"} 0.5
114+
a_bucket{le="+Inf"} 3 # {a="d"} 4 123
115+
# EOF
116+
""")
117+
hfm = HistogramMetricFamily("a", "help")
118+
hfm.add_sample("a_bucket", {"le": "1"}, 0.0, None, Exemplar({"a": "b"}, 0.5))
119+
hfm.add_sample("a_bucket", {"le": "2"}, 2.0, Timestamp(123, 0), Exemplar({"a": "c"}, 0.5)),
120+
hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, None, Exemplar({"a": "d"}, 4, Timestamp(123, 0)))
121+
self.assertEqual([hfm], list(families))
122+
108123
def test_no_metadata(self):
109124
families = text_string_to_metric_families("""a 1
110125
# EOF
@@ -376,11 +391,19 @@ def test_invalid_input(self):
376391
# Bad value.
377392
('a a\n# EOF\n'),
378393
('a 1\n# EOF\n'),
394+
('a 1\t\n# EOF\n'),
379395
('a 1 \n# EOF\n'),
380396
# Bad timestamp.
381397
('a 1 z\n# EOF\n'),
382398
('a 1 1z\n# EOF\n'),
383399
('a 1 1.1.1\n# EOF\n'),
400+
# Bad exemplars.
401+
('# TYPE a histogram\na_bucket{le="+Inf"} 1 #\n# EOF\n'),
402+
('# TYPE a histogram\na_bucket{le="+Inf"} 1# {} 1\n# EOF\n'),
403+
('# TYPE a histogram\na_bucket{le="+Inf"} 1 #{} 1\n# EOF\n'),
404+
('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {}1\n# EOF\n'),
405+
('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {} 1 \n# EOF\n'),
406+
('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {} 1 1 \n# EOF\n'),
384407
]:
385408
with self.assertRaises(ValueError):
386409
list(text_string_to_metric_families(case))

0 commit comments

Comments
 (0)
0