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
80 changes: 80 additions & 0 deletions prometheus/vec.go
Expand Up @@ -99,6 +99,13 @@ 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.
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 +388,79 @@ 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.
func valueMatchesVariableOrCurriedValue(targetValue string, index int, values []string, curry []curriedLabelValue) 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
}
}
// This label was not curried. See if the current value matches our target label.
return values[index] == targetValue
}

// 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.
if valueMatchesVariableOrCurriedValue(v, varLabelIndex, values, curry) {
continue
}
}
return false
}
return true
}

// getOrCreateMetricWithLabelValues retrieves the metric by hash and label value
// or creates it and returns the new one.
//
Expand Down
81 changes: 81 additions & 0 deletions prometheus/vec_test.go
Expand Up @@ -125,6 +125,87 @@ func testDeleteLabelValues(t *testing.T, vec *GaugeVec) {
}
}

func TestDeletePartialMatch(t *testing.T) {
vec := NewGaugeVec(
GaugeOpts{
Name: "test",
Help: "helpless",
},
[]string{"l1", "l2"},
)
testDeletePartialMatch(t, vec)
stone-z marked this conversation as resolved.
Show resolved Hide resolved
}

func testDeletePartialMatch(t *testing.T, vec *GaugeVec) {
// No metric value is set.
if got, want := vec.DeletePartialMatch(Labels{"l1": "v1", "l2": "v2"}), 0; got != want {
t.Errorf("got %v, want %v", got, want)
}

c1 := vec.MustCurryWith(Labels{"l1": "v1"})
c1.WithLabelValues("2").Inc()

// Try to delete nonexistent label lx with existent value v1.
if got, want := c1.DeletePartialMatch(Labels{"lx": "v1"}), 0; got != want {
t.Errorf("got %v, want %v", got, want)
}

// Delete with valid pair l1: v1.
if got, want := c1.DeletePartialMatch(Labels{"l1": "v1"}), 1; got != want {
stone-z marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("got %v, want %v", got, want)
}

// Try to delete with partially invalid labels.
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42)
if got, want := vec.DeletePartialMatch(Labels{"l1": "v1", "l2": "xv2"}), 0; got != want {
t.Errorf("got %v, want %v", got, want)
}

// Try to delete with a single valid label which matches multiple metrics.
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42)
vec.With(Labels{"l1": "v1", "l2": "vv22"}).(Gauge).Set(84)
c3 := vec.MustCurryWith(Labels{"l2": "l2C3CurriedValue"}) // Used below
vec.With(Labels{"l1": "v3", "l2": "v3"}).(Gauge).Set(168)
if got, want := vec.DeletePartialMatch(Labels{"l1": "v1"}), 2; got != want {
t.Errorf("got %v, want %v", got, want)
}

// Try to delete a value which shouldn't be in our base vector (only the curried one c3).
if got, want := vec.DeletePartialMatch(Labels{"l2": "l2C3CurriedValue"}), 0; got != want {
t.Errorf("got %v, want %v", got, want)
}
stone-z marked this conversation as resolved.
Show resolved Hide resolved

c2 := vec.MustCurryWith(Labels{"l2": "l2CurriedValue"})
c2.With(Labels{"l1": "11"}).Inc()

// Delete with valid curried pair l2: l2CurriedValue.
if got, want := c2.DeletePartialMatch(Labels{"l2": "l2CurriedValue"}), 1; got != want {
stone-z marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("got %v, want %v", got, want)
}

c3.With(Labels{"l1": "11"}).Inc()

// Try to delete with invalid curried pair l1: v1.
if got, want := c3.DeletePartialMatch(Labels{"l1": "v1"}), 0; got != want {
t.Errorf("got %v, want %v", got, want)
}
// Delete valid curried pair l2: l2C3CurriedValue.
if got, want := c3.DeletePartialMatch(Labels{"l2": "l2C3CurriedValue"}), 1; got != want {
stone-z marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("got %v, want %v", got, want)
}

// Try to delete with a label value from before currying.
if got, want := c2.DeletePartialMatch(Labels{"l2": "v3"}), 0; got != want {
stone-z marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("got %v, want %v", got, want)
}

// Same labels, value matches.
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42)
if got, want := vec.DeletePartialMatch(Labels{"l1": "v1"}), 1; got != want {
t.Errorf("got %v, want %v", got, want)
}
}

func TestMetricVec(t *testing.T) {
vec := NewGaugeVec(
GaugeOpts{
Expand Down