Skip to content
Merged
80 changes: 80 additions & 0 deletions prometheus/vec.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
}

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

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 {
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 {
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 {
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