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/eval"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||||
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/state/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ruleStates struct {
|
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 {
|
expand := func(original map[string]string) map[string]string {
|
||||||
expanded := make(map[string]string, len(original))
|
expanded := make(map[string]string, len(original))
|
||||||
for k, v := range 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
|
expanded[k] = ev
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error in expanding template", "name", k, "value", v, "error", err)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -16,55 +16,19 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||||
)
|
)
|
||||||
|
|
||||||
// templateCaptureValue represents each value in .Values in the annotations
|
// Value contains the labels and value of a Reduce, Math or Threshold
|
||||||
// and labels template.
|
// expression for a series.
|
||||||
type templateCaptureValue struct {
|
type Value struct {
|
||||||
Labels map[string]string
|
Labels map[string]string
|
||||||
Value float64
|
Value float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements the Stringer interface to print the value of each RefID
|
func (v Value) String() string {
|
||||||
// in the template via {{ $values.A }} rather than {{ $values.A.Value }}.
|
|
||||||
func (v templateCaptureValue) String() string {
|
|
||||||
return strconv.FormatFloat(v.Value, 'f', -1, 64)
|
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) {
|
func NewValues(values map[string]eval.NumberValueCapture) map[string]Value {
|
||||||
name = "__alert_" + name
|
m := make(map[string]Value)
|
||||||
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)
|
|
||||||
for k, v := range values {
|
for k, v := range values {
|
||||||
var f float64
|
var f float64
|
||||||
if v.Value != nil {
|
if v.Value != nil {
|
||||||
@ -72,7 +36,7 @@ func newTemplateCaptureValues(values map[string]eval.NumberValueCapture) map[str
|
|||||||
} else {
|
} else {
|
||||||
f = math.NaN()
|
f = math.NaN()
|
||||||
}
|
}
|
||||||
m[k] = templateCaptureValue{
|
m[k] = Value{
|
||||||
Labels: v.Labels,
|
Labels: v.Labels,
|
||||||
Value: f,
|
Value: f,
|
||||||
}
|
}
|
||||||
@ -80,7 +44,42 @@ func newTemplateCaptureValues(values map[string]eval.NumberValueCapture) map[str
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
type query struct {
|
func Expand(
|
||||||
Datasource string `json:"datasource"`
|
ctx context.Context,
|
||||||
Expr string `json:"expr"`
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -14,28 +14,32 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTemplateCaptureValueStringer(t *testing.T) {
|
func TestValueString(t *testing.T) {
|
||||||
cases := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
value templateCaptureValue
|
value Value
|
||||||
expected string
|
expected string
|
||||||
}{{
|
}{{
|
||||||
name: "0 is returned as integer value",
|
name: "0 is returned as integer value",
|
||||||
value: templateCaptureValue{Value: 0},
|
value: Value{Value: 0},
|
||||||
expected: "0",
|
expected: "0",
|
||||||
}, {
|
}, {
|
||||||
name: "1.0 is returned as integer value",
|
name: "1.0 is returned as integer value",
|
||||||
value: templateCaptureValue{Value: 1.0},
|
value: Value{Value: 1.0},
|
||||||
expected: "1",
|
expected: "1",
|
||||||
}, {
|
}, {
|
||||||
name: "1.1 is returned as decimal value",
|
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",
|
expected: "1.1",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, test := range tests {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
assert.Equal(t, c.expected, c.value.String())
|
assert.Equal(t, test.expected, test.value.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,7 +409,7 @@ func TestExpandTemplate(t *testing.T) {
|
|||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
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 {
|
if c.expectedError != nil {
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
require.EqualError(t, c.expectedError, err.Error())
|
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