diff --git a/docs/content/instrumenting/labels.md b/docs/content/instrumenting/labels.md index 39ad29c8..f2bd2f04 100644 --- a/docs/content/instrumenting/labels.md +++ b/docs/content/instrumenting/labels.md @@ -39,15 +39,19 @@ c.labels('post', '/submit') ## Removing labelsets -### `remove(*labelvalues)` +### `remove(*labelvalues, **labelkwargs)` -Remove a specific labelset from the metric. Values must be passed in the same -order as `labelnames` were declared. +Remove a specific labelset from the metric. As with `labels()`, values can be +passed in the same order as `labelnames` were declared, or as keyword +arguments (one for every label name). ```python c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint']) c.labels('get', '/').inc() c.remove('get', '/') + +c.labels(method='post', endpoint='/submit').inc() +c.remove(method='post', endpoint='/submit') ``` ### `remove_by_labels(labels)` diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index dfa158b9..f7b3fa83 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -209,7 +209,15 @@ def labels(self: T, *labelvalues: object, **labelkwargs: object) -> T: ) return self._metrics[str_labelvalues] - def remove(self, *labelvalues: Any) -> None: + def remove(self, *labelvalues: Any, **labelkwargs: Any) -> None: + """Remove the given labelset from the metric. + + As with labels(), the label values can be passed positionally or as + keyword arguments: + + c.remove('get', '/') + c.remove(method='get', endpoint='/') + """ if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ: warnings.warn( "Removal of labels has not been implemented in multi-process mode yet.", @@ -218,10 +226,18 @@ def remove(self, *labelvalues: Any) -> None: if not self._labelnames: raise ValueError('No label names were set when constructing %s' % self) - """Remove the given labelset from the metric.""" - if len(labelvalues) != len(self._labelnames): - raise ValueError('Incorrect label count (expected %d, got %s)' % (len(self._labelnames), labelvalues)) - labelvalues = tuple(str(l) for l in labelvalues) + if labelvalues and labelkwargs: + raise ValueError("Can't pass both *args and **kwargs") + + if labelkwargs: + if sorted(labelkwargs) != sorted(self._labelnames): + raise ValueError('Incorrect label names (expected %s, got %s)' % ( + sorted(self._labelnames), sorted(labelkwargs))) + labelvalues = tuple(str(labelkwargs[l]) for l in self._labelnames) + else: + if len(labelvalues) != len(self._labelnames): + raise ValueError('Incorrect label count (expected %d, got %s)' % (len(self._labelnames), labelvalues)) + labelvalues = tuple(str(l) for l in labelvalues) with self._lock: if labelvalues in self._metrics: del self._metrics[labelvalues] diff --git a/tests/test_core.py b/tests/test_core.py index 2c29f79a..175aa893 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -664,8 +664,8 @@ def increment(): self.assertEqual(800, self.registry.get_sample_value('c_total', {'l': 'shared'})) def test_recreate_child_after_removal(self): - for remove in (lambda: self.counter.remove('x'), self.counter.clear, - lambda: self.counter.remove_by_labels({'l': 'x'})): + for remove in (lambda: self.counter.remove('x'), lambda: self.counter.remove(l='x'), + self.counter.clear, lambda: self.counter.remove_by_labels({'l': 'x'})): original = self.counter.labels('x') original.inc() remove() @@ -683,6 +683,30 @@ def test_remove(self): self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'})) self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'})) + def test_remove_with_label_kwargs(self): + self.counter.labels('x').inc() + self.counter.labels('y').inc(2) + self.counter.remove(l='x') + self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'})) + self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'})) + + def test_remove_with_label_kwargs_matches_by_name(self): + self.two_labels.labels(a='x', b='y').inc() + self.two_labels.labels(a='y', b='x').inc(2) + + self.two_labels.remove(b='y', a='x') + + self.assertIsNone(self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'})) + self.assertEqual(2, self.registry.get_sample_value('two_total', {'a': 'y', 'b': 'x'})) + + def test_remove_with_incorrect_label_kwargs_raises(self): + self.two_labels.labels('x', 'y').inc() + self.assertRaises(ValueError, self.two_labels.remove, 'x', b='y') + self.assertRaises(ValueError, self.two_labels.remove, a='x') + self.assertRaises(ValueError, self.two_labels.remove, a='x', c='y') + self.assertRaises(ValueError, self.two_labels.remove, a='x', b='y', c='z') + self.assertEqual(1, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'})) + def test_clear(self): self.counter.labels('x').inc() self.counter.labels('y').inc(2) @@ -710,6 +734,10 @@ def test_labels_coerced_to_string(self): self.counter.remove(None) self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'})) + self.counter.labels(None).inc() + self.counter.remove(l=None) + self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'})) + def test_remove_by_labels(self): from prometheus_client import Counter