Skip to content

Commit e325c66

Browse files
committed
Add support for parsing timestamps
Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent 53726b3 commit e325c66

File tree

3 files changed

+77
-32
lines changed

3 files changed

+77
-32
lines changed

prometheus_client/core.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,15 @@ def __init__(self, sec, nsec):
5454
def __str__(self):
5555
return "{0}.{1:09d}".format(self.sec, self.nsec)
5656

57+
def __repr__(self):
58+
return "Timestamp({0}, {1})".format(self.sec, self.nsec)
59+
5760
def __float__(self):
5861
return float(self.sec) + float(self.nsec) / 1e9
5962

63+
def __eq__(self, other):
64+
return self.sec == other.sec and self.nsec == other.nsec
65+
6066

6167
class CollectorRegistry(object):
6268
'''Metric collector registry.
@@ -221,6 +227,7 @@ def __repr__(self):
221227
return "Metric(%s, %s, %s, %s, %s)" % (self.name, self.documentation,
222228
self.type, self.unit, self.samples)
223229

230+
224231
class UntypedMetricFamily(Metric):
225232
'''A single untyped metric and its samples.
226233
For use by custom collectors.
@@ -235,13 +242,13 @@ def __init__(self, name, documentation, value=None, labels=None):
235242
if value is not None:
236243
self.add_metric([], value)
237244

238-
def add_metric(self, labels, value):
245+
def add_metric(self, labels, value, timestamp=None):
239246
'''Add a metric to the metric family.
240247
Args:
241248
labels: A list of label values
242249
value: The value of the metric.
243250
'''
244-
self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value))
251+
self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
245252

246253

247254
class CounterMetricFamily(Metric):
@@ -262,17 +269,17 @@ def __init__(self, name, documentation, value=None, labels=None, created=None):
262269
if value is not None:
263270
self.add_metric([], value, created)
264271

265-
def add_metric(self, labels, value, created=None):
272+
def add_metric(self, labels, value, created=None, timestamp=None):
266273
'''Add a metric to the metric family.
267274
268275
Args:
269276
labels: A list of label values
270277
value: The value of the metric
271278
created: Optional unix timestamp the child was created at.
272279
'''
273-
self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value))
280+
self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp))
274281
if created is not None:
275-
self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created))
282+
self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp))
276283

277284

278285
class GaugeMetricFamily(Metric):
@@ -290,14 +297,14 @@ def __init__(self, name, documentation, value=None, labels=None, unit=''):
290297
if value is not None:
291298
self.add_metric([], value)
292299

293-
def add_metric(self, labels, value):
300+
def add_metric(self, labels, value, timestamp=None):
294301
'''Add a metric to the metric family.
295302
296303
Args:
297304
labels: A list of label values
298305
value: A float
299306
'''
300-
self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value))
307+
self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
301308

302309

303310
class SummaryMetricFamily(Metric):
@@ -317,16 +324,16 @@ def __init__(self, name, documentation, count_value=None, sum_value=None, labels
317324
if count_value is not None:
318325
self.add_metric([], count_value, sum_value)
319326

320-
def add_metric(self, labels, count_value, sum_value):
327+
def add_metric(self, labels, count_value, sum_value, timestamp=None):
321328
'''Add a metric to the metric family.
322329
323330
Args:
324331
labels: A list of label values
325332
count_value: The count value of the metric.
326333
sum_value: The sum value of the metric.
327334
'''
328-
self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value))
329-
self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value))
335+
self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value, timestamp))
336+
self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
330337

331338

332339
class HistogramMetricFamily(Metric):
@@ -346,7 +353,7 @@ def __init__(self, name, documentation, buckets=None, sum_value=None, labels=Non
346353
if buckets is not None:
347354
self.add_metric([], buckets, sum_value)
348355

349-
def add_metric(self, labels, buckets, sum_value):
356+
def add_metric(self, labels, buckets, sum_value, timestamp=None):
350357
'''Add a metric to the metric family.
351358
352359
Args:
@@ -356,10 +363,10 @@ def add_metric(self, labels, buckets, sum_value):
356363
sum_value: The sum value of the metric.
357364
'''
358365
for bucket, value in buckets:
359-
self.samples.append(Sample(self.name + '_bucket', dict(list(zip(self._labelnames, labels)) + [('le', bucket)]), value))
366+
self.samples.append(Sample(self.name + '_bucket', dict(list(zip(self._labelnames, labels)) + [('le', bucket)]), value, timestamp))
360367
# +Inf is last and provides the count value.
361-
self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1]))
362-
self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value))
368+
self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp))
369+
self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
363370

364371

365372
class GaugeHistogramMetricFamily(Metric):
@@ -377,7 +384,7 @@ def __init__(self, name, documentation, buckets=None, labels=None, unit=''):
377384
if buckets is not None:
378385
self.add_metric([], buckets)
379386

