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
126 changes: 126 additions & 0 deletions prometheus/vec.go
Expand Up @@ -80,6 +80,16 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
return m.metricMap.deleteByHashWithLabelValues(h, lvs, m.curry)
}

// DeletePartialMatchLabelValues deletes all metrics where the variable labels
// contain all of the values passed. The order of the passed values does not matter.
// It returns the number of metrics deleted.

// This method deletes a metric if the given value is present for any label.
// To delete a metric based on a partial match for a particular label, use DeletePartialMatch().
func (m *MetricVec) DeletePartialMatchLabelValues(lvs ...string) int {
stone-z marked this conversation as resolved.
Show resolved Hide resolved
return m.metricMap.deleteByLabelValues(lvs)
}

// Delete deletes the metric where the variable labels are the same as those
// passed in as labels. It returns true if a metric was deleted.
//
Expand All @@ -99,6 +109,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.

// This method deletes a metric if the given label: value pair is found in that metric.
// To delete a metric if a particular value is found associated with any label, use DeletePartialMatchLabelValues().
stone-z marked this conversation as resolved.
Show resolved Hide resolved
func (m *MetricVec) DeletePartialMatch(labels Labels) int {
return m.metricMap.deleteByLabels(labels)
}

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

Expand Down Expand Up @@ -353,6 +373,51 @@ func (m *metricMap) deleteByHashWithLabelValues(
return true
}

// deleteByLabelValues deletes a metric if the given values (lvs) are present in the metric.
func (m *metricMap) deleteByLabelValues(lvs []string) int {
m.mtx.Lock()
defer m.mtx.Unlock()

var numDeleted int

for h, metrics := range m.metrics {
i := findMetricWithPartialLabelValues(metrics, lvs)
if i >= len(metrics) {
// Didn't find matching label values in this metric slice.
continue
}
delete(m.metrics, h)
numDeleted++
}

return numDeleted
}

// findMetricWithPartialLabelValues returns the index of the matching metric or
// len(metrics) if not found.
func findMetricWithPartialLabelValues(
metrics []metricWithLabelValues, lvs []string,
) int {
for i, metric := range metrics {
if matchPartialLabelValues(metric.values, lvs) {
return i
}
}
return len(metrics)
}

// matchPartialLabelValues searches the current metric values and returns whether all of the target values are present.
func matchPartialLabelValues(values []string, lvs []string) bool {
for _, v := range lvs {
// Check if the target value exists in our metrics and get the index.
if _, validValue := indexOf(v, values); validValue {
continue
}
return false
}
return true
}

// deleteByHashWithLabels removes the metric from the hash bucket h. If there
// are multiple matches in the bucket, use lvs to select a metric and remove
// only that metric.
Expand Down Expand Up @@ -381,6 +446,67 @@ 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) int {
m.mtx.Lock()
defer m.mtx.Unlock()

var numDeleted int

for h, metrics := range m.metrics {
i := findMetricWithPartialLabels(m.desc, metrics, labels)
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,
) int {
for i, metric := range metrics {
if matchPartialLabels(desc, metric.values, labels) {
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
}

// 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) 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 values[varLabelIndex] == v {
continue

}
}
return false
}
return true
}

// getOrCreateMetricWithLabelValues retrieves the metric by hash and label value
// or creates it and returns the new one.
//
Expand Down
108 changes: 108 additions & 0 deletions prometheus/vec_test.go
Expand Up @@ -125,6 +125,114 @@ 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)
if got, want := vec.DeletePartialMatch(Labels{"l1": "v1"}), 2; got != want {
t.Errorf("got %v, want %v", got, want)
}

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)
}

// 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 TestDeletePartialMatchLabelValues(t *testing.T) {
vec := NewGaugeVec(
GaugeOpts{
Name: "test",
Help: "helpless",
},
[]string{"l1", "l2"},
)
testDeletePartialMatchLabelValues(t, vec)
}

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

// Try to delete with a single valid value.
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42)
if got, want := vec.DeletePartialMatchLabelValues("v1"), 1; got != want {
t.Errorf("got %v, want %v", got, want)
}

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

// Try to delete with a single valid value which matches multiple metrics.
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Gauge).Set(42)
vec.With(Labels{"l1": "v1", "l2": "vv22"}).(Gauge).Set(84)
if got, want := vec.DeletePartialMatchLabelValues("v1"), 2; got != want {
t.Errorf("got %v, want %v", got, want)
}

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

// Try to delete with nonexistent value z1.
if got, want := c1.DeletePartialMatchLabelValues("z1"), 0; got != want {
t.Errorf("got %v, want %v", got, want)
}

// Delete with valid curried value cv1.
if got, want := c1.DeletePartialMatchLabelValues("cv1"), 1; got != want {
t.Errorf("got %v, want %v", got, want)
}
}

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