Skip to content

Commit 60097b9

Browse files
committed
moar tests and fixes
1 parent 630fb65 commit 60097b9

File tree

2 files changed

+159
-2
lines changed

2 files changed

+159
-2
lines changed

prometheus_client/openmetrics/exposition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def generate_latest(registry, escaping):
3838
try:
3939
mname = metric.name
4040
output.append('# HELP {} {}\n'.format(
41-
escape_metric_name(mname, escaping), _escape(metric.documentation, escaping, False)))
41+
escape_metric_name(mname, escaping), _escape(metric.documentation, ALLOWUTF8, False)))
4242
output.append(f'# TYPE {escape_metric_name(mname, escaping)} {metric.type}\n')
4343
if metric.unit:
4444
output.append(f'# UNIT {escape_metric_name(mname, escaping)} {metric.unit}\n')

tests/openmetrics/test_exposition.py

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import time
22
import unittest
33

4+
import pytest
5+
46
from prometheus_client import (
57
CollectorRegistry, Counter, Enum, Gauge, Histogram, Info, Metric, Summary,
68
)
79
from prometheus_client.core import (
810
Exemplar, GaugeHistogramMetricFamily, Timestamp,
911
)
10-
from prometheus_client.openmetrics.exposition import ALLOWUTF8, generate_latest
12+
from prometheus_client.openmetrics.exposition import (
13+
ALLOWUTF8, DOTS, escape_label_name, escape_metric_name, generate_latest,
14+
UNDERSCORES, VALUES,
15+
)
1116

1217

1318
class TestGenerateText(unittest.TestCase):
@@ -39,6 +44,16 @@ def test_counter_utf8(self):
3944
c.inc()
4045
self.assertEqual(b'# HELP "cc.with.dots" A counter\n# TYPE "cc.with.dots" counter\n{"cc.with.dots_total"} 1.0\n{"cc.with.dots_created"} 123.456\n# EOF\n',
4146
generate_latest(self.registry, ALLOWUTF8))
47+
48+
def test_counter_utf8_escaped_underscores(self):
49+
c = Counter('utf8.cc', 'A counter', registry=self.registry)
50+
c.inc()
51+
assert b"""# HELP utf8_cc A counter
52+
# TYPE utf8_cc counter
53+
utf8_cc_total 1.0
54+
utf8_cc_created 123.456
55+
# EOF
56+
""" == generate_latest(self.registry, UNDERSCORES)
4257

