mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-22 16:36:27 +01:00
7ae038766c
Previously the `quotesEscape` function was escaping only double quotes. This wasn't enough, since the input string could contain other special chars, which must be escaped when put inside JSON string. For example, carriage return and line feed chars (\n\r), backslash char, etc. This led to the following issues, which were improperly fixed: - https://github.com/VictoriaMetrics/VictoriaMetrics/issues/890 - this issue was "fixed" by introducing the `crlfEscape` function, which led to unnecessary complications in user templates, while not fixing various corner cases such as backslash chars in the input string. See1de15ad490
- https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3139 - this issue was "fixed" by urlencoding the whole string passed to -external.alert.source command-line flag. This led to invalid urls, which couldn't be parsed by Grafana. See00c838353d
and4bd0244599
This commit properly encodes the input string passed to `quotesEscape`, so it can be safely embedded inside JSON strings. This commit deprecates crlfEscape template function and adds the following new template functions: - strvalue and stripDomain - these functions are supported by Prometheus, so they were added for compatibility purposes. - jsonEscape and htmlEscape for converting the input string to valid quoted JSON string and for html-escaping the input string, so it could be safely embedded as a plaintext into html. This commit also documents all supported template functions at https://docs.victoriametrics.com/vmalert.html#template-functions The deprecated crlfEscape function isn't documented on purpose, since its usefulness is negative in general case.
263 lines
7.0 KiB
Go
263 lines
7.0 KiB
Go
package notifier
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
|
)
|
|
|
|
func TestAlert_ExecTemplate(t *testing.T) {
|
|
extLabels := make(map[string]string)
|
|
const (
|
|
extCluster = "prod"
|
|
extDC = "east"
|
|
extURL = "https://foo.bar"
|
|
)
|
|
extLabels["cluster"] = extCluster
|
|
extLabels["dc"] = extDC
|
|
_, err := Init(nil, extLabels, extURL)
|
|
checkErr(t, err)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
alert *Alert
|
|
annotations map[string]string
|
|
expTpl map[string]string
|
|
}{
|
|
{
|
|
name: "empty-alert",
|
|
alert: &Alert{},
|
|
annotations: map[string]string{},
|
|
expTpl: map[string]string{},
|
|
},
|
|
{
|
|
name: "no-template",
|
|
alert: &Alert{
|
|
Value: 1e4,
|
|
Labels: map[string]string{
|
|
"instance": "localhost",
|
|
},
|
|
},
|
|
annotations: map[string]string{},
|
|
expTpl: map[string]string{},
|
|
},
|
|
{
|
|
name: "label-template",
|
|
alert: &Alert{
|
|
Value: 1e4,
|
|
Labels: map[string]string{
|
|
"job": "staging",
|
|
"instance": "localhost",
|
|
},
|
|
},
|
|
annotations: map[string]string{
|
|
"summary": "Too high connection number for {{$labels.instance}} for job {{$labels.job}}",
|
|
"description": "It is {{ $value }} connections for {{$labels.instance}}",
|
|
},
|
|
expTpl: map[string]string{
|
|
"summary": "Too high connection number for localhost for job staging",
|
|
"description": "It is 10000 connections for localhost",
|
|
},
|
|
},
|
|
{
|
|
name: "expression-template",
|
|
alert: &Alert{
|
|
Expr: `vm_rows{"label"="bar"}<0`,
|
|
},
|
|
annotations: map[string]string{
|
|
"exprEscapedQuery": "{{ $expr|queryEscape }}",
|
|
"exprEscapedPath": "{{ $expr|pathEscape }}",
|
|
"exprEscapedJSON": "{{ $expr|jsonEscape }}",
|
|
"exprEscapedQuotes": "{{ $expr|quotesEscape }}",
|
|
"exprEscapedHTML": "{{ $expr|htmlEscape }}",
|
|
},
|
|
expTpl: map[string]string{
|
|
"exprEscapedQuery": "vm_rows%7B%22label%22%3D%22bar%22%7D%3C0",
|
|
"exprEscapedPath": "vm_rows%7B%22label%22=%22bar%22%7D%3C0",
|
|
"exprEscapedJSON": `"vm_rows{\"label\"=\"bar\"}\u003c0"`,
|
|
"exprEscapedQuotes": `vm_rows{\"label\"=\"bar\"}\u003c0`,
|
|
"exprEscapedHTML": "vm_rows{"label"="bar"}<0",
|
|
},
|
|
},
|
|
{
|
|
name: "query",
|
|
alert: &Alert{Expr: `vm_rows{"label"="bar"}>0`},
|
|
annotations: map[string]string{
|
|
"summary": `{{ query "foo" | first | value }}`,
|
|
"desc": `{{ range query "bar" }}{{ . | label "foo" }} {{ . | value }};{{ end }}`,
|
|
},
|
|
expTpl: map[string]string{
|
|
"summary": "1",
|
|
"desc": "bar 1;garply 2;",
|
|
},
|
|
},
|
|
{
|
|
name: "external",
|
|
alert: &Alert{
|
|
Value: 1e4,
|
|
Labels: map[string]string{
|
|
"job": "staging",
|
|
"instance": "localhost",
|
|
},
|
|
},
|
|
annotations: map[string]string{
|
|
"url": "{{ $externalURL }}",
|
|
"summary": "Issues with {{$labels.instance}} (dc-{{$externalLabels.dc}}) for job {{$labels.job}}",
|
|
"description": "It is {{ $value }} connections for {{$labels.instance}} (cluster-{{$externalLabels.cluster}})",
|
|
},
|
|
expTpl: map[string]string{
|
|
"url": extURL,
|
|
"summary": fmt.Sprintf("Issues with localhost (dc-%s) for job staging", extDC),
|
|
"description": fmt.Sprintf("It is 10000 connections for localhost (cluster-%s)", extCluster),
|
|
},
|
|
},
|
|
{
|
|
name: "alert and group IDs",
|
|
alert: &Alert{
|
|
ID: 42,
|
|
GroupID: 24,
|
|
},
|
|
annotations: map[string]string{
|
|
"url": "/api/v1/alert?alertID={{$alertID}}&groupID={{$groupID}}",
|
|
},
|
|
expTpl: map[string]string{
|
|
"url": "/api/v1/alert?alertID=42&groupID=24",
|
|
},
|
|
},
|
|
{
|
|
name: "ActiveAt time",
|
|
alert: &Alert{
|
|
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
|
|
},
|
|
annotations: map[string]string{
|
|
"diagram": "![](http://example.com?render={{$activeAt.Unix}}",
|
|
},
|
|
expTpl: map[string]string{
|
|
"diagram": "![](http://example.com?render=1660941298",
|
|
},
|
|
},
|
|
{
|
|
name: "ActiveAt time is nil",
|
|
alert: &Alert{},
|
|
annotations: map[string]string{
|
|
"default_time": "{{$activeAt}}",
|
|
},
|
|
expTpl: map[string]string{
|
|
"default_time": "0001-01-01 00:00:00 +0000 UTC",
|
|
},
|
|
},
|
|
{
|
|
name: "ActiveAt custome format",
|
|
alert: &Alert{
|
|
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
|
|
},
|
|
annotations: map[string]string{
|
|
"fire_time": `{{$activeAt.Format "2006/01/02 15:04:05"}}`,
|
|
},
|
|
expTpl: map[string]string{
|
|
"fire_time": "2022/08/19 20:34:58",
|
|
},
|
|
},
|
|
{
|
|
name: "ActiveAt query range",
|
|
alert: &Alert{
|
|
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
|
|
},
|
|
annotations: map[string]string{
|
|
"grafana_url": `vm-grafana.com?from={{($activeAt.Add (parseDurationTime "1h")).Unix}}&to={{($activeAt.Add (parseDurationTime "-1h")).Unix}}`,
|
|
},
|
|
expTpl: map[string]string{
|
|
"grafana_url": "vm-grafana.com?from=1660944898&to=1660937698",
|
|
},
|
|
},
|
|
}
|
|
|
|
qFn := func(q string) ([]datasource.Metric, error) {
|
|
return []datasource.Metric{
|
|
{
|
|
Labels: []datasource.Label{
|
|
{Name: "foo", Value: "bar"},
|
|
{Name: "baz", Value: "qux"},
|
|
},
|
|
Values: []float64{1},
|
|
Timestamps: []int64{1},
|
|
},
|
|
{
|
|
Labels: []datasource.Label{
|
|
{Name: "foo", Value: "garply"},
|
|
{Name: "baz", Value: "fred"},
|
|
},
|
|
Values: []float64{2},
|
|
Timestamps: []int64{1},
|
|
},
|
|
}, nil
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tpl, err := tc.alert.ExecTemplate(qFn, tc.alert.Labels, tc.annotations)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tpl) != len(tc.expTpl) {
|
|
t.Fatalf("expected %d elements; got %d", len(tc.expTpl), len(tpl))
|
|
}
|
|
for k := range tc.expTpl {
|
|
got, exp := tpl[k], tc.expTpl[k]
|
|
if got != exp {
|
|
t.Fatalf("expected %q=%q; got %q=%q", k, exp, k, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAlert_toPromLabels(t *testing.T) {
|
|
fn := func(labels map[string]string, exp []prompbmarshal.Label, relabel *promrelabel.ParsedConfigs) {
|
|
t.Helper()
|
|
a := Alert{Labels: labels}
|
|
got := a.toPromLabels(relabel)
|
|
if !reflect.DeepEqual(got, exp) {
|
|
t.Fatalf("expected to have: \n%v;\ngot:\n%v",
|
|
exp, got)
|
|
}
|
|
}
|
|
|
|
fn(nil, nil, nil)
|
|
fn(
|
|
map[string]string{"foo": "bar", "a": "baz"}, // unsorted
|
|
[]prompbmarshal.Label{{Name: "a", Value: "baz"}, {Name: "foo", Value: "bar"}},
|
|
nil,
|
|
)
|
|
|
|
pcs, err := promrelabel.ParseRelabelConfigsData([]byte(`
|
|
- target_label: "foo"
|
|
replacement: "aaa"
|
|
- action: labeldrop
|
|
regex: "env.*"
|
|
`), false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
fn(
|
|
map[string]string{"a": "baz"},
|
|
[]prompbmarshal.Label{{Name: "a", Value: "baz"}, {Name: "foo", Value: "aaa"}},
|
|
pcs,
|
|
)
|
|
fn(
|
|
map[string]string{"foo": "bar", "a": "baz"},
|
|
[]prompbmarshal.Label{{Name: "a", Value: "baz"}, {Name: "foo", Value: "aaa"}},
|
|
pcs,
|
|
)
|
|
fn(
|
|
map[string]string{"qux": "bar", "env": "prod", "environment": "production"},
|
|
[]prompbmarshal.Label{{Name: "foo", Value: "aaa"}, {Name: "qux", Value: "bar"}},
|
|
pcs,
|
|
)
|
|
}
|