Alerting: Improve legacy migration to include send reminder & frequency (#60275)

* Alerting: Improve legacy migration to include send reminder & frequency

Legacy channel frequency is migrated to the channel's migrated route's
repeat interval if send reminder is true. If send reminder is false, we
pseudo-disable the repeat interval by setting it to a large value (1y).

If there were no default channels, the root notification policy is still
created with the default 4h repeat interval.
This commit is contained in:
Matthew Jacobson 2023-01-10 23:01:43 -05:00 committed by GitHub
parent be1c5e13d5
commit 63ba3ccb58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 294 additions and 116 deletions

View File

@ -8,6 +8,7 @@ import (
"regexp"
"sort"
"strings"
"time"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
@ -16,6 +17,11 @@ import (
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
// DisabledRepeatInterval is a large duration that will be used as a pseudo-disable in case a legacy channel doesn't have SendReminders enabled.
DisabledRepeatInterval = model.Duration(time.Duration(8736) * time.Hour) // 1y
)
type notificationChannel struct {
ID int64 `xorm:"id"`
OrgID int64 `xorm:"org_id"`
@ -26,6 +32,8 @@ type notificationChannel struct {
IsDefault bool `xorm:"is_default"`
Settings *simplejson.Json `xorm:"settings"`
SecureSettings SecureJsonData `xorm:"secure_settings"`
SendReminder bool `xorm:"send_reminder"`
Frequency model.Duration `xorm:"frequency"`
}
// channelsPerOrg maps notification channels per organisation
@ -37,6 +45,12 @@ type defaultChannelsPerOrg map[int64][]*notificationChannel
// uidOrID for both uid and ID, primarily used for mapping legacy channel to migrated receiver.
type uidOrID interface{}
// channelReceiver is a convenience struct that contains a notificationChannel and its corresponding migrated PostableApiReceiver.
type channelReceiver struct {
channel *notificationChannel
receiver *PostableApiReceiver
}
// setupAlertmanagerConfigs creates Alertmanager configs with migrated receivers and routes.
func (m *migration) setupAlertmanagerConfigs(rulesPerOrg map[int64]map[*alertRule][]uidOrID) (amConfigsPerOrg, error) {
// allChannels: channelUID -> channelConfig
@ -66,7 +80,9 @@ func (m *migration) setupAlertmanagerConfigs(rulesPerOrg map[int64]map[*alertRul
continue
}
amConfig.AlertmanagerConfig.Receivers = receivers
for _, cr := range receivers {
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, cr.receiver)
}
defaultReceivers := make(map[string]struct{})
defaultChannels, ok := defaultChannelsPerOrg[orgID]
@ -85,10 +101,10 @@ func (m *migration) setupAlertmanagerConfigs(rulesPerOrg map[int64]map[*alertRul
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, defaultReceiver)
}
for _, recv := range receivers {
route, err := createRoute(recv)
for _, cr := range receivers {
route, err := createRoute(cr)
if err != nil {
return nil, fmt.Errorf("failed to create route for receiver %s in orgId %d: %w", recv.Name, orgID, err)
return nil, fmt.Errorf("failed to create route for receiver %s in orgId %d: %w", cr.receiver.Name, orgID, err)
}
amConfigPerOrg[orgID].AlertmanagerConfig.Route.Routes = append(amConfigPerOrg[orgID].AlertmanagerConfig.Route.Routes, route)
@ -141,7 +157,9 @@ func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannels
disable_resolve_message,
is_default,
settings,
secure_settings
secure_settings,
send_reminder,
frequency
FROM
alert_notification
`
@ -196,8 +214,8 @@ func (m *migration) createNotifier(c *notificationChannel) (*PostableGrafanaRece
}
// Create one receiver for every unique notification channel.
func (m *migration) createReceivers(allChannels []*notificationChannel) (map[uidOrID]*PostableApiReceiver, []*PostableApiReceiver, error) {
receivers := make([]*PostableApiReceiver, 0, len(allChannels))
func (m *migration) createReceivers(allChannels []*notificationChannel) (map[uidOrID]*PostableApiReceiver, []channelReceiver, error) {
receivers := make([]channelReceiver, 0, len(allChannels))
receiversMap := make(map[uidOrID]*PostableApiReceiver)
set := make(map[string]struct{}) // Used to deduplicate sanitized names.
@ -218,20 +236,23 @@ func (m *migration) createReceivers(allChannels []*notificationChannel) (map[uid
set[sanitizedName] = struct{}{}
recv := &PostableApiReceiver{
Name: sanitizedName, // Channel name is unique within an Org.
GrafanaManagedReceivers: []*PostableGrafanaReceiver{notifier},
cr := channelReceiver{
channel: c,
receiver: &PostableApiReceiver{
Name: sanitizedName, // Channel name is unique within an Org.
GrafanaManagedReceivers: []*PostableGrafanaReceiver{notifier},
},
}
receivers = append(receivers, recv)
receivers = append(receivers, cr)
// Store receivers for creating routes from alert rules later.
if c.Uid != "" {
receiversMap[c.Uid] = recv
receiversMap[c.Uid] = cr.receiver
}
if c.ID != 0 {
// In certain circumstances, the alert rule uses ID instead of uid. So, we add this to be able to lookup by ID in case.
receiversMap[c.ID] = recv
receiversMap[c.ID] = cr.receiver
}
}
@ -240,17 +261,27 @@ func (m *migration) createReceivers(allChannels []*notificationChannel) (map[uid
// Create the root-level route with the default receiver. If no new receiver is created specifically for the root-level route, the returned receiver will be nil.
func (m *migration) createDefaultRouteAndReceiver(defaultChannels []*notificationChannel) (*PostableApiReceiver, *Route, error) {
var defaultReceiver *PostableApiReceiver
defaultReceiverName := "autogen-contact-point-default"
if len(defaultChannels) != 1 {
// If there are zero or more than one default channels we create a separate contact group that is used only in the root policy. This is to simplify the migrated notification policy structure.
// If we ever allow more than one receiver per route this won't be necessary.
defaultReceiver = &PostableApiReceiver{
Name: defaultReceiverName,
GrafanaManagedReceivers: []*PostableGrafanaReceiver{},
}
defaultRoute := &Route{
Receiver: defaultReceiverName,
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, // To keep parity with pre-migration notifications.
RepeatInterval: nil,
}
newDefaultReceiver := &PostableApiReceiver{
Name: defaultReceiverName,
GrafanaManagedReceivers: []*PostableGrafanaReceiver{},
}
// Return early if there are no default channels
if len(defaultChannels) == 0 {
return newDefaultReceiver, defaultRoute, nil
}
repeatInterval := DisabledRepeatInterval // If no channels have SendReminders enabled, we will use this large value as a pseudo-disable.
if len(defaultChannels) > 1 {
// If there are more than one default channels we create a separate contact group that is used only in the root policy. This is to simplify the migrated notification policy structure.
// If we ever allow more than one receiver per route this won't be necessary.
for _, c := range defaultChannels {
// Need to create a new notifier to prevent uid conflict.
defaultNotifier, err := m.createNotifier(c)
@ -258,39 +289,51 @@ func (m *migration) createDefaultRouteAndReceiver(defaultChannels []*notificatio
return nil, nil, err
}
defaultReceiver.GrafanaManagedReceivers = append(defaultReceiver.GrafanaManagedReceivers, defaultNotifier)
newDefaultReceiver.GrafanaManagedReceivers = append(newDefaultReceiver.GrafanaManagedReceivers, defaultNotifier)
// Choose the lowest send reminder duration from all the notifiers to use for default route.
if c.SendReminder && c.Frequency < repeatInterval {
repeatInterval = c.Frequency
}
}
} else {
// If there is only a single default channel, we don't need a separate receiver to hold it. We can reuse the existing receiver for that single notifier.
defaultReceiverName = defaultChannels[0].Name
}
defaultRoute.Receiver = defaultChannels[0].Name
if defaultChannels[0].SendReminder {
repeatInterval = defaultChannels[0].Frequency
}
defaultRoute := &Route{
Receiver: defaultReceiverName,
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, // To keep parity with pre-migration notifications.
// No need to create a new receiver.
newDefaultReceiver = nil
}
defaultRoute.RepeatInterval = &repeatInterval
return defaultReceiver, defaultRoute, nil
return newDefaultReceiver, defaultRoute, nil
}
// Create one route per contact point, matching based on ContactLabel.
func createRoute(recv *PostableApiReceiver) (*Route, error) {
func createRoute(cr channelReceiver) (*Route, error) {
// We create a regex matcher so that each alert rule need only have a single ContactLabel entry for all contact points it sends to.
// For example, if an alert needs to send to contact1 and contact2 it will have ContactLabel=`"contact1","contact2"` and will match both routes looking
// for `.*"contact1".*` and `.*"contact2".*`.
// We quote and escape here to ensure the regex will correctly match the ContactLabel on the alerts.
name := fmt.Sprintf(`.*%s.*`, regexp.QuoteMeta(quote(recv.Name)))
name := fmt.Sprintf(`.*%s.*`, regexp.QuoteMeta(quote(cr.receiver.Name)))
mat, err := labels.NewMatcher(labels.MatchRegexp, ContactLabel, name)
if err != nil {
return nil, err
}
repeatInterval := DisabledRepeatInterval
if cr.channel.SendReminder {
repeatInterval = cr.channel.Frequency
}
return &Route{
Receiver: recv.Name,
Receiver: cr.receiver.Name,
ObjectMatchers: ObjectMatchers{mat},
Continue: true, // We continue so that each sibling contact point route can separately match.
RepeatInterval: &repeatInterval,
}, nil
}
@ -430,11 +473,12 @@ type PostableApiAlertingConfig struct {
}
type Route struct {
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"`
ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"`
Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"`
Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"`
GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"`
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"`
ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"`
Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"`
Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"`
GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"`
RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"`
}
type ObjectMatchers labels.Matchers

View File

@ -2,6 +2,7 @@ package ualert
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
@ -121,11 +122,13 @@ func TestFilterReceiversForAlert(t *testing.T) {
func TestCreateRoute(t *testing.T) {
tc := []struct {
name string
channel *notificationChannel
recv *PostableApiReceiver
expected *Route
}{
{
name: "when a receiver is passed in, the route should regex match based on quoted name with continue=true",
name: "when a receiver is passed in, the route should regex match based on quoted name with continue=true",
channel: &notificationChannel{},
recv: &PostableApiReceiver{
Name: "recv1",
},
@ -135,10 +138,12 @@ func TestCreateRoute(t *testing.T) {
Routes: nil,
Continue: true,
GroupByStr: nil,
RepeatInterval: durationPointer(DisabledRepeatInterval),
},
},
{
name: "notification channel should be escaped for regex in the matcher",
name: "notification channel should be escaped for regex in the matcher",
channel: &notificationChannel{},
recv: &PostableApiReceiver{
Name: `. ^ $ * + - ? ( ) [ ] { } \ |`,
},
@ -148,13 +153,47 @@ func TestCreateRoute(t *testing.T) {
Routes: nil,
Continue: true,
GroupByStr: nil,
RepeatInterval: durationPointer(DisabledRepeatInterval),
},
},
{
name: "when a channel has sendReminder=true, the route should use the frequency in repeat interval",
channel: &notificationChannel{SendReminder: true, Frequency: model.Duration(time.Duration(42) * time.Hour)},
recv: &PostableApiReceiver{
Name: "recv1",
},
expected: &Route{
Receiver: "recv1",
ObjectMatchers: ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"recv1".*`}},
Routes: nil,
Continue: true,
GroupByStr: nil,
RepeatInterval: durationPointer(model.Duration(time.Duration(42) * time.Hour)),
},
},
{
name: "when a channel has sendReminder=false, the route should ignore the frequency in repeat interval and use DisabledRepeatInterval",
channel: &notificationChannel{SendReminder: false, Frequency: model.Duration(time.Duration(42) * time.Hour)},
recv: &PostableApiReceiver{
Name: "recv1",
},
expected: &Route{
Receiver: "recv1",
ObjectMatchers: ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"recv1".*`}},
Routes: nil,
Continue: true,
GroupByStr: nil,
RepeatInterval: durationPointer(DisabledRepeatInterval),
},
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
res, err := createRoute(tt.recv)
res, err := createRoute(channelReceiver{
channel: tt.channel,
receiver: tt.recv,
})
require.NoError(t, err)
// Order of nested routes is not guaranteed.
@ -180,13 +219,18 @@ func createNotChannel(t *testing.T, uid string, id int64, name string) *notifica
return &notificationChannel{Uid: uid, ID: id, Name: name, Settings: simplejson.New()}
}
func createNotChannelWithReminder(t *testing.T, uid string, id int64, name string, frequency model.Duration) *notificationChannel {
t.Helper()
return &notificationChannel{Uid: uid, ID: id, Name: name, SendReminder: true, Frequency: frequency, Settings: simplejson.New()}
}
func TestCreateReceivers(t *testing.T) {
tc := []struct {
name string
allChannels []*notificationChannel
defaultChannels []*notificationChannel
expRecvMap map[uidOrID]*PostableApiReceiver
expRecv []*PostableApiReceiver
expRecv []channelReceiver
expErr error
}{
{
@ -210,14 +254,20 @@ func TestCreateReceivers(t *testing.T) {
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}},
},
},
expRecv: []*PostableApiReceiver{
expRecv: []channelReceiver{
{
Name: "name1",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}},
channel: createNotChannel(t, "uid1", int64(1), "name1"),
receiver: &PostableApiReceiver{
Name: "name1",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}},
},
},
{
Name: "name2",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}},
channel: createNotChannel(t, "uid2", int64(2), "name2"),
receiver: &PostableApiReceiver{
Name: "name2",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}},
},
},
},
},
@ -234,10 +284,13 @@ func TestCreateReceivers(t *testing.T) {
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
},
},
expRecv: []*PostableApiReceiver{
expRecv: []channelReceiver{
{
Name: "name_1",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
channel: createNotChannel(t, "uid1", int64(1), "name\"1"),
receiver: &PostableApiReceiver{
Name: "name_1",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
},
},
},
},
@ -262,14 +315,20 @@ func TestCreateReceivers(t *testing.T) {
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1_dba13d"}},
},
},
expRecv: []*PostableApiReceiver{
expRecv: []channelReceiver{
{
Name: "name_1",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
channel: createNotChannel(t, "uid1", int64(1), "name\"1"),
receiver: &PostableApiReceiver{
Name: "name_1",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
},
},
{
Name: "name_1_dba13d",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1_dba13d"}},
channel: createNotChannel(t, "uid2", int64(2), "name_1"),
receiver: &PostableApiReceiver{
Name: "name_1_dba13d",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1_dba13d"}},
},
},
},
},
@ -289,7 +348,7 @@ func TestCreateReceivers(t *testing.T) {
// We ignore certain fields for the purposes of this test
for _, recv := range recvs {
for _, not := range recv.GrafanaManagedReceivers {
for _, not := range recv.receiver.GrafanaManagedReceivers {
not.UID = ""
not.Settings = nil
not.SecureSettings = nil
@ -319,9 +378,27 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) {
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}, {Name: "name2"}},
},
expRoute: &Route{
Receiver: "autogen-contact-point-default",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Receiver: "autogen-contact-point-default",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
RepeatInterval: durationPointer(DisabledRepeatInterval),
},
},
{
name: "when given multiple default notification channels migrate them to a single receiver with RepeatInterval set to be the minimum of all channel frequencies",
defaultChannels: []*notificationChannel{
createNotChannelWithReminder(t, "uid1", int64(1), "name1", model.Duration(42)),
createNotChannelWithReminder(t, "uid2", int64(2), "name2", model.Duration(100000)),
},
expRecv: &PostableApiReceiver{
Name: "autogen-contact-point-default",
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}, {Name: "name2"}},
},
expRoute: &Route{
Receiver: "autogen-contact-point-default",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
RepeatInterval: durationPointer(model.Duration(42)),
},
},
{
@ -332,9 +409,10 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) {
GrafanaManagedReceivers: []*PostableGrafanaReceiver{},
},
expRoute: &Route{
Receiver: "autogen-contact-point-default",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Receiver: "autogen-contact-point-default",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
RepeatInterval: nil,
},
},
{
@ -342,9 +420,21 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) {
defaultChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1")},
expRecv: nil,
expRoute: &Route{
Receiver: "name1",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Receiver: "name1",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
RepeatInterval: durationPointer(DisabledRepeatInterval),
},
},
{
name: "when given a single default notification channel with SendReminder=true, use the channels Frequency as the RepeatInterval",
defaultChannels: []*notificationChannel{createNotChannelWithReminder(t, "uid1", int64(1), "name1", model.Duration(42))},
expRecv: nil,
expRoute: &Route{
Receiver: "name1",
Routes: make([]*Route, 0),
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
RepeatInterval: durationPointer(model.Duration(42)),
},
},
}
@ -375,3 +465,7 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) {
})
}
}
func durationPointer(d model.Duration) *model.Duration {
return &d
}

