Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement deletion based on partially matching labels #1013

Merged
merged 14 commits into from Apr 21, 2022
86 changes: 86 additions & 0 deletions prometheus/vec.go
Expand Up @@ -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)
}
Comment on lines +108 to +110
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the computational complexity of this function? I imagine it is O(metrics count), right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. I hoped to find a constant time implementation but didn't find one (at least at the time)


// Without explicit forwarding of Describe, Collect, Reset, those methods won't
// show up in GoDoc.

Expand Down Expand Up @@ -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 value in this metric matches the curry value as well as our target.
return curriedValue.value == targetValue && values[index] == 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.
//
Expand Down
87 changes: 87 additions & 0 deletions prometheus/vec_test.go
Expand Up @@ -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{
Expand Down