diff --git a/prometheus_client/validation.py b/prometheus_client/validation.py index 6fcc8018..cdbba116 100644 --- a/prometheus_client/validation.py +++ b/prometheus_client/validation.py @@ -1,3 +1,4 @@ +from collections import Counter import os import re @@ -110,6 +111,10 @@ def _validate_labelnames(cls, labelnames): _validate_labelname(l) if l in cls._reserved_labelnames: raise ValueError('Reserved label methe fric name: ' + l) + # Duplicate names collapse distinct label sets during collection. + duplicates = sorted(name for name, count in Counter(labelnames).items() if count > 1) + if duplicates: + raise ValueError(f'Duplicate label names: {duplicates!r}') return labelnames diff --git a/tests/test_core.py b/tests/test_core.py index 3aa19c24..75269b3a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -736,6 +736,43 @@ def test_invalid_names_raise(self): self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved']) self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile']) + def test_duplicate_labelnames_raise_before_registration(self): + # Repeated names would collapse distinct label sets on export. + for metric_type in (Counter, Gauge, Summary, Histogram, Info, Enum): + sequence_types = (list, tuple) if metric_type is Enum else (list, tuple, iter) + for sequence_type in sequence_types: + with self.subTest(metric_type=metric_type, sequence_type=sequence_type): + registry = CollectorRegistry() + kwargs = {'states': ['ready']} if metric_type is Enum else {} + with self.assertRaisesRegex(ValueError, r"^Duplicate label names: \['a'\]$"): + metric_type( + 'duplicate_labels', 'help', + labelnames=sequence_type(['a', 'a']), + registry=registry, **kwargs, + ) + self.assertEqual(list(registry.collect()), []) + + def test_duplicate_labelnames_error_lists_only_duplicates(self): + for labelnames, expected in ( + (['b', 'a', 'b', 'unique', 'a', 'b'], ['a', 'b']), + (['a', 'a', 'a'], ['a']), + (['β', 'unique', 'β'], ['β']), + ): + with self.subTest(labelnames=labelnames): + registry = CollectorRegistry() + with self.assertRaises(ValueError) as error: + Counter('duplicate_labels', 'help', iter(labelnames), registry=registry) + self.assertEqual(str(error.exception), f'Duplicate label names: {expected!r}') + self.assertEqual(list(registry.collect()), []) + + def test_unique_labelnames_iterator(self): + # Valid one-shot label iterables still preserve both names. + counter = Counter('iter_labels', 'help', iter(['a', 'b']), registry=self.registry) + counter.labels('x', 'y').inc() + self.assertEqual( + self.registry.get_sample_value('iter_labels_total', {'a': 'x', 'b': 'y'}), 1, + ) + def test_empty_labels_list(self): Histogram('h', 'help', [], registry=self.registry) self.assertEqual(0, self.registry.get_sample_value('h_sum'))