View File

@ -158,10 +158,11 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier3", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier3".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier3", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier3".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
RepeatInterval: nil,
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
@ -177,15 +178,16 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "notifier6",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier4", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier4".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier5", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier5".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier6", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier6".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier4", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier4".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier5", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier5".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier6", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier6".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
RepeatInterval: durationPointer(ualert.DisabledRepeatInterval),
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier4", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}}},
{Name: "notifier5", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier5", Type: "slack"}}},
{Name: "notifier6", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}, // empty default
{Name: "notifier6", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}},
},
},
},
@ -204,8 +206,9 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
RepeatInterval: nil,
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
@ -228,8 +231,33 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "notifier1",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
RepeatInterval: durationPointer(ualert.DisabledRepeatInterval),
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
},
},
},
},
},
{
name: "when single default channel with SendReminder, use channel Frequency as RepeatInterval",
legacyChannels: []*models.AlertNotification{
createAlertNotificationWithReminder(t, int64(1), "notifier1", "email", emailSettings, true, true, time.Duration(1)*time.Hour),
},
alerts: []*models.Alert{},
expected: map[int64]*ualert.PostableUserConfig{
int64(1): {
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
Route: &ualert.Route{
Receiver: "notifier1",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(model.Duration(time.Duration(1) * time.Hour))},
},
RepeatInterval: durationPointer(model.Duration(time.Duration(1) * time.Hour)),
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
@ -252,9 +280,38 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
RepeatInterval: durationPointer(ualert.DisabledRepeatInterval),
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
{Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}},
{Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}},
},
},
},
},
},
{
name: "when multiple default channels with SendReminder, use minimum channel frequency as RepeatInterval",
legacyChannels: []*models.AlertNotification{
createAlertNotificationWithReminder(t, int64(1), "notifier1", "email", emailSettings, true, true, time.Duration(1)*time.Hour),
createAlertNotificationWithReminder(t, int64(1), "notifier2", "slack", slackSettings, true, true, time.Duration(30)*time.Minute),
},
alerts: []*models.Alert{},
expected: map[int64]*ualert.PostableUserConfig{
int64(1): {
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
Route: &ualert.Route{
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(model.Duration(time.Duration(1) * time.Hour))},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(model.Duration(time.Duration(30) * time.Minute))},
},
RepeatInterval: durationPointer(model.Duration(time.Duration(30) * time.Minute)),
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
@ -280,10 +337,11 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier3", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier3".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier3", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier3".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
RepeatInterval: durationPointer(ualert.DisabledRepeatInterval),
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
@ -294,36 +352,6 @@ func TestAMConfigMigration(t *testing.T) {
},
},
},
{
name: "when alert has only defaults, don't create route for it",
legacyChannels: []*models.AlertNotification{
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true), // default
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, true), // default
},
alerts: []*models.Alert{
createAlert(t, int64(1), int64(1), int64(1), "alert1", []string{"notifier1"}),
createAlert(t, int64(1), int64(2), int64(3), "alert2", []string{}),
},
expected: map[int64]*ualert.PostableUserConfig{
int64(1): {
AlertmanagerConfig: ualert.PostableApiAlertingConfig{
Route: &ualert.Route{
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true},
},
},
Receivers: []*ualert.PostableApiReceiver{
{Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}},
{Name: "notifier2", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}},
{Name: "autogen-contact-point-default", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}, {Name: "notifier2", Type: "slack"}}},
},
},
},
},
},
{
name: "when alerts share channels, only create one receiver per legacy channel",
legacyChannels: []*models.AlertNotification{
@ -341,8 +369,8 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
{Receiver: "notifier2", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier2".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
},
Receivers: []*ualert.PostableApiReceiver{
@ -367,7 +395,7 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
},
Receivers: []*ualert.PostableApiReceiver{
@ -393,7 +421,7 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
},
Receivers: []*ualert.PostableApiReceiver{
@ -420,7 +448,7 @@ func TestAMConfigMigration(t *testing.T) {
Receiver: "autogen-contact-point-default",
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
Routes: []*ualert.Route{
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true},
{Receiver: "notifier1", ObjectMatchers: ualert.ObjectMatchers{{Type: 2, Name: ualert.ContactLabel, Value: `.*"notifier1".*`}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(ualert.DisabledRepeatInterval)},
},
},
Receivers: []*ualert.PostableApiReceiver{
@ -444,6 +472,7 @@ func TestAMConfigMigration(t *testing.T) {
// Order of nested GrafanaManagedReceivers is not guaranteed.
cOpt := []cmp.Option{
cmpopts.IgnoreUnexported(ualert.PostableApiReceiver{}),
cmpopts.IgnoreFields(ualert.PostableGrafanaReceiver{}, "UID", "Settings", "SecureSettings"),
cmpopts.SortSlices(func(a, b *ualert.PostableGrafanaReceiver) bool { return a.Name < b.Name }),
cmpopts.SortSlices(func(a, b *ualert.PostableApiReceiver) bool { return a.Name < b.Name }),
@ -576,8 +605,8 @@ var (
now = time.Now()
)
// createAlertNotification creates a legacy alert notification channel for inserting into the test database.
func createAlertNotification(t *testing.T, orgId int64, uid string, channelType string, settings string, defaultChannel bool) *models.AlertNotification {
// createAlertNotificationWithReminder creates a legacy alert notification channel for inserting into the test database.
func createAlertNotificationWithReminder(t *testing.T, orgId int64, uid string, channelType string, settings string, defaultChannel bool, sendReminder bool, frequency time.Duration) *models.AlertNotification {
t.Helper()
settingsJson := simplejson.New()
if settings != "" {
@ -599,9 +628,16 @@ func createAlertNotification(t *testing.T, orgId int64, uid string, channelType
SecureSettings: make(map[string][]byte),
Created: now,
Updated: now,
SendReminder: sendReminder,
Frequency: frequency,
}
}
// createAlertNotification creates a legacy alert notification channel for inserting into the test database.
func createAlertNotification(t *testing.T, orgId int64, uid string, channelType string, settings string, defaultChannel bool) *models.AlertNotification {
return createAlertNotificationWithReminder(t, orgId, uid, channelType, settings, defaultChannel, false, time.Duration(0))
}
// createAlert creates a legacy alert rule for inserting into the test database.
func createAlert(t *testing.T, orgId int64, dashboardId int64, panelsId int64, name string, notifierUids []string) *models.Alert {
t.Helper()
@ -766,3 +802,7 @@ func getAlertRules(t *testing.T, x *xorm.Engine, orgId int64) []*ngModels.AlertR
func boolPointer(b bool) *bool {
return &b
}
func durationPointer(d model.Duration) *model.Duration {
return &d
}