mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: add template funcs (#38404)
* Alerting: (wip) add template funcs * Alerting: (wip) numeric template functions * Alerting: (wip) template functions * Test for the "args" function * Alerting: (wip) Documentation for template functions * Alerting: template functions - refactor * code review changes * disable linter error * Use Prometheus implementation of TemplateExpander * Update docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
parent
cde133fe05
commit
d6fb0181fb
@ -112,6 +112,23 @@ The following template variables are available when expanding annotations and la
|
||||
| $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. This is unavailable when the rule uses a classic condition. |
|
||||
| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. |
|
||||
|
||||
#### Template functions
|
||||
|
||||
The following template functions are available when expanding annotations and labels.
|
||||
|
||||
| Name | Argument | Return | Description |
|
||||
| ------------------ | ---------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| humanize | number or string | string | Converts a number to a more readable format, using metric prefixes. |
|
||||
| humanize1024 | number or string | string | Like humanize, but uses 1024 as the base rather than 1000. |
|
||||
| humanizeDuration | number or string | string | Converts a duration in seconds to a more readable format. |
|
||||
| humanizePercentage | number or string | string | Converts a ratio value to a fraction of 100. |
|
||||
| humanizeTimestamp | number or string | string | Converts a Unix timestamp in seconds to a more readable format. |
|
||||
| title | string | string | strings.Title, capitalises first character of each word. |
|
||||
| toUpper | string | string | strings.ToUpper, converts all characters to upper case. |
|
||||
| toLower | string | string | strings.ToLower, converts all characters to lower case. |
|
||||
| match | pattern, text | boolean | Regexp.ReplaceAllString Regexp substitution, unanchored. |
|
||||
| args | []interface{} | map[string]interface{} | Converts a list of objects to a map with keys, for example, arg0, arg1. Use this function to pass multiple arguments to templates. |
|
||||
|
||||
## Preview alerts
|
||||
|
||||
To evaluate the rule and see what alerts it would produce, click **Preview alerts**. It will display a list of alerts with state and value for each one.
|
||||
|
41
go.mod
41
go.mod
@ -19,7 +19,7 @@ require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
|
||||
github.com/aws/aws-sdk-go v1.40.11
|
||||
github.com/aws/aws-sdk-go v1.40.37
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/benbjohnson/clock v1.1.0
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
@ -27,14 +27,14 @@ require (
|
||||
github.com/cortexproject/cortex v1.8.2-0.20210428155238-d382e1d80eaf
|
||||
github.com/crewjam/saml v0.4.6-0.20201227203850-bca570abb2ce
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204
|
||||
github.com/denisenkom/go-mssqldb v0.10.0
|
||||
github.com/dop251/goja v0.0.0-20210804101310-32956a348b49
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/gchaincl/sqlhooks v1.3.0
|
||||
github.com/getsentry/sentry-go v0.10.0
|
||||
github.com/go-kit/kit v0.11.0
|
||||
github.com/go-macaron/binding v0.0.0-20190806013118-0b4f37bab25b
|
||||
github.com/go-openapi/strfmt v0.20.1
|
||||
github.com/go-openapi/strfmt v0.20.2
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-stack/stack v1.8.0
|
||||
@ -56,7 +56,7 @@ require (
|
||||
github.com/hashicorp/go-plugin v1.4.2
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.2.3
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040
|
||||
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097
|
||||
github.com/jmespath/go-jmespath v0.4.0
|
||||
github.com/json-iterator/go v1.1.11
|
||||
@ -68,17 +68,17 @@ require (
|
||||
github.com/magefile/mage v1.11.0
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/browser v0.0.0-20210904010418-6d279e18f982 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/alertmanager v0.23.0-rc.0.0.20210906104939-8da517524a87
|
||||
github.com/prometheus/alertmanager v0.23.1-0.20210914172521-e35efbddb66a
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.30.0
|
||||
github.com/prometheus/prometheus v1.8.2-0.20210621150501-ff58416a0b02
|
||||
github.com/prometheus/prometheus v1.8.2-0.20210915140241-bd217c58a735
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/goxmldsig v1.1.0
|
||||
@ -98,13 +98,13 @@ require (
|
||||
go.opentelemetry.io/collector/model v0.31.0
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a
|
||||
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
||||
golang.org/x/tools v0.1.5
|
||||
gonum.org/v1/gonum v0.9.1
|
||||
google.golang.org/api v0.54.0
|
||||
google.golang.org/api v0.56.0
|
||||
google.golang.org/grpc v1.40.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
@ -121,7 +121,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.90.0 // indirect
|
||||
cloud.google.com/go v0.93.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 // indirect
|
||||
github.com/FZambia/eagle v0.0.1 // indirect
|
||||
github.com/FZambia/sentinel v1.1.0 // indirect
|
||||
@ -136,11 +136,12 @@ require (
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
github.com/centrifugal/protocol v0.7.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.3.13 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.6.0 // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
@ -148,7 +149,7 @@ require (
|
||||
github.com/emicklei/proto v1.6.15 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/go-kit/log v0.1.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-openapi/analysis v0.20.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
@ -165,8 +166,8 @@ require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/flatbuffers v1.12.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
|
||||
github.com/google/flatbuffers v2.0.0+incompatible // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.0 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
@ -183,7 +184,6 @@ require (
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/jstemmer/go-junit-report v0.9.1 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
@ -191,7 +191,7 @@ require (
|
||||
github.com/mattetti/filebuffer v1.0.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/miekg/dns v1.1.42 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/mna/redisc v1.2.1 // indirect
|
||||
@ -230,12 +230,11 @@ require (
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/goleak v1.1.10 // indirect
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||
xorm.io/builder v0.3.6 // indirect
|
||||
)
|
||||
|
@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package process
|
||||
|
@ -1,3 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package process
|
||||
|
@ -1,13 +1,9 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
text_template "text/template"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
@ -107,70 +103,6 @@ func (c *cache) expandRuleLabelsAndAnnotations(alertRule *ngModels.AlertRule, la
|
||||
return expand(alertRule.Labels), expand(alertRule.Annotations)
|
||||
}
|
||||
|
||||
// templateCaptureValue represents each value in .Values in the annotations
|
||||
// and labels template.
|
||||
type templateCaptureValue 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 {
|
||||
return strconv.FormatFloat(v.Value, 'f', -1, 64)
|
||||
}
|
||||
|
||||
func expandTemplate(name, text string, labels map[string]string, alertInstance eval.Result) (result string, resultErr error) {
|
||||
name = "__alert_" + name
|
||||
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() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
resultErr, ok = r.(error)
|
||||
if !ok {
|
||||
resultErr = fmt.Errorf("panic expanding template %v: %v", name, r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
tmpl, err := text_template.New(name).Option("missingkey=error").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing template %v: %s", name, err.Error())
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
if err := tmpl.Execute(&buffer, struct {
|
||||
Labels map[string]string
|
||||
Values map[string]templateCaptureValue
|
||||
Value string
|
||||
}{
|
||||
Labels: labels,
|
||||
Values: newTemplateCaptureValues(alertInstance.Values),
|
||||
Value: alertInstance.EvaluationString,
|
||||
}); err != nil {
|
||||
return "", fmt.Errorf("error executing template %v: %s", name, err.Error())
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func newTemplateCaptureValues(values map[string]eval.NumberValueCapture) map[string]templateCaptureValue {
|
||||
m := make(map[string]templateCaptureValue)
|
||||
for k, v := range values {
|
||||
var f float64
|
||||
if v.Value != nil {
|
||||
f = *v.Value
|
||||
} else {
|
||||
f = math.NaN()
|
||||
}
|
||||
m[k] = templateCaptureValue{
|
||||
Labels: v.Labels,
|
||||
Value: f,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *cache) set(entry *State) {
|
||||
c.mtxStates.Lock()
|
||||
defer c.mtxStates.Unlock()
|
||||
|
@ -1,126 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestTemplateCaptureValueStringer(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
value templateCaptureValue
|
||||
expected string
|
||||
}{{
|
||||
name: "0 is returned as integer value",
|
||||
value: templateCaptureValue{Value: 0},
|
||||
expected: "0",
|
||||
}, {
|
||||
name: "1.0 is returned as integer value",
|
||||
value: templateCaptureValue{Value: 1.0},
|
||||
expected: "1",
|
||||
}, {
|
||||
name: "1.1 is returned as decimal value",
|
||||
value: templateCaptureValue{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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandTemplate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
text string
|
||||
alertInstance eval.Result
|
||||
labels data.Labels
|
||||
expected string
|
||||
expectedError error
|
||||
}{{
|
||||
name: "labels are expanded into $labels",
|
||||
text: "{{ $labels.instance }} is down",
|
||||
labels: data.Labels{"instance": "foo"},
|
||||
expected: "foo is down",
|
||||
}, {
|
||||
name: "missing label in $labels 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(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foo has value 1",
|
||||
}, {
|
||||
name: "values can be passed to template functions such as printf",
|
||||
text: "{{ $values.A.Labels.instance }} has value {{ $values.A.Value | printf \"%.1f\" }}",
|
||||
alertInstance: eval.Result{
|
||||
Values: map[string]eval.NumberValueCapture{
|
||||
"A": {
|
||||
Var: "A",
|
||||
Labels: data.Labels{"instance": "foo"},
|
||||
Value: ptr.Float64(1.1),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foo has value 1.1",
|
||||
}, {
|
||||
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(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
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: "missing value in $values is returned as NaN",
|
||||
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: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foo has value NaN",
|
||||
}, {
|
||||
name: "assert 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)
|
||||
})
|
||||
}
|
||||
}
|
69
pkg/services/ngalert/state/template.go
Normal file
69
pkg/services/ngalert/state/template.go
Normal file
@ -0,0 +1,69 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/pkg/timestamp"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/template"
|
||||
)
|
||||
|
||||
// templateCaptureValue represents each value in .Values in the annotations
|
||||
// and labels template.
|
||||
type templateCaptureValue 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 {
|
||||
if v.Value != nil {
|
||||
return strconv.FormatFloat(*v.Value, 'f', -1, 64)
|
||||
}
|
||||
return "null"
|
||||
}
|
||||
|
||||
func expandTemplate(name, text string, labels map[string]string, alertInstance eval.Result) (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: newTemplateCaptureValueMap(alertInstance.Values),
|
||||
Value: alertInstance.EvaluationString,
|
||||
}
|
||||
|
||||
expander := template.NewTemplateExpander(
|
||||
context.TODO(),
|
||||
text,
|
||||
name,
|
||||
data,
|
||||
model.Time(timestamp.FromTime(time.Now())),
|
||||
func(context.Context, string, time.Time) (promql.Vector, error) {
|
||||
return nil, nil
|
||||
},
|
||||
nil,
|
||||
[]string{"missingkey=error"},
|
||||
)
|
||||
|
||||
return expander.Expand()
|
||||
}
|
||||
|
||||
func newTemplateCaptureValueMap(values map[string]eval.NumberValueCapture) map[string]templateCaptureValue {
|
||||
m := make(map[string]templateCaptureValue)
|
||||
for k, v := range values {
|
||||
m[k] = templateCaptureValue{
|
||||
Labels: v.Labels,
|
||||
Value: v.Value,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
220
pkg/services/ngalert/state/template_test.go
Normal file
220
pkg/services/ngalert/state/template_test.go
Normal file
@ -0,0 +1,220 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestTemplateCaptureValueStringer(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
value templateCaptureValue
|
||||
expected string
|
||||
}{{
|
||||
name: "nil value returns null",
|
||||
value: templateCaptureValue{Value: nil},
|
||||
expected: "null",
|
||||
}, {
|
||||
name: "1.0 is returned as integer value",
|
||||
value: templateCaptureValue{Value: ptr.Float64(1.0)},
|
||||
expected: "1",
|
||||
}, {
|
||||
name: "1.1 is returned as decimal value",
|
||||
value: templateCaptureValue{Value: ptr.Float64(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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 ]",
|
||||
}, {
|
||||
name: "float64 is humanized correctly",
|
||||
text: "{{ humanize $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "1234567.0",
|
||||
},
|
||||
expected: "1.235M",
|
||||
}, {
|
||||
name: "int is humanized correctly",
|
||||
text: "{{ humanize $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "1234567",
|
||||
},
|
||||
expected: "1.235M",
|
||||
}, {
|
||||
name: "humanize string with error",
|
||||
text: `{{ humanize $value }}`,
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "invalid",
|
||||
},
|
||||
expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at <humanize $value>: error calling humanize: strconv.ParseFloat: parsing "invalid": invalid syntax`),
|
||||
}, {
|
||||
name: "humanize1024 float",
|
||||
text: "{{ humanize1024 $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "1048576.0",
|
||||
},
|
||||
expected: "1Mi",
|
||||
}, {
|
||||
name: "humanize1024 string with error",
|
||||
text: "{{ humanize1024 $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "invalid",
|
||||
},
|
||||
expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at <humanize1024 $value>: error calling humanize1024: strconv.ParseFloat: parsing "invalid": invalid syntax`),
|
||||
}, {
|
||||
name: "humanizeDuration - int",
|
||||
text: "{{ humanizeDuration $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "86400",
|
||||
},
|
||||
expected: "1d 0h 0m 0s",
|
||||
}, {
|
||||
name: "humanizeDuration - fractional subsecond",
|
||||
text: "{{ humanizeDuration $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: ".12345",
|
||||
},
|
||||
expected: "123.5ms",
|
||||
}, {
|
||||
name: "humanizeDuration - subsecond",
|
||||
text: "{{ humanizeDuration $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: ".0001",
|
||||
},
|
||||
expected: "100us",
|
||||
}, {
|
||||
name: "humanizeDuration - fractional seconds",
|
||||
text: "{{ humanizeDuration $value }}",
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "1.2345",
|
||||
},
|
||||
expected: "1.234s",
|
||||
}, {
|
||||
name: "humanizeDuration - string with error",
|
||||
text: `{{ humanizeDuration $value }}`,
|
||||
alertInstance: eval.Result{
|
||||
EvaluationString: "invalid",
|
||||
},
|
||||
expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:79: executing "__alert_test" at <humanizeDuration $value>: error calling humanizeDuration: strconv.ParseFloat: parsing "invalid": invalid syntax`),
|
||||
}, {
|
||||
name: "humanizePercentage - float64",
|
||||
text: "{{ -0.22222 | humanizePercentage }}:{{ 0.0 | humanizePercentage }}:{{ 0.1234567 | humanizePercentage }}:{{ 1.23456 | humanizePercentage }}",
|
||||
expected: "-22.22%:0%:12.35%:123.5%",
|
||||
}, {
|
||||
name: "humanizePercentage - string",
|
||||
text: `{{ "-0.22222" | humanizePercentage }}:{{ "0.0" | humanizePercentage }}:{{ "0.1234567" | humanizePercentage }}:{{ "1.23456" | humanizePercentage }}`,
|
||||
expected: "-22.22%:0%:12.35%:123.5%",
|
||||
}, {
|
||||
name: "humanizePercentage - string with error",
|
||||
text: `{{ "invalid" | humanizePercentage }}`,
|
||||
expectedError: errors.New(`error executing template __alert_test: template: __alert_test:1:91: executing "__alert_test" at <humanizePercentage>: error calling humanizePercentage: strconv.ParseFloat: parsing "invalid": invalid syntax`),
|
||||
}, {
|
||||
name: "humanizeTimestamp - float64",
|
||||
text: "{{ 1435065584.128 | humanizeTimestamp }}",
|
||||
expected: "2015-06-23 13:19:44.128 +0000 UTC",
|
||||
}, {
|
||||
name: "humanizeTimestamp - string",
|
||||
text: `{{ "1435065584.128" | humanizeTimestamp }}`,
|
||||
expected: "2015-06-23 13:19:44.128 +0000 UTC",
|
||||
}, {
|
||||
name: "title",
|
||||
text: `{{ "aa bb CC" | title }}`,
|
||||
expected: "Aa Bb CC",
|
||||
}, {
|
||||
name: "toUpper",
|
||||
text: `{{ "aa bb CC" | toUpper }}`,
|
||||
expected: "AA BB CC",
|
||||
}, {
|
||||
name: "toLower",
|
||||
text: `{{ "aA bB CC" | toLower }}`,
|
||||
expected: "aa bb cc",
|
||||
}, {
|
||||
name: "match",
|
||||
text: `{{ match "a+" "aa" }} {{ match "a+" "b" }}`,
|
||||
expected: "true false",
|
||||
}, {
|
||||
name: "match",
|
||||
text: `{{ match "a+" "aa" }} {{ match "a+" "b" }}`,
|
||||
expected: "true false",
|
||||
}, {
|
||||
name: "pass multiple arguments to templates",
|
||||
text: `{{define "x"}}{{.arg0}} {{.arg1}}{{end}}{{template "x" (args 1 "2")}}`,
|
||||
expected: "1 2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
v, err := expandTemplate("test", c.text, c.labels, c.alertInstance)
|
||||
if c.expectedError != nil {
|
||||
require.NotNil(t, err)
|
||||
require.EqualError(t, c.expectedError, err.Error())
|
||||
} else {
|
||||
require.Nil(t, c.expectedError)
|
||||
}
|
||||
require.Equal(t, c.expected, v)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user