8000 Check samples are grouped, untyped isn't used, and for invalid/missin… · danarwix/client_python@c7bfebc · GitHub
[go: up one dir, main page]

Skip to content

Commit c7bfebc

Browse files
committed
Check samples are grouped, untyped isn't used, and for invalid/missing le/quantile values.
Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent afe09a3 commit c7bfebc

File tree

2 files changed

+70
-13
lines changed

2 files changed

+70
-13
lines changed

prometheus_client/openmetrics/parser.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,26 @@ def _parse_sample(text):
228228
_parse_timestamp(exemplar_timestamp))
229229

230230
return core.Sample(''.join(name), labels, val, ts, exemplar)
231-
231+
232+
233+
def _group_for_sample(sample, name, typ):
234+
if typ == 'info':
235+
# We can't distinguish between groups for info metrics.
236+
return {}
237+
if typ == 'summary' and sample.name == name:
238+
d = sample.labels.copy()
239+
del d['quantile']
240+
return d
241+
if typ == 'stateset':
242+
d = sample.labels.copy()
243+
del d[name]
244+
return d
245+
if typ in ['histogram', 'gaugehistogram'] and sample.name == name + '_bucket':
246+
d = sample.labels.copy()
247+
del d['le']
248+
return d
249+
return sample.labels
250+
232251

233252
def text_fd_to_metric_families(fd):
234253
"""Parse Prometheus text format from a file descriptor.
@@ -240,9 +259,11 @@ def text_fd_to_metric_families(fd):
240259
Yields core.Metric's.
241260
"""
242261
name = ''
243-
documentation = ''
244-
typ = 'untyped'
245-
unit = ''
262+
documentation = None
263+
typ = None
264+
unit = None
265+
group = None
266+
seen_groups = set()
246267
samples = []
247268
allowed_names = []
248269
eof = False
@@ -253,7 +274,7 @@ def build_metric(name, documentation, typ, unit, samples):
253274
raise ValueError("Duplicate metric: " + name)
254275
seen_metrics.add(name)
255276
if typ is None:
256-
typ = 'untyped'
277+
typ = 'unknown'
257278
if documentation is None:
258279
documentation = ''
259280
if unit is None:
@@ -264,7 +285,7 @@ def build_metric(name, documentation, typ, unit, samples):
264285
raise ValueError("Units not allowed for this metric type: " + name)
265286
metric = core.Metric(name, documentation, typ, unit)
266287
# TODO: check labelvalues are valid utf8
267-
# TODO: check samples are appropriately grouped and ordered
288+
# TODO: check samples are appropriately ordered
268289
# TODO: Check histogram bucket rules being followed
269290
# TODO: Check for dupliate samples
270291
# TODO: Check for decresing timestamps
@@ -294,6 +315,8 @@ def build_metric(name, documentation, typ, unit, samples):
294315
unit = None
295316
typ = None
296317
documentation = None
318+
group = None
319+
seen_groups = set()
297320
samples = []
298321
allowed_names = [parts[2]]
299322