4358
def test_counter_total(self):
4459
c = Counter('cc_total', 'A counter', registry=self.registry)
@@ -282,5 +297,147 @@ def collect(self):
282297
""", generate_latest(self.registry, ALLOWUTF8))
283298

284299

300+
@pytest.mark.parametrize("scenario", [
301+
{
302+
"name": "empty string",
303+
"input": "",
304+
"expectedUnderscores": "",
305+
"expectedDots": "",
306+
"expectedValue": "",
307+
},
308+
{
309+
"name": "legacy valid metric name",
310+
"input": "no:escaping_required",
311+
"expectedUnderscores": "no:escaping_required",
312+
"expectedDots": "no:escaping__required",
313+
"expectedValue": "no:escaping_required",
314+
},
315+
{
316+
"name": "metric name with dots",
317+
"input": "mysystem.prod.west.cpu.load",
318+
"expectedUnderscores": "mysystem_prod_west_cpu_load",
319+
"expectedDots": "mysystem_dot_prod_dot_west_dot_cpu_dot_load",
320+
"expectedValue": "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load",
321+
},
322+
{
323+
"name": "metric name with dots and underscore",
324+
"input": "mysystem.prod.west.cpu.load_total",
325+
"expectedUnderscores": "mysystem_prod_west_cpu_load_total",
326+
"expectedDots": "mysystem_dot_prod_dot_west_dot_cpu_dot_load__total",
327+
"expectedValue": "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load__total",
328+
},
329+
{
330+
"name": "metric name with dots and colon",
331+
"input": "http.status:sum",
332+
"expectedUnderscores": "http_status:sum",
333+
"expectedDots": "http_dot_status:sum",
334+
"expectedValue": "U__http_2e_status:sum",
335+
},
336+
{
337+
"name": "metric name with spaces and emoji",
338+
"input": "label with 😱",
339+
"expectedUnderscores": "label_with__",
340+
"expectedDots": "label__with____",
341+
"expectedValue": "U__label_20_with_20__1f631_",
342+
},
343+
{
344+
"name": "metric name with unicode characters > 0x100",
345+
"input": "花火",
346+
"expectedUnderscores": "__",
347+
"expectedDots": "____",
348+
"expectedValue": "U___82b1__706b_",
349+
},
350+
{
351+
"name": "metric name with spaces and edge-case value",
352+
"input": "label with \u0100",
353+
"expectedUnderscores": "label_with__",
354+
"expectedDots": "label__with____",
355+
"expectedValue": "U__label_20_with_20__100_",
356+
},
357+
])
358+
def test_escape_metric_name(scenario):
359+
input = scenario["input"]
360+
361+
got = escape_metric_name(input, UNDERSCORES)
362+
assert got == scenario["expectedUnderscores"], f"[{scenario['name']}] Underscore escaping failed"
363+
364+
got = escape_metric_name(input, DOTS)
365+
assert got == scenario["expectedDots"], f"[{scenario['name']}] Dots escaping failed"
366+
367+
got = escape_metric_name(input, VALUES)
368+
assert got == scenario["expectedValue"], f"[{scenario['name']}] Value encoding failed"
369+
370+
371+
@pytest.mark.parametrize("scenario", [
372+
{
373+
"name": "empty string",
374+
"input": "",
375+
"expectedUnderscores": "",
376+
"expectedDots": "",
377+
"expectedValue": "",
378+
},
379+
{
380+
"name": "legacy valid label name",
381+
"input": "no_escaping_required",
382+
"expectedUnderscores": "no_escaping_required",
383+
"expectedDots": "no__escaping__required",
384+
"expectedValue": "no_escaping_required",
385+
},
386+
{
387+
"name": "label name with dots",
388+
"input": "mysystem.prod.west.cpu.load",
389+
"expectedUnderscores": "mysystem_prod_west_cpu_load",
390+
"expectedDots": "mysystem_dot_prod_dot_west_dot_cpu_dot_load",
391+
"expectedValue": "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load",
392+
},
393+
{
394+
"name": "label name with dots and underscore",
395+
"input": "mysystem.prod.west.cpu.load_total",
396+
"expectedUnderscores": "mysystem_prod_west_cpu_load_total",
397+
"expectedDots": "mysystem_dot_prod_dot_west_dot_cpu_dot_load__total",
398+
"expectedValue": "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load__total",
399+
},
400+
{
401+
"name": "label name with dots and colon",
402+
"input": "http.status:sum",
403+
"expectedUnderscores": "http_status_sum",
404+
"expectedDots": "http_dot_status__sum",
405+
"expectedValue": "U__http_2e_status_3a_sum",
406+
},
407+
{
408+
"name": "label name with spaces and emoji",
409+
"input": "label with 😱",
410+
"expectedUnderscores": "label_with__",
411+
"expectedDots": "label__with____",
412+
"expectedValue": "U__label_20_with_20__1f631_",
413+
},
414+
{
415+
"name": "label name with unicode characters > 0x100",
416+
"input": "花火",
417+
"expectedUnderscores": "__",
418+
"expectedDots": "____",
419+
"expectedValue": "U___82b1__706b_",
420+
},
421+
{
422+
"name": "label name with spaces and edge-case value",
423+
"input": "label with \u0100",
424+
"expectedUnderscores": "label_with__",
425+
"expectedDots": "label__with____",
426+
"expectedValue": "U__label_20_with_20__100_",
427+
},
428+
])
429+
def test_escape_label_name(scenario):
430+
input = scenario["input"]
431+
432+
got = escape_label_name(input, UNDERSCORES)
433+
assert got == scenario["expectedUnderscores"], f"[{scenario['name']}] Underscore escaping failed"
434+
435+
got = escape_label_name(input, DOTS)
436+
assert got == scenario["expectedDots"], f"[{scenario['name']}] Dots escaping failed"
437+
438+
got = escape_label_name(input, VALUES)
439+
assert got == scenario["expectedValue"], f"[{scenario['name']}] Value encoding failed"
440+
441+
285442
if __name__ == '__main__':
286443
unittest.main()

0 commit comments

Comments
 (0)