Skip to content

Commit edb0e7c

Browse files
author
Hazel
committed
Add remove_matching() method for metric label deletion
Signed-off-by: Hazel <[email protected]>
1 parent 7368028 commit edb0e7c

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

prometheus_client/metrics.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,41 @@ def remove(self, *labelvalues: Any) -> None:
203203
if labelvalues in self._metrics:
204204
del self._metrics[labelvalues]
205205

206+
def remove_matching(self, partial: dict[str, str]) -> int:
207+
if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
208+
warnings.warn(
209+
"Removal of labels has not been implemented in multi-process mode yet.",
210+
UserWarning
211+
)
212+
213+
if not self._labelnames:
214+
raise ValueError('No label names were set when constructing %s' % self)
215+
216+
if not isinstance(partial, dict):
217+
raise TypeError("partial must be a dict of {label_name: label_value}")
218+
219+
if not partial:
220+
return 0
221+
222+
invalid = [k for k in partial.keys() if k not in self._labelnames]
223+
if invalid:
224+
raise ValueError(
225+
'Unknown label names: %s; expected %s' % (invalid, self._labelnames)
226+
)
227+
228+
pos_filter = {self._labelnames.index(k): str(v) for k, v in partial.items()}
229+
230+
deleted = 0
231+
with self._lock:
232+
for lv in list(self._metrics.keys()):
233+
if all(lv[pos] == want for pos, want in pos_filter.items()):
234+
if lv in self._metrics:
235+
del self._metrics[lv]
236+
deleted += 1
237+
238+
return deleted
239+
240+
206241
def clear(self) -> None:
207242
"""Remove all labelsets from the metric"""
208243
if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:

tests/test_core.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,36 @@ def test_labels_coerced_to_string(self):
630630
self.counter.remove(None)
631631
self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'}))
632632

633+
def test_remove_matching(self):
634+
from prometheus_client import Counter
635+
636+
c = Counter('c2', 'help', ['tenant', 'endpoint'], registry=self.registry)
637+
c.labels('acme', '/').inc()
638+
c.labels('acme', '/checkout').inc()
639+
c.labels('globex', '/').inc()
640+
641+
642+
deleted = c.remove_matching({'tenant': 'acme'})
643+
self.assertEqual(2, deleted)
644+
645+
self.assertIsNone(self.registry.get_sample_value('c2_total', {'tenant': 'acme', 'endpoint': '/'}))
646+
self.assertIsNone(self.registry.get_sample_value('c2_total', {'tenant': 'acme', 'endpoint': '/checkout'}))
647+
self.assertEqual(1, self.registry.get_sample_value('c2_total', {'tenant': 'globex', 'endpoint': '/'}))
648+
649+
def test_remove_matching_invalid_label_name(self):
650+
from prometheus_client import Counter
651+
c = Counter('c3', 'help', ['tenant', 'endpoint'], registry=self.registry)
652+
c.labels('acme', '/').inc()
653+
with self.assertRaises(ValueError):
654+
c.remove_matching({'badkey': 'x'})
655+
656+
def test_remove_matching_empty_is_noop(self):
657+
from prometheus_client import Counter
658+
c = Counter('c4', 'help', ['tenant', 'endpoint'], registry=self.registry)
659+
c.labels('acme', '/').inc()
660+
self.assertEqual(0, c.remove_matching({}))
661+
self.assertEqual(1, self.registry.get_sample_value('c4_total', {'tenant': 'acme', 'endpoint': '/'}))
662+
633663
def test_non_string_labels_raises(self):
634664
class Test:
635665
__str__ = None

0 commit comments

Comments
 (0)