mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 20:37:12 +01:00
ae4d376e41
…pair `alert_relabel_configs` in [notifier config](https://docs.victoriametrics.com/vmalert/#notifier-configuration-file) can drop alert labels when used to filter different tenant alert message to different notifier. alertmanager would report error like `msg="Failed to validate alerts" err="at least one label pair required"` in this case, but the rest of the alerts inside one request would still be valid in alertmanager, so it's not severe.
235 lines
6.6 KiB
Go
235 lines
6.6 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 TestAlertExecTemplate(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)
|
|
|
|
f := func(alert *Alert, annotations map[string]string, tplExpected map[string]string) {
|
|
t.Helper()
|
|
|
|
if err := ValidateTemplates(annotations); err != nil {
|
|
t.Fatalf("cannot validate annotations: %s", err)
|
|
}
|
|
|
|
qFn := func(_ 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
|
|
}
|
|
|
|
tpl, err := alert.ExecTemplate(qFn, alert.Labels, annotations)
|
|
if err != nil {
|
|
t.Fatalf("cannot execute template: %s", err)
|
|
}
|
|
if len(tpl) != len(tplExpected) {
|
|
t.Fatalf("unexpected number of elements; got %d; want %d", len(tpl), len(tplExpected))
|
|
}
|
|
for k := range tplExpected {
|
|
got, exp := tpl[k], tplExpected[k]
|
|
if got != exp {
|
|
t.Fatalf("unexpected template for key=%q; got %q; want %q", k, got, exp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// empty-alert
|
|
f(&Alert{}, map[string]string{}, map[string]string{})
|
|
|
|
// no-template
|
|
f(&Alert{
|
|
Value: 1e4,
|
|
Labels: map[string]string{
|
|
"instance": "localhost",
|
|
},
|
|
}, map[string]string{}, map[string]string{})
|
|
|
|
// label-template
|
|
f(&Alert{
|
|
Value: 1e4,
|
|
Labels: map[string]string{
|
|
"job": "staging",
|
|
"instance": "localhost",
|
|
},
|
|
For: 5 * time.Minute,
|
|
}, map[string]string{
|
|
"summary": "Too high connection number for {{$labels.instance}} for job {{$labels.job}}",
|
|
"description": "It is {{ $value }} connections for {{$labels.instance}} for more than {{ .For }}",
|
|
}, map[string]string{
|
|
"summary": "Too high connection number for localhost for job staging",
|
|
"description": "It is 10000 connections for localhost for more than 5m0s",
|
|
})
|
|
|
|
// expression-template
|
|
f(&Alert{
|
|
Expr: `vm_rows{"label"="bar"}<0`,
|
|
}, map[string]string{
|
|
"exprEscapedQuery": "{{ $expr|queryEscape }}",
|
|
"exprEscapedPath": "{{ $expr|pathEscape }}",
|
|
"exprEscapedJSON": "{{ $expr|jsonEscape }}",
|
|
"exprEscapedQuotes": "{{ $expr|quotesEscape }}",
|
|
"exprEscapedHTML": "{{ $expr|htmlEscape }}",
|
|
}, 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",
|
|
})
|
|
|
|
// query
|
|
f(&Alert{
|
|
Expr: `vm_rows{"label"="bar"}>0`,
|
|
}, map[string]string{
|
|
"summary": `{{ query "foo" | first | value }}`,
|
|
"desc": `{{ range query "bar" }}{{ . | label "foo" }} {{ . | value }};{{ end }}`,
|
|
}, map[string]string{
|
|
"summary": "1",
|
|
"desc": "bar 1;garply 2;",
|
|
})
|
|
|
|
// external
|
|
f(&Alert{
|
|
Value: 1e4,
|
|
Labels: map[string]string{
|
|
"job": "staging",
|
|
"instance": "localhost",
|
|
},
|
|
}, 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}})",
|
|
}, 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),
|
|
})
|
|
|
|
// alert and group IDs
|
|
f(&Alert{
|
|
ID: 42,
|
|
GroupID: 24,
|
|
}, map[string]string{
|
|
"url": "/api/v1/alert?alertID={{$alertID}}&groupID={{$groupID}}",
|
|
}, map[string]string{
|
|
"url": "/api/v1/alert?alertID=42&groupID=24",
|
|
})
|
|
|
|
// ActiveAt time
|
|
f(&Alert{
|
|
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
|
|
}, map[string]string{
|
|
"diagram": "![](http://example.com?render={{$activeAt.Unix}}",
|
|
}, map[string]string{
|
|
"diagram": "![](http://example.com?render=1660941298",
|
|
})
|
|
|
|
// ActiveAt time is nil
|
|
f(&Alert{}, map[string]string{
|
|
"default_time": "{{$activeAt}}",
|
|
}, map[string]string{
|
|
"default_time": "0001-01-01 00:00:00 +0000 UTC",
|
|
})
|
|
|
|
// ActiveAt custom format
|
|
f(&Alert{
|
|
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
|
|
}, map[string]string{
|
|
"fire_time": `{{$activeAt.Format "2006/01/02 15:04:05"}}`,
|
|
}, map[string]string{
|
|
"fire_time": "2022/08/19 20:34:58",
|
|
})
|
|
|
|
// ActiveAt query range
|
|
f(&Alert{
|
|
ActiveAt: time.Date(2022, 8, 19, 20, 34, 58, 651387237, time.UTC),
|
|
}, map[string]string{
|
|
"grafana_url": `vm-grafana.com?from={{($activeAt.Add (parseDurationTime "1h")).Unix}}&to={{($activeAt.Add (parseDurationTime "-1h")).Unix}}`,
|
|
}, map[string]string{
|
|
"grafana_url": "vm-grafana.com?from=1660944898&to=1660937698",
|
|
})
|
|
}
|
|
|
|
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.applyRelabelingIfNeeded(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,
|
|
)
|
|
fn(
|
|
map[string]string{"foo.bar": "baz", "service!name": "qux"},
|
|
[]prompbmarshal.Label{{Name: "foo_bar", Value: "baz"}, {Name: "service_name", Value: "qux"}},
|
|
nil,
|
|
)
|
|
|
|
pcs, err := promrelabel.ParseRelabelConfigsData([]byte(`
|
|
- target_label: "foo"
|
|
replacement: "aaa"
|
|
- action: labeldrop
|
|
regex: "env.*"
|
|
`))
|
|
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,
|
|
)
|
|
}
|