Skip to content

Commit 378510b

Browse files
hazel-shenHazel
andauthored
Add remove_matching() method for metric label deletion (#1121)
* Add remove_matching() method for metric label deletion Signed-off-by: Hazel <[email protected]> * Rename function name, and the parameter's name Signed-off-by: Hazel <[email protected]> * Make remove_by_labels() consistent with remove(): return None Signed-off-by: Hazel <[email protected]> --------- Signed-off-by: Hazel <[email protected]> Co-authored-by: Hazel <[email protected]>
1 parent 10db862 commit 378510b

File tree

2 files changed

+67
-0
lines changed

2 files changed

+67
-0
lines changed

prometheus_client/metrics.py

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

206+
def remove_by_labels(self, labels: dict[str, str]) -> None:
207+
"""Remove all series whose labelset partially matches the given labels."""
208+
if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
209+
warnings.warn(
210+
"Removal of labels has not been implemented in multi-process mode yet.",
211+
UserWarning
212+
)
213+
214+
if not self._labelnames:
215+
raise ValueError('No label names were set when constructing %s' % self)
216+
217+
if not isinstance(labels, dict):
218+
raise TypeError("labels must be a dict of {label_name: label_value}")
219+
220+
if not labels:
221+
return # no operation
222+
223+
invalid = [k for k in labels.keys() if k not in self._labelnames]
224+
if invalid:
225+
raise ValueError(
226+
'Unknown label names: %s; expected %s' % (invalid, self._labelnames)
227+
)
228+
229+
pos_filter = {self._labelnames.index(k): str(v) for k, v in labels.items()}
230+
231+
with self._lock:
232+
# list(...) to avoid "dictionary changed size during iteration"
233+
for lv in list(self._metrics.keys()):
234+
if all(lv[pos] == want for pos, want in pos_filter.items()):
235+
# pop with default avoids KeyError if concurrently removed
236+
self._metrics.pop(lv, None)
237+
238+
206239
def clear(self) -> None:
207240
"""Remove all labelsets from the metric"""
208241
if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:

tests/test_core.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,40 @@ 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_by_labels(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+
ret = c.remove_by_labels({'tenant': 'acme'})
642+
self.assertIsNone(ret)
643+
644+
self.assertIsNone(self.registry.get_sample_value('c2_total', {'tenant': 'acme', 'endpoint': '/'}))
645+
self.assertIsNone(self.registry.get_sample_value('c2_total', {'tenant': 'acme', 'endpoint': '/checkout'}))
646+
self.assertEqual(1, self.registry.get_sample_value('c2_total', {'tenant': 'globex', 'endpoint': '/'}))
647+
648+
649+
def test_remove_by_labels_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_by_labels({'badkey': 'x'})
655+
656+
657+
def test_remove_by_labels_empty_is_noop(self):
658+
from prometheus_client import Counter
659+
c = Counter('c4', 'help', ['tenant', 'endpoint'], registry=self.registry)
660+
c.labels('acme', '/').inc()
661+
662+
ret = c.remove_by_labels({})
663+
self.assertIsNone(ret)
664+
# Ensure the series is still present
665+
self.assertEqual(1, self.registry.get_sample_value('c4_total', {'tenant': 'acme', 'endpoint': '/'}))
666+
633667
def test_non_string_labels_raises(self):
634668
class Test:
635669
__str__ = None

0 commit comments

Comments
 (0)