From 2f4c893cf3438040be244c95745d3d48c163aa7b Mon Sep 17 00:00:00 2001 From: George Robinson <85952834+gerobinson@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:20:44 +0100 Subject: [PATCH] Expand the value string in annotations and labels of alerts (#37051) This commit makes it possible to use the value string in annotations and labels for alerts with "{{ $value }}" --- .../create-grafana-managed-rule.md | 5 +- pkg/services/ngalert/state/cache.go | 14 ++-- pkg/services/ngalert/state/cache_test.go | 66 +++++++++++++++++++ 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md index 0e1d7604205..f38051b5ca8 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md @@ -109,8 +109,9 @@ The following template variables are available when expanding annotations and la | Name | Description | | ------- | --------------- | -| $labels | Labels contains the labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. | -| $values | Values contains the values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. | +| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. | +| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. | +| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. | ## Preview alerts diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index 89f370b4e00..f962d6c9ebd 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -39,7 +39,7 @@ func (c *cache) getOrCreate(alertRule *ngModels.AlertRule, result eval.Result) * // clone the labels so we don't change eval.Result labels := result.Instance.Copy() attachRuleLabels(labels, alertRule) - ruleLabels, annotations := c.expandRuleLabelsAndAnnotations(alertRule, labels, result.Values) + ruleLabels, annotations := c.expandRuleLabelsAndAnnotations(alertRule, labels, result) // if duplicate labels exist, alertRule label will take precedence lbs := mergeLabels(ruleLabels, result.Instance) @@ -88,11 +88,11 @@ func attachRuleLabels(m map[string]string, alertRule *ngModels.AlertRule) { m[prometheusModel.AlertNameLabel] = alertRule.Title } -func (c *cache) expandRuleLabelsAndAnnotations(alertRule *ngModels.AlertRule, labels map[string]string, values map[string]eval.NumberValueCapture) (map[string]string, map[string]string) { +func (c *cache) expandRuleLabelsAndAnnotations(alertRule *ngModels.AlertRule, labels map[string]string, alertInstance eval.Result) (map[string]string, map[string]string) { expand := func(original map[string]string) map[string]string { expanded := make(map[string]string, len(original)) for k, v := range original { - ev, err := expandTemplate(alertRule.Title, v, labels, values) + ev, err := expandTemplate(alertRule.Title, v, labels, alertInstance) expanded[k] = ev if err != nil { c.log.Error("error in expanding template", "name", k, "value", v, "err", err.Error()) @@ -122,9 +122,9 @@ func (v templateCaptureValue) String() string { return "null" } -func expandTemplate(name, text string, labels map[string]string, values map[string]eval.NumberValueCapture) (result string, resultErr error) { +func expandTemplate(name, text string, labels map[string]string, alertInstance eval.Result) (result string, resultErr error) { name = "__alert_" + name - text = "{{- $labels := .Labels -}}{{- $values := .Values -}}" + text + text = "{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}" + text // It'd better to have no alert description than to kill the whole process // if there's a bug in the template. defer func() { @@ -145,11 +145,12 @@ func expandTemplate(name, text string, labels map[string]string, values map[stri if err := tmpl.Execute(&buffer, struct { Labels map[string]string Values map[string]templateCaptureValue + Value string }{ Labels: labels, Values: func() map[string]templateCaptureValue { m := make(map[string]templateCaptureValue) - for k, v := range values { + for k, v := range alertInstance.Values { m[k] = templateCaptureValue{ Labels: v.Labels, Value: v.Value, @@ -157,6 +158,7 @@ func expandTemplate(name, text string, labels map[string]string, values map[stri } return m }(), + Value: alertInstance.EvaluationString, }); err != nil { return "", fmt.Errorf("error executing template %v: %s", name, err.Error()) } diff --git a/pkg/services/ngalert/state/cache_test.go b/pkg/services/ngalert/state/cache_test.go index bb54b7823d3..946a2c6352b 100644 --- a/pkg/services/ngalert/state/cache_test.go +++ b/pkg/services/ngalert/state/cache_test.go @@ -1,9 +1,13 @@ package state import ( + "errors" "testing" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ptr "github.com/xorcare/pointer" ) @@ -32,3 +36,65 @@ func TestTemplateCaptureValueStringer(t *testing.T) { }) } } + +func TestExpandTemplate(t *testing.T) { + cases := []struct { + name string + text string + alertInstance eval.Result + labels data.Labels + expected string + expectedError error + }{{ + name: "instance labels are expanded into $labels", + text: "{{ $labels.instance }} is down", + labels: data.Labels{"instance": "foo"}, + expected: "foo is down", + }, { + name: "missing instance label returns error", + text: "{{ $labels.instance }} is down", + labels: data.Labels{}, + expectedError: errors.New("error executing template __alert_test: template: __alert_test:1:86: executing \"__alert_test\" at <$labels.instance>: map has no entry for key \"instance\""), + }, { + name: "values are expanded into $values", + text: "{{ $values.A.Labels.instance }} has value {{ $values.A }}", + alertInstance: eval.Result{ + Values: map[string]eval.NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{"instance": "foo"}, + Value: ptr.Float64(10), + }, + }, + }, + expected: "foo has value 10", + }, { + name: "missing label in $values returns error", + text: "{{ $values.A.Labels.instance }} has value {{ $values.A }}", + alertInstance: eval.Result{ + Values: map[string]eval.NumberValueCapture{ + "A": { + Var: "A", + Labels: data.Labels{}, + Value: ptr.Float64(10), + }, + }, + }, + expectedError: errors.New("error executing template __alert_test: template: __alert_test:1:86: executing \"__alert_test\" at <$values.A.Labels.instance>: map has no entry for key \"instance\""), + }, { + name: "value string is expanded into $value", + text: "{{ $value }}", + alertInstance: eval.Result{ + EvaluationString: "[ var='A' labels={instance=foo} value=10 ]", + }, + expected: "[ var='A' labels={instance=foo} value=10 ]", + }} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + v, err := expandTemplate("test", c.text, c.labels, c.alertInstance) + require.Equal(t, c.expectedError, err) + require.Equal(t, c.expected, v) + }) + } +}