The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/services/ngalert/notifier/autogen_alertmanager_test.go

304 lines
14 KiB

package notifier
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
func TestAddAutogenConfig(t *testing.T) {
rootRoute := func() *definitions.Route {
return &definitions.Route{
Receiver: "default",
}
}
configGen := func(receivers []string, muteIntervals []string) *definitions.PostableApiAlertingConfig {
cfg := &definitions.PostableApiAlertingConfig{
Config: definitions.Config{
Route: rootRoute(),
},
}
for _, receiver := range receivers {
cfg.Receivers = append(cfg.Receivers, &definitions.PostableApiReceiver{
Receiver: config.Receiver{
Name: receiver,
},
})
}
for _, muteInterval := range muteIntervals {
cfg.MuteTimeIntervals = append(cfg.MuteTimeIntervals, config.MuteTimeInterval{
Name: muteInterval,
})
}
return cfg
}
withChildRoutes := func(route *definitions.Route, children ...*definitions.Route) *definitions.Route {
route.Routes = append(route.Routes, children...)
return route
}
matcher := func(key, val string) definitions.ObjectMatchers {
m, err := labels.NewMatcher(labels.MatchEqual, key, val)
require.NoError(t, err)
return definitions.ObjectMatchers{m}
}
basicContactRoute := func(receiver string) *definitions.Route {
return &definitions.Route{
Receiver: receiver,
ObjectMatchers: matcher(models.AutogeneratedRouteReceiverNameLabel, receiver),
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel},
}
}
testCases := []struct {
name string
existingConfig *definitions.PostableApiAlertingConfig
storeSettings []models.NotificationSettings
skipInvalid bool
expRoute *definitions.Route
expErrorContains string
}{
{
name: "no settings or receivers, no change",
existingConfig: configGen(nil, nil),
storeSettings: []models.NotificationSettings{},
expRoute: rootRoute(),
},
{
name: "no settings but some receivers, add default routes for receivers",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil),
storeSettings: []models.NotificationSettings{},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
basicContactRoute("receiver1"),
basicContactRoute("receiver3"),
basicContactRoute("receiver2"),
},
}),
},
{
name: "settings with no custom options, add default routes only",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil),
storeSettings: []models.NotificationSettings{models.NewDefaultNotificationSettings("receiver1"), models.NewDefaultNotificationSettings("receiver2")},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
basicContactRoute("receiver1"),
basicContactRoute("receiver3"),
basicContactRoute("receiver2"),
},
}),
},
{
name: "settings with custom options, add option-specific routes",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3", "receiver4", "receiver5"}, []string{"maintenance"}),
storeSettings: []models.NotificationSettings{
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute))),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver2"), models.NSMuts.WithGroupWait(util.Pointer(2*time.Minute))),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver3"), models.NSMuts.WithRepeatInterval(util.Pointer(3*time.Minute))),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver4"), models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel, "custom")),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver5"), models.NSMuts.WithMuteTimeIntervals("maintenance")),
{
Receiver: "receiver1",
GroupBy: []string{model.AlertNameLabel, models.FolderTitleLabel, "custom"},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance"},
},
},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
withChildRoutes(basicContactRoute("receiver5"), &definitions.Route{
Receiver: "receiver5",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "030d6474aec0b553"),
MuteTimeIntervals: []string{"maintenance"},
}),
withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "4f095749ddf3eeeb"),
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance"},
}, &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"),
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
}),
withChildRoutes(basicContactRoute("receiver2"), &definitions.Route{
Receiver: "receiver2",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "27e1d1717c9ef621"),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
}),
withChildRoutes(basicContactRoute("receiver4"), &definitions.Route{
Receiver: "receiver4",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "b3a2fa5e615dcc7e"),
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"},
}),
withChildRoutes(basicContactRoute("receiver3"), &definitions.Route{
Receiver: "receiver3",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "9e282ef0193d830a"),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
}),
},
}),
},
{
name: "settings with custom options and nil groupBy, groupBy should inherit from parent",
existingConfig: configGen([]string{"receiver1"}, nil),
storeSettings: []models.NotificationSettings{
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy()),
},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"),
GroupByStr: nil,
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
}),
},
}),
},
{
name: "settings with nil groupBy should have different fingerprint than default groupBy",
existingConfig: configGen([]string{"receiver1"}, nil),
storeSettings: []models.NotificationSettings{
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy()),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy(models.DefaultNotificationSettingsGroupBy...)),
},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"),
GroupByStr: nil,
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
}, &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "e1f3a275a8918385"), // Different hash.
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
}),
},
}),
},
{
name: "settings with incomplete required groupBy labels will be completed and should have the same fingerprint",
existingConfig: configGen([]string{"receiver1"}, nil),
storeSettings: []models.NotificationSettings{
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy(models.FolderTitleLabel)),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy(model.AlertNameLabel)),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy(models.DefaultNotificationSettingsGroupBy...)),
},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "e1f3a275a8918385"),
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
}),
},
}),
},
{
name: "when skipInvalid=true, invalid settings are skipped",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil),
storeSettings: []models.NotificationSettings{
models.NewDefaultNotificationSettings("receiverA"), // Doesn't exist.
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithMuteTimeIntervals("maintenance")), // Doesn't exist.
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver2"), models.NSMuts.WithGroupWait(util.Pointer(-2*time.Minute))), // Negative.
},
skipInvalid: true,
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
Receiver: "default",
ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"),
Routes: []*definitions.Route{
basicContactRoute("receiver1"),
basicContactRoute("receiver3"),
basicContactRoute("receiver2"),
},
}),
},
{
name: "when skipInvalid=false, invalid receiver throws error",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil),
storeSettings: []models.NotificationSettings{models.NewDefaultNotificationSettings("receiverA")},
skipInvalid: false,
expErrorContains: "receiverA",
},
{
name: "when skipInvalid=false, invalid settings throws error",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil),
storeSettings: []models.NotificationSettings{models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithMuteTimeIntervals("maintenance"))},
skipInvalid: false,
expErrorContains: "maintenance",
},
{
name: "when skipInvalid=false, invalid settings throws error",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil),
storeSettings: []models.NotificationSettings{models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver2"), models.NSMuts.WithGroupWait(util.Pointer(-2*time.Minute)))},
skipInvalid: false,
expErrorContains: "group wait",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
orgId := int64(1)
store := &fakeConfigStore{
notificationSettings: make(map[int64]map[models.AlertRuleKey][]models.NotificationSettings),
}
store.notificationSettings[orgId] = make(map[models.AlertRuleKey][]models.NotificationSettings)
for _, setting := range tt.storeSettings {
store.notificationSettings[orgId][models.AlertRuleKey{OrgID: orgId, UID: util.GenerateShortUID()}] = []models.NotificationSettings{setting}
}
err := AddAutogenConfig(context.Background(), &logtest.Fake{}, store, orgId, tt.existingConfig, tt.skipInvalid)
if tt.expErrorContains != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.expErrorContains)
return
} else {
require.NoError(t, err)
}
cOpt := []cmp.Option{
cmpopts.IgnoreUnexported(definitions.Route{}, labels.Matcher{}),
}
if !cmp.Equal(tt.expRoute, tt.existingConfig.Route, cOpt...) {
t.Errorf("Unexpected Route: %v", cmp.Diff(tt.expRoute, tt.existingConfig.Route, cOpt...))
}
})
}
}