From 9e86916d482476bf79f0754e9fa00a4b61fb651d Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 16 Feb 2023 17:16:36 +0100 Subject: [PATCH] Alerting: Move templating to template package (#63347) This commit moves templating from the state package to a sub-package called template. This sub-package will be the logical package for future ease-of-use improvements to templating custom annotations and labels. --- pkg/services/ngalert/state/cache.go | 3 +- pkg/services/ngalert/state/template/funcs.go | 41 ++++++++ .../ngalert/state/{ => template}/template.go | 93 +++++++++---------- .../state/{ => template}/template_test.go | 26 +++--- .../ngalert/state/template_functions.go | 46 --------- 5 files changed, 104 insertions(+), 105 deletions(-) create mode 100644 pkg/services/ngalert/state/template/funcs.go rename pkg/services/ngalert/state/{ => template}/template.go (51%) rename pkg/services/ngalert/state/{ => template}/template_test.go (95%) delete mode 100644 pkg/services/ngalert/state/template_functions.go diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index 8b85d8b0cd1..12a62cb89e9 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state/template" ) type ruleStates struct { @@ -141,7 +142,7 @@ func (rs *ruleStates) expandRuleLabelsAndAnnotations(ctx context.Context, log lo expand := func(original map[string]string) map[string]string { expanded := make(map[string]string, len(original)) for k, v := range original { - ev, err := expandTemplate(ctx, alertRule.Title, v, templateLabels, alertInstance, externalURL) + ev, err := template.Expand(ctx, alertRule.Title, v, templateLabels, alertInstance, externalURL) expanded[k] = ev if err != nil { log.Error("Error in expanding template", "name", k, "value", v, "error", err) diff --git a/pkg/services/ngalert/state/template/funcs.go b/pkg/services/ngalert/state/template/funcs.go new file mode 100644 index 00000000000..4d89b606cfb --- /dev/null +++ b/pkg/services/ngalert/state/template/funcs.go @@ -0,0 +1,41 @@ +package template + +import ( + "encoding/json" + "fmt" + "net/url" + "text/template" +) + +type query struct { + Datasource string `json:"datasource"` + Expr string `json:"expr"` +} + +var ( + defaultFuncs = template.FuncMap{ + "graphLink": graphLink, + "tableLink": tableLink, + } +) + +var ( + graphLink = func(data string) string { + var q query + if err := json.Unmarshal([]byte(data), &q); err != nil { + return "" + } + datasource := url.QueryEscape(q.Datasource) + expr := url.QueryEscape(q.Expr) + return fmt.Sprintf(`/explore?left={"datasource":%[1]q,"queries":[{"datasource":%[1]q,"expr":%q,"instant":false,"range":true,"refId":"A"}],"range":{"from":"now-1h","to":"now"}}`, datasource, expr) + } + tableLink = func(data string) string { + var q query + if err := json.Unmarshal([]byte(data), &q); err != nil { + return "" + } + datasource := url.QueryEscape(q.Datasource) + expr := url.QueryEscape(q.Expr) + return fmt.Sprintf(`/explore?left={"datasource":%[1]q,"queries":[{"datasource":%[1]q,"expr":%q,"instant":true,"range":false,"refId":"A"}],"range":{"from":"now-1h","to":"now"}}`, datasource, expr) + } +) diff --git a/pkg/services/ngalert/state/template.go b/pkg/services/ngalert/state/template/template.go similarity index 51% rename from pkg/services/ngalert/state/template.go rename to pkg/services/ngalert/state/template/template.go index 70474b86db8..c0a92be548b 100644 --- a/pkg/services/ngalert/state/template.go +++ b/pkg/services/ngalert/state/template/template.go @@ -1,4 +1,4 @@ -package state +package template import ( "context" @@ -16,55 +16,19 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" ) -// templateCaptureValue represents each value in .Values in the annotations -// and labels template. -type templateCaptureValue struct { +// Value contains the labels and value of a Reduce, Math or Threshold +// expression for a series. +type Value struct { Labels map[string]string Value float64 } -// String implements the Stringer interface to print the value of each RefID -// in the template via {{ $values.A }} rather than {{ $values.A.Value }}. -func (v templateCaptureValue) String() string { +func (v Value) String() string { return strconv.FormatFloat(v.Value, 'f', -1, 64) } -func expandTemplate(ctx context.Context, name, text string, labels map[string]string, alertInstance eval.Result, externalURL *url.URL) (result string, resultErr error) { - name = "__alert_" + name - text = "{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}" + text - data := struct { - Labels map[string]string - Values map[string]templateCaptureValue - Value string - }{ - Labels: labels, - Values: newTemplateCaptureValues(alertInstance.Values), - Value: alertInstance.EvaluationString, - } - - expander := template.NewTemplateExpander( - ctx, // This context is only used with the `query()` function - which we don't support yet. - text, - name, - data, - model.Time(timestamp.FromTime(alertInstance.EvaluatedAt)), - func(context.Context, string, time.Time) (promql.Vector, error) { - return nil, nil - }, - externalURL, - []string{"missingkey=invalid"}, - ) - - expander.Funcs(FuncMap) - result, resultErr = expander.Expand() - // Replace missing key value to one that does not look like an HTML tag. This can cause problems downstream in some notifiers. - // For example, Telegram in HTML mode rejects requests with unsupported tags. - result = strings.ReplaceAll(result, "", "[no value]") - return result, resultErr -} - -func newTemplateCaptureValues(values map[string]eval.NumberValueCapture) map[string]templateCaptureValue { - m := make(map[string]templateCaptureValue) +func NewValues(values map[string]eval.NumberValueCapture) map[string]Value { + m := make(map[string]Value) for k, v := range values { var f float64 if v.Value != nil { @@ -72,7 +36,7 @@ func newTemplateCaptureValues(values map[string]eval.NumberValueCapture) map[str } else { f = math.NaN() } - m[k] = templateCaptureValue{ + m[k] = Value{ Labels: v.Labels, Value: f, } @@ -80,7 +44,42 @@ func newTemplateCaptureValues(values map[string]eval.NumberValueCapture) map[str return m } -type query struct { - Datasource string `json:"datasource"` - Expr string `json:"expr"` +func Expand( + ctx context.Context, + name, tmpl string, + labels map[string]string, + res eval.Result, + externalURL *url.URL) (string, error) { + name = "__alert_" + name + tmpl = "{{- $labels := .Labels -}}{{- $values := .Values -}}{{- $value := .Value -}}" + tmpl + data := struct { + Labels map[string]string + Values map[string]Value + Value string + }{ + Labels: labels, + Values: NewValues(res.Values), + Value: res.EvaluationString, + } + + expander := template.NewTemplateExpander( + ctx, // This context is only used with the `query()` function - which we don't support yet. + tmpl, + name, + data, + model.Time(timestamp.FromTime(res.EvaluatedAt)), + func(context.Context, string, time.Time) (promql.Vector, error) { + return nil, nil + }, + externalURL, + []string{"missingkey=invalid"}, + ) + expander.Funcs(defaultFuncs) + + result, err := expander.Expand() + // Replace missing key value to one that does not look like an HTML tag. This can cause problems downstream in some notifiers. + // For example, Telegram in HTML mode rejects requests with unsupported tags. + result = strings.ReplaceAll(result, "", "[no value]") + + return result, err } diff --git a/pkg/services/ngalert/state/template_test.go b/pkg/services/ngalert/state/template/template_test.go similarity index 95% rename from pkg/services/ngalert/state/template_test.go rename to pkg/services/ngalert/state/template/template_test.go index ae5538bfdc0..a1206281f37 100644 --- a/pkg/services/ngalert/state/template_test.go +++ b/pkg/services/ngalert/state/template/template_test.go @@ -1,4 +1,4 @@ -package state +package template import ( "context" @@ -14,28 +14,32 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" ) -func TestTemplateCaptureValueStringer(t *testing.T) { - cases := []struct { +func TestValueString(t *testing.T) { + tests := []struct { name string - value templateCaptureValue + value Value expected string }{{ name: "0 is returned as integer value", - value: templateCaptureValue{Value: 0}, + value: Value{Value: 0}, expected: "0", }, { name: "1.0 is returned as integer value", - value: templateCaptureValue{Value: 1.0}, + value: Value{Value: 1.0}, expected: "1", }, { name: "1.1 is returned as decimal value", - value: templateCaptureValue{Value: 1.1}, + value: Value{Value: 1.1}, + expected: "1.1", + }, { + name: "1.1 is returned as decimal value, no labels", + value: Value{Labels: map[string]string{"foo": "bar"}, Value: 1.1}, expected: "1.1", }} - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - assert.Equal(t, c.expected, c.value.String()) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, test.value.String()) }) } } @@ -405,7 +409,7 @@ func TestExpandTemplate(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - v, err := expandTemplate(context.Background(), "test", c.text, c.labels, c.alertInstance, externalURL) + v, err := Expand(context.Background(), "test", c.text, c.labels, c.alertInstance, externalURL) if c.expectedError != nil { require.NotNil(t, err) require.EqualError(t, c.expectedError, err.Error()) diff --git a/pkg/services/ngalert/state/template_functions.go b/pkg/services/ngalert/state/template_functions.go deleted file mode 100644 index 3f7c14b5e3c..00000000000 --- a/pkg/services/ngalert/state/template_functions.go +++ /dev/null @@ -1,46 +0,0 @@ -package state - -import ( - "encoding/json" - "fmt" - "net/url" - text_template "text/template" -) - -// FuncMap is a map of custom functions we use for templates. -var FuncMap = text_template.FuncMap{ - "graphLink": graphLink, - "tableLink": tableLink, - "strvalue": strValue, -} - -func graphLink(rawQuery string) string { - var q query - if err := json.Unmarshal([]byte(rawQuery), &q); err != nil { - return "" - } - - escapedExpression := url.QueryEscape(q.Expr) - escapedDatasource := url.QueryEscape(q.Datasource) - - return fmt.Sprintf( - `/explore?left={"datasource":%[1]q,"queries":[{"datasource":%[1]q,"expr":%q,"instant":false,"range":true,"refId":"A"}],"range":{"from":"now-1h","to":"now"}}`, escapedDatasource, escapedExpression) -} - -func tableLink(rawQuery string) string { - var q query - if err := json.Unmarshal([]byte(rawQuery), &q); err != nil { - return "" - } - - escapedExpression := url.QueryEscape(q.Expr) - escapedDatasource := url.QueryEscape(q.Datasource) - - return fmt.Sprintf( - `/explore?left={"datasource":%[1]q,"queries":[{"datasource":%[1]q,"expr":%q,"instant":true,"range":false,"refId":"A"}],"range":{"from":"now-1h","to":"now"}}`, escapedDatasource, escapedExpression) -} - -// This function is a no-op for now. -func strValue(value templateCaptureValue) string { - return "" -}