Skip to content

Commit 4dcf02e

Browse files
authored
Implement deletion based on partially matching labels (#1013)
* WIP partial match Signed-off-by: Zach Stone <[email protected]> * Cleanup Signed-off-by: Zach Stone <[email protected]> * Comments Signed-off-by: Zach Stone <[email protected]> * Tests and comments Signed-off-by: Zach Stone <[email protected]> * Handle properly deleting multiple metrics, update tests Signed-off-by: Zach Stone <[email protected]> * Comments Signed-off-by: Zach Stone <[email protected]> * Try using curry values Signed-off-by: Zach Stone <[email protected]> * Skip curry value to demo Signed-off-by: Zach Stone <[email protected]> * Fix curry deletion, remove outdated comment. Signed-off-by: Zach Stone <[email protected]> * Fix logic for deletion of metrics from prior to currying Signed-off-by: Zach Stone <[email protected]> * Don't match curried values. Update tests. Signed-off-by: Zach Stone <[email protected]> * Remove unneccesasry helper and todo comments Signed-off-by: Zach Stone <[email protected]> * Comment about partial matching curried labels Signed-off-by: Zach Stone <[email protected]> * Simplify curried value check Signed-off-by: Zach Stone <[email protected]>
1 parent 48a686a commit 4dcf02e

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed

prometheus/vec.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ func (m *MetricVec) Delete(labels Labels) bool {
9999
return m.metricMap.deleteByHashWithLabels(h, labels, m.curry)
100100
}
101101

102+
// DeletePartialMatch deletes all metrics where the variable labels contain all of those
103+
// passed in as labels. The order of the labels does not matter.
104+
// It returns the number of metrics deleted.
105+
//
106+
// Note that curried labels will never be matched if deleting from the curried vector.
107+
// To match curried labels with DeletePartialMatch, it must be called on the base vector.
108+
func (m *MetricVec) DeletePartialMatch(labels Labels) int {
109+
return m.metricMap.deleteByLabels(labels, m.curry)
110+
}
111+
102112
// Without explicit forwarding of Describe, Collect, Reset, those methods won't
103113
// show up in GoDoc.
104114

@@ -381,6 +391,82 @@ func (m *metricMap) deleteByHashWithLabels(
381391
return true
382392
}
383393

394+
// deleteByLabels deletes a metric if the given labels are present in the metric.
395+
func (m *metricMap) deleteByLabels(labels Labels, curry []curriedLabelValue) int {
396+
m.mtx.Lock()
397+
defer m.mtx.Unlock()
398+
399+
var numDeleted int
400+
401+
for h, metrics := range m.metrics {
402+
i := findMetricWithPartialLabels(m.desc, metrics, labels, curry)
403+
if i >= len(metrics) {
404+
// Didn't find matching labels in this metric slice.
405+
continue
406+
}
407+
delete(m.metrics, h)
408+
numDeleted++
409+
}
410+
411+
return numDeleted
412+
}
413+
414+
// findMetricWithPartialLabel returns the index of the matching metric or
415+
// len(metrics) if not found.
416+
func findMetricWithPartialLabels(
417+
desc *Desc, metrics []metricWithLabelValues, labels Labels, curry []curriedLabelValue,
418+
) int {
419+
for i, metric := range metrics {
420+
if matchPartialLabels(desc, metric.values, labels, curry) {
421+
return i
422+
}
423+
}
424+
return len(metrics)
425+
}
426+
427+
// indexOf searches the given slice of strings for the target string and returns
428+
// the index or len(items) as well as a boolean whether the search succeeded.
429+
func indexOf(target string, items []string) (int, bool) {
430+
for i, l := range items {
431+
if l == target {
432+
return i, true
433+
}
434+
}
435+
return len(items), false
436+
}
437+
438+
// valueMatchesVariableOrCurriedValue determines if a value was previously curried,
439+
// and returns whether it matches either the "base" value or the curried value accordingly.
440+
// It also indicates whether the match is against a curried or uncurried value.
441+
func valueMatchesVariableOrCurriedValue(targetValue string, index int, values []string, curry []curriedLabelValue) (bool, bool) {
442+
for _, curriedValue := range curry {
443+
if curriedValue.index == index {
444+
// This label was curried. See if the curried value matches our target.
445+
return curriedValue.value == targetValue, true
446+
}
447+
}
448+
// This label was not curried. See if the current value matches our target label.
449+
return values[index] == targetValue, false
450+
}
451+
452+
// matchPartialLabels searches the current metric and returns whether all of the target label:value pairs are present.
453+
func matchPartialLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool {
454+
for l, v := range labels {
455+
// Check if the target label exists in our metrics and get the index.
456+
varLabelIndex, validLabel := indexOf(l, desc.variableLabels)
457+
if validLabel {
458+
// Check the value of that label against the target value.
459+
// We don't consider curried values in partial matches.
460+
matches, curried := valueMatchesVariableOrCurriedValue(v, varLabelIndex, values, curry)
461+
if matches && !curried {
462+
continue
463+
}
464+
}
465+
return false
466+
}
467+
return true
468+
}
469+
384470
// getOrCreateMetricWithLabelValues retrieves the metric by hash and label value
385471
// or creates it and returns the new one.
386472
//

prometheus/vec_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,93 @@ func testDeleteLabelValues(t *testing.T, vec *GaugeVec) {
125125
}
126126
}
127127