380-
def add_metric(self, labels, buckets):
387+
def add_metric(self, labels, buckets, timestamp=None):
381388
'''Add a metric to the metric family.
382389
383390
Args:
@@ -389,7 +396,7 @@ def add_metric(self, labels, buckets):
389396
self.samples.append(Sample(
390397
self.name + '_bucket',
391398
dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
392-
value))
399+
value, timestamp))
393400

394401

395402
class InfoMetricFamily(Metric):
@@ -407,15 +414,15 @@ def __init__(self, name, documentation, value=None, labels=None):
407414
if value is not None:
408415
self.add_metric([], value)
409416

410-
def add_metric(self, labels, value):
417+
def add_metric(self, labels, value, timestamp=None):
411418
'''Add a metric to the metric family.
412419
413420
Args:
414421
labels: A list of label values
415422
value: A dict of labels
416423
'''
417424
self.samples.append(Sample(self.name + '_info',
418-
dict(dict(zip(self._labelnames, labels)), **value), 1))
425+
dict(dict(zip(self._labelnames, labels)), **value), 1, timestamp))
419426

420427

421428
class StateSetMetricFamily(Metric):
@@ -433,7 +440,7 @@ def __init__(self, name, documentation, value=None, labels=None):
433440
if value is not None:
434441
self.add_metric([], value)
435442

436-
def add_metric(self, labels, value):
443+
def add_metric(self, labels, value, timestamp=None):
437444
'''Add a metric to the metric family.
438445
439446
Args:
@@ -444,7 +451,7 @@ def add_metric(self, labels, value):
444451
for state, enabled in value.items():
445452
v = (1 if enabled else 0)
446453
self.samples.append(Sample(self.name,
447-
dict(zip(self._labelnames + (self.name,), labels + (state,))), v))
454+
dict(zip(self._labelnames + (self.name,), labels + (state,))), v, timestamp))
448455

449456

450457
class _MutexValue(object):

prometheus_client/openmetrics/parser.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def _parse_sample(text):
5252
labelname = []
5353
labelvalue = []
5454
value = []
55+
timestamp = []
5556
labels = {}
5657

5758
state = 'name'
@@ -115,20 +116,42 @@ def _parse_sample(text):
115116
else:
116117
raise ValueError("Invalid line: " + text)
117118
elif state == 'value':
118-
if char == ' ' or char == '#':
119-
# Timestamps and examplars are not supported, halt
120-
break
119+
if char == ' ':
120+
state = 'timestamp'
121121
else:
122122
value.append(char)
123+
elif state == 'timestamp':
124+
if char == ' ':
125+
# examplars are not supported, halt
126+
break
127+
else:
128+
timestamp.append(char)
129+
123130
if not value:
124131
raise ValueError("Invalid line: " + text)
132+
value = ''.join(value)
125133
val = None
126134
try:
127-
val = int(''.join(value))
135+
val = int(value)
128136
except ValueError:
129-
val = float(''.join(value))
137+
val = float(value)
138+
139+
ts = None
140+
timestamp = ''.join(timestamp)
141+
if timestamp:
142+
try:
143+
# Simple int.
144+
ts = core.Timestamp(int(timestamp), 0)
145+
except ValueError:
146+
try:
147+
# aaaa.bbbb. Nanosecond resolution supported.
148+
parts = timestamp.split('.', 1)
149+
ts = core.Timestamp(int(parts[0]), int(parts[1][:9].ljust(9, "0")))
150+
except ValueError:
151+
# Float.
152+
ts = float(timestamp)
130153

131-
return core.Sample(''.join(name), labels, val)
154+
return core.Sample(''.join(name), labels, val, ts)
132155

133156

134157
def text_fd_to_metric_families(fd):

tests/openmetrics/test_parser.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Metric,
1818
Sample,
1919
SummaryMetricFamily,
20+
Timestamp,
2021
)
2122
from prometheus_client.openmetrics.exposition import (
2223
generate_latest,
@@ -67,7 +68,7 @@ def test_unit_gauge(self):
6768
a_seconds 1
6869
# EOF
6970
""")
70-
self.assertEqual([GaugeMetricFamily("a_seconds", "help", value=1)], list(families))
71+
self.assertEqual([GaugeMetricFamily("a_seconds", "help", value=1, unit='seconds')], list(families))
7172

7273
def test_simple_summary(self):
7374
families = text_string_to_metric_families("""# TYPE a summary
@@ -254,18 +255,27 @@ def test_escaping(self):
254255
metric_family.add_metric(["b\\a\\z"], 2)
255256
self.assertEqual([metric_family], list(families))
256257

257-
def test_timestamps_discarded(self):
258+
def test_timestamps(self):
258259
families = text_string_to_metric_families("""# TYPE a counter
259260
# HELP a help
260-
a_total{foo="bar"} 1 000
261+
a_total{foo="1"} 1 000
262+
a_total{foo="2"} 1 0.0
263+
a_total{foo="3"} 1 1.1
264+
a_total{foo="4"} 1 12345678901234567890.1234567890
265+
a_total{foo="5"} 1 1.5e3
261266
# TYPE b counter
262267
# HELP b help
263-
b_total 2 1234567890
268+
b_total 2 1234567890
264269
# EOF
265270
""")
266271
a = CounterMetricFamily("a", "help", labels=["foo"])
267-
a.add_metric(["bar"], 1)
268-
b = CounterMetricFamily("b", "help", value=2)
272+
a.add_metric(["1"], 1, timestamp=Timestamp(0, 0))
273+
a.add_metric(["2"], 1, timestamp=Timestamp(0, 0))
274+
a.add_metric(["3"], 1, timestamp=Timestamp(1, 100000000))
275+
a.add_metric(["4"], 1, timestamp=Timestamp(12345678901234567890, 123456789))
276+
a.add_metric(["5"], 1, timestamp=1500.0)
277+
b = CounterMetricFamily("b", "help")
278+
b.add_metric([], 2, timestamp=Timestamp(1234567890, 0))
269279
self.assertEqual([a, b], list(families))
270280

271281
@unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
@@ -363,6 +373,11 @@ def test_invalid_input(self):
363373
('0a 1\n# EOF\n'),
364374
('a.b 1\n# EOF\n'),
365375
('a-b 1\n# EOF\n'),
376+
# Bad timestamp.
377+
('a 1 1 \n# EOF\n'),
378+
('a 1 z\n# EOF\n'),
379+
('a 1 1z\n# EOF\n'),
380+
('a 1 1.1.1\n# EOF\n'),
366381
]:
367382
with self.assertRaises(ValueError):
368383
list(text_string_to_metric_families(case))

0 commit comments

Comments
 (0)