vmalert: allow using extra labels in annotations (#3181)

According to Ruler specification, only labels returned within time series
should be available for use in annotations.

For long time, vmalert didn't respect this rule. And in PR
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2403
this was fixed for the sake of compatibility. However, this resulted
into users confusion, as they expected all configured and extra labels
to be available - https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3013

This fix allows to use extra labels in Annotations. But in the case of conflicts
the original labels (extracted from time series) are preferred.

Signed-off-by: hagen1778 <roman@victoriametrics.com>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
This commit is contained in:
Roman Khavronenko 2022-09-29 18:22:50 +02:00 committed by Aliaksandr Valialkin
parent c0aa10bd73
commit a2ded58600
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
3 changed files with 38 additions and 10 deletions

View file

@ -165,11 +165,15 @@ func (ar *AlertingRule) logDebugf(at time.Time, a *notifier.Alert, format string
} }
type labelSet struct { type labelSet struct {
// origin labels from series // origin labels extracted from received time series
// used for templating // plus extra labels (group labels, service labels like alertNameLabel).
// in case of conflicts, origin labels from time series preferred.
// used for templating annotations
origin map[string]string origin map[string]string
// processed labels with additional data // processed labels includes origin labels
// used as Alert labels // plus extra labels (group labels, service labels like alertNameLabel).
// in case of conflicts, extra labels are preferred.
// used as labels attached to notifier.Alert and ALERTS series written to remote storage.
processed map[string]string processed map[string]string
} }
@ -177,7 +181,7 @@ type labelSet struct {
// to labelSet which contains original and processed labels. // to labelSet which contains original and processed labels.
func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) { func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
ls := &labelSet{ ls := &labelSet{
origin: make(map[string]string, len(m.Labels)), origin: make(map[string]string),
processed: make(map[string]string), processed: make(map[string]string),
} }
for _, l := range m.Labels { for _, l := range m.Labels {
@ -199,14 +203,23 @@ func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*l
} }
for k, v := range extraLabels { for k, v := range extraLabels {
ls.processed[k] = v ls.processed[k] = v
if _, ok := ls.origin[k]; !ok {
ls.origin[k] = v
}
} }
// set additional labels to identify group and rule name // set additional labels to identify group and rule name
if ar.Name != "" { if ar.Name != "" {
ls.processed[alertNameLabel] = ar.Name ls.processed[alertNameLabel] = ar.Name
if _, ok := ls.origin[alertNameLabel]; !ok {
ls.origin[alertNameLabel] = ar.Name
}
} }
if !*disableAlertGroupLabel && ar.GroupName != "" { if !*disableAlertGroupLabel && ar.GroupName != "" {
ls.processed[alertGroupNameLabel] = ar.GroupName ls.processed[alertGroupNameLabel] = ar.GroupName
if _, ok := ls.origin[alertGroupNameLabel]; !ok {
ls.origin[alertGroupNameLabel] = ar.GroupName
}
} }
return ls, nil return ls, nil
} }

View file

@ -700,14 +700,26 @@ func TestAlertingRule_Template(t *testing.T) {
expAlerts map[uint64]*notifier.Alert expAlerts map[uint64]*notifier.Alert
}{ }{
{ {
newTestRuleWithLabels("common", "region", "east"), &AlertingRule{
Name: "common",
Labels: map[string]string{
"region": "east",
},
Annotations: map[string]string{
"summary": `{{ $labels.alertname }}: Too high connection number for "{{ $labels.instance }}"`,
},
alerts: make(map[uint64]*notifier.Alert),
state: newRuleState(),
},
[]datasource.Metric{ []datasource.Metric{
metricWithValueAndLabels(t, 1, "instance", "foo"), metricWithValueAndLabels(t, 1, "instance", "foo"),
metricWithValueAndLabels(t, 1, "instance", "bar"), metricWithValueAndLabels(t, 1, "instance", "bar"),
}, },
map[uint64]*notifier.Alert{ map[uint64]*notifier.Alert{
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "foo"}): { hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "foo"}): {
Annotations: map[string]string{}, Annotations: map[string]string{
"summary": `common: Too high connection number for "foo"`,
},
Labels: map[string]string{ Labels: map[string]string{
alertNameLabel: "common", alertNameLabel: "common",
"region": "east", "region": "east",
@ -715,7 +727,9 @@ func TestAlertingRule_Template(t *testing.T) {
}, },
}, },
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "bar"}): { hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "bar"}): {
Annotations: map[string]string{}, Annotations: map[string]string{
"summary": `common: Too high connection number for "bar"`,
},
Labels: map[string]string{ Labels: map[string]string{
alertNameLabel: "common", alertNameLabel: "common",
"region": "east", "region": "east",

View file

@ -9,11 +9,12 @@ groups:
denyPartialResponse: ["true"] denyPartialResponse: ["true"]
rules: rules:
- alert: Conns - alert: Conns
expr: sum(vm_tcplistener_conns) by(instance) > 1 expr: vm_tcplistener_conns > 0
for: 3m for: 3m
debug: true debug: true
annotations: annotations:
summary: Too high connection number for {{$labels.instance}} labels: "Available labels: {{ $labels }}"
summary: Too high connection number for {{ $labels.instance }}
{{ with printf "sum(vm_tcplistener_conns{instance=%q})" .Labels.instance | query }} {{ with printf "sum(vm_tcplistener_conns{instance=%q})" .Labels.instance | query }}
{{ . | first | value }} {{ . | first | value }}
{{ end }} {{ end }}