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.
This commit is contained in:
George Robinson 2023-02-16 17:16:36 +01:00 committed by GitHub
parent 0c99730c20
commit 9e86916d48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 105 deletions

View File

@ -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)

View File

@ -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)
}
)

View File

@ -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>", "[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>", "[no value]")
return result, err
}

View File

@ -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())

View File

@ -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 ""
}