mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0c99730c20
commit
9e86916d48
@ -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)
|
||||
|
41
pkg/services/ngalert/state/template/funcs.go
Normal file
41
pkg/services/ngalert/state/template/funcs.go
Normal 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)
|
||||
}
|
||||
)
|
@ -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
|
||||
}
|
@ -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())
|
@ -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 ""
|
||||
}
|
Loading…
Reference in New Issue
Block a user