Skip to content

Commit ff9a2d3

Browse files
authored
Reject duplicate label names before metric registration (#1217)
* Reject duplicate label names before metric registration Signed-off-by: fhgffy <102001626+fhgffy@users.noreply.github.com> * Improve duplicate label name diagnostics Remove date prefixes and include each duplicated label name in the validation error. Cover multiple duplicates, repeated names and Unicode labels. Signed-off-by: fhgffy <102001626+fhgffy@users.noreply.github.com> --------- Signed-off-by: fhgffy <102001626+fhgffy@users.noreply.github.com>
1 parent 512a8d4 commit ff9a2d3

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

‎prometheus_client/validation.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import Counter
12
import os
23
import re
34

@@ -110,6 +111,10 @@ def _validate_labelnames(cls, labelnames):
110111
_validate_labelname(l)
111112
if l in cls._reserved_labelnames:
112113
raise ValueError('Reserved label methe fric name: ' + l)
114+
# Duplicate names collapse distinct label sets during collection.
115+
duplicates = sorted(name for name, count in Counter(labelnames).items() if count > 1)
116+
if duplicates:
117+
raise ValueError(f'Duplicate label names: {duplicates!r}')
113118
return labelnames
114119

115120

‎tests/test_core.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,43 @@ def test_invalid_names_raise(self):
789789
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved'])
790790
self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile'])
791791

792+
def test_duplicate_labelnames_raise_before_registration(self):
793+
# Repeated names would collapse distinct label sets on export.
794+
for metric_type in (Counter, Gauge, Summary, Histogram, Info, Enum):
795+
sequence_types = (list, tuple) if metric_type is Enum else (list, tuple, iter)
796+
for sequence_type in sequence_types:
797+
with self.subTest(metric_type=metric_type, sequence_type=sequence_type):
798+
registry = CollectorRegistry()
799+
kwargs = {'states': ['ready']} if metric_type is Enum else {}
800+
with self.assertRaisesRegex(ValueError, r"^Duplicate label names: \['a'\]$"):
801+
metric_type(
802+
'duplicate_labels', 'help',
803+
labelnames=sequence_type(['a', 'a']),
804+
registry=registry, **kwargs,
805+
)
806+
self.assertEqual(list(registry.collect()), [])
807+
808+
def test_duplicate_labelnames_error_lists_only_duplicates(self):
809+
for labelnames, expected in (
810+
(['b', 'a', 'b', 'unique', 'a', 'b'], ['a', 'b']),
811+
(['a', 'a', 'a'], ['a']),
812+
(['β', 'unique', 'β'], ['β']),
813+
):
814+
with self.subTest(labelnames=labelnames):
815+
registry = CollectorRegistry()
816+
with self.assertRaises(ValueError) as error:
817+
Counter('duplicate_labels', 'help', iter(labelnames), registry=registry)
818+
self.assertEqual(str(error.exception), f'Duplicate label names: {expected!r}')
819+
self.assertEqual(list(registry.collect()), [])
820+
821+
def test_unique_labelnames_iterator(self):
822+
# Valid one-shot label iterables still preserve both names.
823+
counter = Counter('iter_labels', 'help', iter(['a', 'b']), registry=self.registry)
824+
counter.labels('x', 'y').inc()
825+
self.assertEqual(
826+
self.registry.get_sample_value('iter_labels_total', {'a': 'x', 'b': 'y'}), 1,
827+
)
828+
792829
def test_empty_labels_list(self):
793830
Histogram('h', 'help', [], registry=self.registry)
794831
self.assertEqual(0, self.registry.get_sample_value('h_sum'))

0 commit comments

Comments
 (0)