mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
204 lines
4.6 KiB
Go
204 lines
4.6 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"reflect"
|
||
|
"sort"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||
|
)
|
||
|
|
||
|
func TestUpdateWith(t *testing.T) {
|
||
|
testCases := []struct {
|
||
|
name string
|
||
|
currentRules []*Rule
|
||
|
newRules []*Rule
|
||
|
}{
|
||
|
{
|
||
|
"new rule",
|
||
|
[]*Rule{},
|
||
|
[]*Rule{{Name: "bar"}},
|
||
|
},
|
||
|
{
|
||
|
"update rule",
|
||
|
[]*Rule{{
|
||
|
Name: "foo",
|
||
|
Expr: "up > 0",
|
||
|
For: time.Second,
|
||
|
Labels: map[string]string{
|
||
|
"bar": "baz",
|
||
|
},
|
||
|
Annotations: map[string]string{
|
||
|
"summary": "{{ $value|humanize }}",
|
||
|
"description": "{{$labels}}",
|
||
|
},
|
||
|
}},
|
||
|
[]*Rule{{
|
||
|
Name: "bar",
|
||
|
Expr: "up > 10",
|
||
|
For: time.Second,
|
||
|
Labels: map[string]string{
|
||
|
"baz": "bar",
|
||
|
},
|
||
|
Annotations: map[string]string{
|
||
|
"summary": "none",
|
||
|
},
|
||
|
}},
|
||
|
},
|
||
|
{
|
||
|
"empty rule",
|
||
|
[]*Rule{{Name: "foo"}},
|
||
|
[]*Rule{},
|
||
|
},
|
||
|
{
|
||
|
"multiple rules",
|
||
|
[]*Rule{{Name: "foo"}, {Name: "bar"}, {Name: "baz"}},
|
||
|
[]*Rule{{Name: "foo"}, {Name: "baz"}},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, tc := range testCases {
|
||
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
g := &Group{Rules: tc.currentRules}
|
||
|
g.updateWith(Group{Rules: tc.newRules})
|
||
|
|
||
|
if len(g.Rules) != len(tc.newRules) {
|
||
|
t.Fatalf("expected to have %d rules; got: %d",
|
||
|
len(g.Rules), len(tc.newRules))
|
||
|
}
|
||
|
for i, r := range g.Rules {
|
||
|
got, want := r, tc.newRules[i]
|
||
|
if got.Name != want.Name {
|
||
|
t.Fatalf("expected to have rule %q; got %q", want.Name, got.Name)
|
||
|
}
|
||
|
if got.Expr != want.Expr {
|
||
|
t.Fatalf("expected to have expression %q; got %q", want.Expr, got.Expr)
|
||
|
}
|
||
|
if got.For != want.For {
|
||
|
t.Fatalf("expected to have for %q; got %q", want.For, got.For)
|
||
|
}
|
||
|
if !reflect.DeepEqual(got.Annotations, want.Annotations) {
|
||
|
t.Fatalf("expected to have annotations %#v; got %#v", want.Annotations, got.Annotations)
|
||
|
}
|
||
|
if !reflect.DeepEqual(got.Labels, want.Labels) {
|
||
|
t.Fatalf("expected to have labels %#v; got %#v", want.Labels, got.Labels)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGroupStart(t *testing.T) {
|
||
|
// TODO: make parsing from string instead of file
|
||
|
groups, err := Parse([]string{"testdata/rules1-good.rules"}, true)
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to parse rules: %s", err)
|
||
|
}
|
||
|
g := groups[0]
|
||
|
|
||
|
fn := &fakeNotifier{}
|
||
|
fs := &fakeQuerier{}
|
||
|
|
||
|
const inst1, inst2, job = "foo", "bar", "baz"
|
||
|
m1 := metricWithLabels(t, "instance", inst1, "job", job)
|
||
|
m2 := metricWithLabels(t, "instance", inst2, "job", job)
|
||
|
|
||
|
r := g.Rules[0]
|
||
|
alert1, err := r.newAlert(m1)
|
||
|
if err != nil {
|
||
|
t.Fatalf("faield to create alert: %s", err)
|
||
|
}
|
||
|
alert1.State = notifier.StateFiring
|
||
|
alert1.ID = hash(m1)
|
||
|
|
||
|
alert2, err := r.newAlert(m2)
|
||
|
if err != nil {
|
||
|
t.Fatalf("faield to create alert: %s", err)
|
||
|
}
|
||
|
alert2.State = notifier.StateFiring
|
||
|
alert2.ID = hash(m2)
|
||
|
|
||
|
const evalInterval = time.Millisecond
|
||
|
finished := make(chan struct{})
|
||
|
fs.add(m1)
|
||
|
fs.add(m2)
|
||
|
go func() {
|
||
|
g.start(context.Background(), evalInterval, fs, fn, nil)
|
||
|
close(finished)
|
||
|
}()
|
||
|
|
||
|
// wait for multiple evals
|
||
|
time.Sleep(20 * evalInterval)
|
||
|
|
||
|
gotAlerts := fn.getAlerts()
|
||
|
expectedAlerts := []notifier.Alert{*alert1, *alert2}
|
||
|
compareAlerts(t, expectedAlerts, gotAlerts)
|
||
|
|
||
|
// reset previous data
|
||
|
fs.reset()
|
||
|
// and set only one datapoint for response
|
||
|
fs.add(m1)
|
||
|
|
||
|
// wait for multiple evals
|
||
|
time.Sleep(20 * evalInterval)
|
||
|
|
||
|
gotAlerts = fn.getAlerts()
|
||
|
expectedAlerts = []notifier.Alert{*alert1}
|
||
|
compareAlerts(t, expectedAlerts, gotAlerts)
|
||
|
|
||
|
g.close()
|
||
|
<-finished
|
||
|
}
|
||
|
|
||
|
func compareAlerts(t *testing.T, as, bs []notifier.Alert) {
|
||
|
t.Helper()
|
||
|
if len(as) != len(bs) {
|
||
|
t.Fatalf("expected to have length %d; got %d", len(as), len(bs))
|
||
|
}
|
||
|
sort.Slice(as, func(i, j int) bool {
|
||
|
return as[i].ID < as[j].ID
|
||
|
})
|
||
|
sort.Slice(bs, func(i, j int) bool {
|
||
|
return bs[i].ID < bs[j].ID
|
||
|
})
|
||
|
for i := range as {
|
||
|
a, b := as[i], bs[i]
|
||
|
if a.Name != b.Name {
|
||
|
t.Fatalf("expected t have Name %q; got %q", a.Name, b.Name)
|
||
|
}
|
||
|
if a.State != b.State {
|
||
|
t.Fatalf("expected t have State %q; got %q", a.State, b.State)
|
||
|
}
|
||
|
if a.Value != b.Value {
|
||
|
t.Fatalf("expected t have Value %f; got %f", a.Value, b.Value)
|
||
|
}
|
||
|
if !reflect.DeepEqual(a.Annotations, b.Annotations) {
|
||
|
t.Fatalf("expected to have annotations %#v; got %#v", a.Annotations, b.Annotations)
|
||
|
}
|
||
|
if !reflect.DeepEqual(a.Labels, b.Labels) {
|
||
|
t.Fatalf("expected to have labels %#v; got %#v", a.Labels, b.Labels)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type fakeNotifier struct {
|
||
|
sync.Mutex
|
||
|
alerts []notifier.Alert
|
||
|
}
|
||
|
|
||
|
func (fn *fakeNotifier) Send(_ context.Context, alerts []notifier.Alert) error {
|
||
|
fn.Lock()
|
||
|
defer fn.Unlock()
|
||
|
fn.alerts = alerts
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (fn *fakeNotifier) getAlerts() []notifier.Alert {
|
||
|
fn.Lock()
|
||
|
defer fn.Unlock()
|
||
|
return fn.alerts
|
||
|
}
|