Skip to content

Commit

Permalink
Implement deletion based on partially matching labels (#1013)
Browse files Browse the repository at this point in the history
* WIP partial match

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Cleanup

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Comments

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Tests and comments

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Handle properly deleting multiple metrics, update tests

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Comments

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Try using curry values

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Skip curry value to demo

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Fix curry deletion, remove outdated comment.

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Fix logic for deletion of metrics from prior to currying

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Don't match curried values. Update tests.

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Remove unneccesasry helper and todo comments

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Comment about partial matching curried labels

Signed-off-by: Zach Stone <zach@giantswarm.io>

* Simplify curried value check

Signed-off-by: Zach Stone <zach@giantswarm.io>
  • Loading branch information
stone-z committed Apr 21, 2022
1 parent 48a686a commit 4dcf02e
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 0 deletions.
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)
}

// 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 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.
//
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

0 comments on commit 4dcf02e

Please sign in to comment.