Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions internal/impl/prometheus/metrics_prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,30 @@ func (p *metrics) NewGaugeCtor(path string, labelNames ...string) service.Metric
}
}

// DeleteSeriesPartialMatch deletes all metric series containing labels
// matching all of the provided label key/value pairs, e.g. all series of a
// stream deleted in streams mode. Implements the optional
// service.MetricsExporterSeriesDeleter interface.
func (p *metrics) DeleteSeriesPartialMatch(labels map[string]string) {
promLabels := prometheus.Labels(labels)

p.mut.Lock()
defer p.mut.Unlock()

for _, pv := range p.counters {
pv.ctr.DeletePartialMatch(promLabels)
}
for _, pv := range p.gauges {
pv.ctr.DeletePartialMatch(promLabels)
}
for _, pv := range p.timers {
pv.sum.DeletePartialMatch(promLabels)
}
for _, pv := range p.timersHist {
pv.sum.DeletePartialMatch(promLabels)
}
Comment on lines +533 to +544

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeletePartialMatch is applied unconditionally to every registered vec, including vecs that do not declare the label being matched on.

In prometheus/client_golang (pinned here at v1.23.2 per go.mod#L136), MetricVec.DeletePartialMatchmetricMap.deleteByLabelsmatchPartialLabels, which looks each requested label key up in the vec's variable label names and skips the comparison entirely when the key is not one of them. If none of the requested keys are variable labels of that vec, the match is vacuously true and every series in the vec is deleted.

Failure scenario: DeleteSeriesPartialMatch({"stream": "foo"}) is called while a metric vec exists that has no stream label — e.g. the zero-label uptime counter created at metrics_prometheus_test.go#L189-L191, or any global/non-stream-scoped vec. All of its series are dropped from /metrics, so deleting one stream silently wipes unrelated counters and gauges. Note this also means the new test's own assert.Contains(t, body, "\nuptime 9") assertion at line 208 should be failing — worth confirming the test actually passes locally before merging.

Suggested fix: record the declared label names on promCounterVec/promGaugeVec/promTimingVec/promTimingHistVec (they already carry count) and skip any vec that does not declare every key present in labels before calling DeletePartialMatch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against the pinned client_golang (v1.23.2): the premise is inverted. In matchPartialLabels (prometheus/vec.go:468-483), when a requested key is not one of the vec's variable labels, indexOf returns validLabel == false, the inner branch is skipped, and the function hits return false — an unknown label key is a non-match for that vec, not a wildcard. There is no vacuously-true path; the loop only continues on an actual value match.

Consistent with that, TestPrometheusDeleteSeriesPartialMatch passes as written, including the uptime 9 assertion this comment predicted would fail. Added a labeled vec without the stream label (batch_created{mechanism="count"}) to the test to pin the non-match behavior explicitly.

}

func (p *metrics) Close(context.Context) error {
if atomic.CompareAndSwapInt32(&p.running, 1, 0) {
close(p.closedChan)
Expand Down
66 changes: 66 additions & 0 deletions internal/impl/prometheus/metrics_prometheus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,72 @@ func TestPrometheusMetrics(t *testing.T) {
assert.Contains(t, body, "\ngaugethree 10.452")
}

func TestPrometheusDeleteSeriesPartialMatch(t *testing.T) {
nm, handler := getTestProm(t)

ctr := nm.NewCounterCtor("input_received", "label", "stream")
ctr("in", "foo").Incr(3)
ctr("in", "bar").Incr(4)

gge := nm.NewGaugeCtor("input_connection_up", "stream")
gge("foo").Set(1)
gge("bar").Set(1)

tmr := nm.NewTimerCtor("input_latency_ns", "stream")
tmr("foo").Timing(100)
tmr("bar").Timing(200)

unlabelled := nm.NewCounterCtor("uptime")()
unlabelled.Incr(9)

// A vec with labels that do not include the matched key must be untouched:
// client_golang treats an unknown label key as a non-match, not a wildcard.
otherLabels := nm.NewCounterCtor("batch_created", "mechanism")
otherLabels("count").Incr(5)

body := getPage(t, handler)
require.Contains(t, body, "\ninput_received{label=\"in\",stream=\"foo\"} 3")
require.Contains(t, body, "\ninput_latency_ns_sum{stream=\"foo\"} 100")

purger, ok := any(nm).(interface {
DeleteSeriesPartialMatch(labels map[string]string)
})
require.True(t, ok, "prometheus exporter should support deleting series by label match")
purger.DeleteSeriesPartialMatch(map[string]string{"stream": "foo"})

body = getPage(t, handler)
assert.NotContains(t, body, "stream=\"foo\"")
assert.Contains(t, body, "\ninput_received{label=\"in\",stream=\"bar\"} 4")
assert.Contains(t, body, "\ninput_connection_up{stream=\"bar\"} 1")
assert.Contains(t, body, "\ninput_latency_ns_sum{stream=\"bar\"} 200")
assert.Contains(t, body, "\nuptime 9")
assert.Contains(t, body, "\nbatch_created{mechanism=\"count\"} 5")
}

func TestPrometheusDeleteSeriesPartialMatchHistogram(t *testing.T) {
nm := promFromYAML(t, `
use_histogram_timing: true
`)

tmr := nm.NewTimerCtor("input_latency_ns", "stream")
tmr("foo").Timing(100)
tmr("bar").Timing(200)

handler := nm.HandlerFunc()
body := getPage(t, handler)
require.Contains(t, body, "stream=\"foo\"")

purger, ok := any(nm).(interface {
DeleteSeriesPartialMatch(labels map[string]string)
})
require.True(t, ok, "prometheus exporter should support deleting series by label match")
purger.DeleteSeriesPartialMatch(map[string]string{"stream": "foo"})

body = getPage(t, handler)
assert.NotContains(t, body, "stream=\"foo\"")
assert.Contains(t, body, "\ninput_latency_ns_count{stream=\"bar\"} 1")
}

func TestPrometheusHistMetrics(t *testing.T) {
nm := promFromYAML(t, `
use_histogram_timing: true
Expand Down
Loading