128+
func TestDeletePartialMatch(t *testing.T) {
129+
baseVec := NewGaugeVec(
130+
GaugeOpts{
131+
Name: "test",
132+
Help: "helpless",
133+
},
134+
[]string{"l1", "l2", "l3"},
135+
)
136+
137+
assertNoMetric := func(t *testing.T) {
138+
if n := len(baseVec.metricMap.metrics); n != 0 {
139+
t.Error("expected no metrics, got", n)
140+
}
141+
}
142+
143+
// No metric value is set.
144+
if got, want := baseVec.DeletePartialMatch(Labels{"l1": "v1", "l2": "v2"}), 0; got != want {
145+
t.Errorf("got %v, want %v", got, want)
146+
}
147+
148+
baseVec.With(Labels{"l1": "baseValue1", "l2": "baseValue2", "l3": "baseValue3"}).Inc()
149+
baseVec.With(Labels{"l1": "multiDeleteV1", "l2": "diff1BaseValue2", "l3": "v3"}).(Gauge).Set(42)
150+
baseVec.With(Labels{"l1": "multiDeleteV1", "l2": "diff2BaseValue2", "l3": "v3"}).(Gauge).Set(84)
151+
baseVec.With(Labels{"l1": "multiDeleteV1", "l2": "diff3BaseValue2", "l3": "v3"}).(Gauge).Set(168)
152+
153+
curriedVec := baseVec.MustCurryWith(Labels{"l2": "curriedValue2"})
154+
curriedVec.WithLabelValues("curriedValue1", "curriedValue3").Inc()
155+
curriedVec.WithLabelValues("curriedValue1", "differentCurriedValue3").Inc()
156+
curriedVec.WithLabelValues("differentCurriedValue1", "differentCurriedValue3").Inc()
157+
158+
// Try to delete nonexistent label with existent value from curried vector.
159+
if got, want := curriedVec.DeletePartialMatch(Labels{"lx": "curriedValue1"}), 0; got != want {
160+
t.Errorf("got %v, want %v", got, want)
161+
}
162+
163+
// Try to delete valid label with nonexistent value from curried vector.
164+
if got, want := curriedVec.DeletePartialMatch(Labels{"l1": "badValue1"}), 0; got != want {
165+
t.Errorf("got %v, want %v", got, want)
166+
}
167+
168+
// Try to delete from a curried vector based on labels which were curried.
169+
// This operation succeeds when run against the base vector below.
170+
if got, want := curriedVec.DeletePartialMatch(Labels{"l2": "curriedValue2"}), 0; got != want {
171+
t.Errorf("got %v, want %v", got, want)
172+
}
173+
174+
// Try to delete from a curried vector based on labels which were curried,
175+
// but the value actually exists in the base vector.
176+
if got, want := curriedVec.DeletePartialMatch(Labels{"l2": "baseValue2"}), 0; got != want {
177+
t.Errorf("got %v, want %v", got, want)
178+
}
179+
180+
// Delete multiple matching metrics from a curried vector based on partial values.
181+
if got, want := curriedVec.DeletePartialMatch(Labels{"l1": "curriedValue1"}), 2; got != want {
182+
t.Errorf("got %v, want %v", got, want)
183+
}
184+
185+
// Try to delete nonexistent label with existent value from base vector.
186+
if got, want := baseVec.DeletePartialMatch(Labels{"lx": "curriedValue1"}), 0; got != want {
187+
t.Errorf("got %v, want %v", got, want)
188+
}
189+
190+
// Try to delete partially invalid labels from base vector.
191+
if got, want := baseVec.DeletePartialMatch(Labels{"l1": "baseValue1", "l2": "badValue2"}), 0; got != want {
192+
t.Errorf("got %v, want %v", got, want)
193+
}
194+
195+
// Delete from the base vector based on values which were curried.
196+
// This operation fails when run against a curried vector above.
197+
if got, want := baseVec.DeletePartialMatch(Labels{"l2": "curriedValue2"}), 1; got != want {
198+
t.Errorf("got %v, want %v", got, want)
199+
}
200+
201+
// Delete multiple metrics from the base vector based on a single valid label.
202+
if got, want := baseVec.DeletePartialMatch(Labels{"l1": "multiDeleteV1"}), 3; got != want {
203+
t.Errorf("got %v, want %v", got, want)
204+
}
205+
206+
// Delete from the base vector based on values which were not curried.
207+
if got, want := baseVec.DeletePartialMatch(Labels{"l3": "baseValue3"}), 1; got != want {
208+
t.Errorf("got %v, want %v", got, want)
209+
}
210+
211+
// All metrics should have been deleted now.
212+
assertNoMetric(t)
213+
}
214+
128215
func TestMetricVec(t *testing.T) {
129216
vec := NewGaugeVec(
130217
GaugeOpts{

0 commit comments

Comments
 (0)