@@ -308,6 +331,8 @@ def build_metric(name, documentation, typ, unit, samples):
308331
if typ is not None:
309332
raise ValueError("More than one TYPE for metric: " + line)
310333
typ = parts[3]
334+
if typ == 'untyped':
335+
raise ValueError("Invalid TYPE for metric: " + line)
311336
allowed_names = {
312337
'counter': ['_total', '_created'],
313338
'summary': ['_count', '_sum', '', '_created'],
@@ -327,16 +352,33 @@ def build_metric(name, documentation, typ, unit, samples):
327352
if sample.name not in allowed_names:
328353
if name != '':
329354
yield build_metric(name, documentation, typ, unit, samples)
330-
# Start an untyped metric.
355+
# Start an unknown metric.
331356
name = sample.name
332-
documentation = ''
333-
unit = ''
334-
typ = 'untyped'
357+
documentation = None
358+
unit = None
359+
typ = 'unknown'
335360
samples = [sample]
361+
group = None
362+
seen_groups = set()
336363
allowed_names = [sample.name]
337364
else:
338365
samples.append(sample)
339366

367+
if typ == 'stateset' and name not in sample.labels:
368+
raise ValueError("Stateset missing label: " + line)
369+
if (typ in ['histogram', 'gaugehistogram'] and name + '_bucket' == sample.name
370+
and float(sample.labels.get('le', -1)) < 0):
371+
raise ValueError("Invalid le label: " + line)
372+
if (typ == 'summary' and name == sample.name
373+
and not (0 <= float(sample.labels.get('quantile', -1)) <= 1)):
374+
raise ValueError("Invalid quantile label: " + line)
375+
376+
g = tuple(sorted(_group_for_sample(sample, name, typ).items()))
377+
if group is not None and g != group and g in seen_groups:
378+
raise ValueError("Invalid metric group ordering: " + line)
379+
group = g
380+
seen_groups.add(g)
381+
340382
if typ == 'stateset' and sample.value not in [0, 1]:
341383
raise ValueError("Stateset samples can only have values zero and one: " + line)
342384
if typ == 'info' and sample.value != 1:

tests/openmetrics/test_parser.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def test_empty_metadata(self):
192192
def test_untyped(self):
193193
# https://github.com/prometheus/client_python/issues/79
194194
families = text_string_to_metric_families("""# HELP redis_connected_clients Redis connected clients
195-
# TYPE redis_connected_clients untyped
195+
# TYPE redis_connected_clients unknown
196196
redis_connected_clients{instance="rough-snowflake-web",port="6380"} 10.0
197197
redis_connected_clients{instance="rough-snowflake-web",port="6381"} 12.0
198198
# EOF
@@ -446,6 +446,7 @@ def test_invalid_input(self):
446446
('# TYPE a meh\n# EOF\n'),
447447
('# TYPE a meh \n# EOF\n'),
448448
('# TYPE a gauge \n# EOF\n'),
449+
('# TYPE a untyped\n# EOF\n'),
449450
# Bad UNIT.
450451
('# UNIT\n# EOF\n'),
451452
('# UNIT \n# EOF\n'),
@@ -498,14 +499,28 @@ def test_invalid_input(self):
498499
('# TYPE a info\na 2\n# EOF\n'),
499500
('# TYPE a stateset\na 2.0\n# EOF\n'),
500501
('# TYPE a info\na 2.0\n# EOF\n'),
502+
# Missing or invalid labels for a type.
503+
('# TYPE a summary\na 0\n# EOF\n'),
504+
('# TYPE a summary\na{quantile="-1"} 0\n# EOF\n'),
505+
('# TYPE a summary\na{quantile="foo"} 0\n# EOF\n'),
506+
('# TYPE a summary\na{quantile="1.01"} 0\n# EOF\n'),
507+
('# TYPE a summary\na{quantile="NaN"} 0\n# EOF\n'),
508+
('# TYPE a histogram\na_bucket 0\n# EOF\n'),
509+
('# TYPE a gaugehistogram\na_bucket 0\n# EOF\n'),
510+
('# TYPE a stateset\na 0\n# EOF\n'),
501511
# Bad counter values.
502512
('# TYPE a counter\na_total NaN\n# EOF\n'),
503513
('# TYPE a histogram\na_sum NaN\n# EOF\n'),
504514
('# TYPE a histogram\na_count NaN\n# EOF\n'),
505-
('# TYPE a histogram\na_bucket NaN\n# EOF\n'),
506-
('# TYPE a gaugehistogram\na_bucket NaN\n# EOF\n'),
515+
('# TYPE a histogram\na_bucket{le="+Inf"} NaN\n# EOF\n'),
516+
('# TYPE a gaugehistogram\na_bucket{le="+Inf"} NaN\n# EOF\n'),
507517
('# TYPE a summary\na_sum NaN\n# EOF\n'),
508518
('# TYPE a summary\na_count NaN\n# EOF\n'),
519+
# Bad grouping.
520+
('# TYPE a histogram\na_sum{a="1"} 0\na_sum{a="2"} 0\na_count{a="1"} 0\n# EOF\n'),
521+
('# TYPE a histogram\na_bucket{a="1",le="1"} 0\na_bucket{a="2",le="+Inf""} 0\na_bucket{a="1",le="+Inf"} 0\n# EOF\n'),
522+
('# TYPE a gaugehistogram\na_gsum{a="1"} 0\na_gsum{a="2"} 0\na_gcount{a="1"} 0\n# EOF\n'),
523+
('# TYPE a summary\nquantile{quantile="0"} 0\na_sum{a="1"} 0\nquantile{quantile="1"} 0\n# EOF\n'),
509524
]:
510525
with self.assertRaises(ValueError):
511526
list(text_string_to_metric_families(case))

0 commit comments

Comments
 (0)
0