diff --git a/prometheus/vec.go b/prometheus/vec.go index 4ababe6c9..d0b5f0b66 100644 --- a/prometheus/vec.go +++ b/prometheus/vec.go @@ -99,6 +99,16 @@ func (m *MetricVec) Delete(labels Labels) bool { return m.metricMap.deleteByHashWithLabels(h, labels, m.curry) } +// DeletePartialMatch deletes all metrics where the variable labels contain all of those +// passed in as labels. The order of the labels does not matter. +// It returns the number of metrics deleted. +// +// Note that curried labels will never be matched if deleting from the curried vector. +// To match curried labels with DeletePartialMatch, it must be called on the base vector. +func (m *MetricVec) DeletePartialMatch(labels Labels) int { + return m.metricMap.deleteByLabels(labels, m.curry) +} + // Without explicit forwarding of Describe, Collect, Reset, those methods won't // show up in GoDoc. @@ -381,6 +391,82 @@ func (m *metricMap) deleteByHashWithLabels( return true } +// deleteByLabels deletes a metric if the given labels are present in the metric. +func (m *metricMap) deleteByLabels(labels Labels, curry []curriedLabelValue) int { + m.mtx.Lock() + defer m.mtx.Unlock() + + var numDeleted int + + for h, metrics := range m.metrics { + i := findMetricWithPartialLabels(m.desc, metrics, labels, curry) + if i >= len(metrics) { + // Didn't find matching labels in this metric slice. + continue + } + delete(m.metrics, h) + numDeleted++ + } + + return numDeleted +} + +// findMetricWithPartialLabel returns the index of the matching metric or +// len(metrics) if not found. +func findMetricWithPartialLabels( + desc *Desc, metrics []metricWithLabelValues, labels Labels, curry []curriedLabelValue, +) int { + for i, metric := range metrics { + if matchPartialLabels(desc, metric.values, labels, curry) { + return i + } + } + return len(metrics) +} + +// indexOf searches the given slice of strings for the target string and returns +// the index or len(items) as well as a boolean whether the search succeeded. +func indexOf(target string, items []string) (int, bool) { + for i, l := range items { + if l == target { + return i, true + } + } + return len(items), false +} + +// valueMatchesVariableOrCurriedValue determines if a value was previously curried, +// and returns whether it matches either the "base" value or the curried value accordingly. +// It also indicates whether the match is against a curried or uncurried value. +func valueMatchesVariableOrCurriedValue(targetValue string, index int, values []string, curry []curriedLabelValue) (bool, bool) { + for _, curriedValue := range curry { + if curriedValue.index == index { + // This label was curried. See if the curried value matches our target. + return curriedValue.value == targetValue, true + } + } + // This label was not curried. See if the current value matches our target label. + return values[index] == targetValue, false +} + +// matchPartialLabels searches the current metric and returns whether all of the target label:value pairs are present. +func matchPartialLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool { + for l, v := range labels { + // Check if the target label exists in our metrics and get the index. + varLabelIndex, validLabel := indexOf(l, desc.variableLabels) + if validLabel { + // Check the value of that label against the target value. + // We don't consider curried values in partial matches. + matches, curried := valueMatchesVariableOrCurriedValue(v, varLabelIndex, values, curry) + if matches && !curried { + continue + } + } + return false + } + return true +} + // getOrCreateMetricWithLabelValues retrieves the metric by hash and label value // or creates it and returns the new one. // diff --git a/prometheus/vec_test.go b/prometheus/vec_test.go index bd18a9f4e..fdb050fa7 100644 --- a/prometheus/vec_test.go +++ b/prometheus/vec_test.go @@ -125,6 +125,93 @@ func testDeleteLabelValues(t *testing.T, vec *GaugeVec) { } } +func TestDeletePartialMatch(t *testing.T) { + baseVec := NewGaugeVec( + GaugeOpts{ + Name: "test", + Help: "helpless", + }, + []string{"l1", "l2", "l3"}, + ) + + assertNoMetric := func(t *testing.T) { + if n := len(baseVec.metricMap.metrics); n != 0 { + t.Error("expected no metrics, got", n) + } + } + + // No metric value is set. + if got, want := baseVec.DeletePartialMatch(Labels{"l1": "v1", "l2": "v2"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + baseVec.With(Labels{"l1": "baseValue1", "l2": "baseValue2", "l3": "baseValue3"}).Inc() + baseVec.With(Labels{"l1": "multiDeleteV1", "l2": "diff1BaseValue2", "l3": "v3"}).(Gauge).Set(42) + baseVec.With(Labels{"l1": "multiDeleteV1", "l2": "diff2BaseValue2", "l3": "v3"}).(Gauge).Set(84) + baseVec.With(Labels{"l1": "multiDeleteV1", "l2": "diff3BaseValue2", "l3": "v3"}).(Gauge).Set(168) + + curriedVec := baseVec.MustCurryWith(Labels{"l2": "curriedValue2"}) + curriedVec.WithLabelValues("curriedValue1", "curriedValue3").Inc() + curriedVec.WithLabelValues("curriedValue1", "differentCurriedValue3").Inc() + curriedVec.WithLabelValues("differentCurriedValue1", "differentCurriedValue3").Inc() + + // Try to delete nonexistent label with existent value from curried vector. + if got, want := curriedVec.DeletePartialMatch(Labels{"lx": "curriedValue1"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Try to delete valid label with nonexistent value from curried vector. + if got, want := curriedVec.DeletePartialMatch(Labels{"l1": "badValue1"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Try to delete from a curried vector based on labels which were curried. + // This operation succeeds when run against the base vector below. + if got, want := curriedVec.DeletePartialMatch(Labels{"l2": "curriedValue2"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Try to delete from a curried vector based on labels which were curried, + // but the value actually exists in the base vector. + if got, want := curriedVec.DeletePartialMatch(Labels{"l2": "baseValue2"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Delete multiple matching metrics from a curried vector based on partial values. + if got, want := curriedVec.DeletePartialMatch(Labels{"l1": "curriedValue1"}), 2; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Try to delete nonexistent label with existent value from base vector. + if got, want := baseVec.DeletePartialMatch(Labels{"lx": "curriedValue1"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Try to delete partially invalid labels from base vector. + if got, want := baseVec.DeletePartialMatch(Labels{"l1": "baseValue1", "l2": "badValue2"}), 0; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Delete from the base vector based on values which were curried. + // This operation fails when run against a curried vector above. + if got, want := baseVec.DeletePartialMatch(Labels{"l2": "curriedValue2"}), 1; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Delete multiple metrics from the base vector based on a single valid label. + if got, want := baseVec.DeletePartialMatch(Labels{"l1": "multiDeleteV1"}), 3; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // Delete from the base vector based on values which were not curried. + if got, want := baseVec.DeletePartialMatch(Labels{"l3": "baseValue3"}), 1; got != want { + t.Errorf("got %v, want %v", got, want) + } + + // All metrics should have been deleted now. + assertNoMetric(t) +} + func TestMetricVec(t *testing.T) { vec := NewGaugeVec( GaugeOpts{