mirror of https://github.com/grafana/grafana
Alerting: Create fewer contact points on migration (#47291)
* Alerting: Create fewer contact points on migration Previously a new contact point was created for every unique combination of channels attached to any legacy alert. This was very hard to maintain, requiring modifications in every generated contact point. This change deduplicates the generated contact points to a more reasonable state. There should now only be one contact point per legacy channel, and we attached multiple contact points to a route by nesting them. The sole exception to this is if there were multiple default legacy channels, in which case we create a redundant contact point containing all of them used only in the root policy. This allows for a much simpler notification policy structure. Co-authored-by: gotjosh <josue.abreu@gmail.com>pull/48022/head^2
parent
5f594addbf
commit
0301d956da
@ -0,0 +1,349 @@ |
||||
package ualert |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
) |
||||
|
||||
func TestFilterReceiversForAlert(t *testing.T) { |
||||
tc := []struct { |
||||
name string |
||||
da dashAlert |
||||
receivers map[uidOrID]*PostableApiReceiver |
||||
defaultReceivers map[string]struct{} |
||||
expected map[string]interface{} |
||||
}{ |
||||
{ |
||||
name: "when an alert has multiple channels, each should filter for the correct receiver", |
||||
da: dashAlert{ |
||||
ParsedSettings: &dashAlertSettings{ |
||||
Notifications: []dashAlertNot{{UID: "uid1"}, {UID: "uid2"}}, |
||||
}, |
||||
}, |
||||
receivers: map[uidOrID]*PostableApiReceiver{ |
||||
"uid1": { |
||||
Name: "recv1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
"uid2": { |
||||
Name: "recv2", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
"uid3": { |
||||
Name: "recv3", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
}, |
||||
defaultReceivers: map[string]struct{}{}, |
||||
expected: map[string]interface{}{ |
||||
"recv1": struct{}{}, |
||||
"recv2": struct{}{}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "when default receivers exist, they should be added to an alert's filtered receivers", |
||||
da: dashAlert{ |
||||
ParsedSettings: &dashAlertSettings{ |
||||
Notifications: []dashAlertNot{{UID: "uid1"}}, |
||||
}, |
||||
}, |
||||
receivers: map[uidOrID]*PostableApiReceiver{ |
||||
"uid1": { |
||||
Name: "recv1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
"uid2": { |
||||
Name: "recv2", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
"uid3": { |
||||
Name: "recv3", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
}, |
||||
defaultReceivers: map[string]struct{}{ |
||||
"recv2": {}, |
||||
}, |
||||
expected: map[string]interface{}{ |
||||
"recv1": struct{}{}, // From alert
|
||||
"recv2": struct{}{}, // From default
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "when an alert has a channels associated by ID instead of UID, it should be included", |
||||
da: dashAlert{ |
||||
ParsedSettings: &dashAlertSettings{ |
||||
Notifications: []dashAlertNot{{ID: int64(42)}}, |
||||
}, |
||||
}, |
||||
receivers: map[uidOrID]*PostableApiReceiver{ |
||||
int64(42): { |
||||
Name: "recv1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
}, |
||||
defaultReceivers: map[string]struct{}{}, |
||||
expected: map[string]interface{}{ |
||||
"recv1": struct{}{}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "when an alert's receivers are covered by the defaults, return nil to use default receiver downstream", |
||||
da: dashAlert{ |
||||
ParsedSettings: &dashAlertSettings{ |
||||
Notifications: []dashAlertNot{{UID: "uid1"}}, |
||||
}, |
||||
}, |
||||
receivers: map[uidOrID]*PostableApiReceiver{ |
||||
"uid1": { |
||||
Name: "recv1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
"uid2": { |
||||
Name: "recv2", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
"uid3": { |
||||
Name: "recv3", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
}, |
||||
defaultReceivers: map[string]struct{}{ |
||||
"recv1": {}, |
||||
"recv2": {}, |
||||
}, |
||||
expected: nil, // recv1 is already a default
|
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tc { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
m := newTestMigration(t) |
||||
res := m.filterReceiversForAlert(tt.da, tt.receivers, tt.defaultReceivers) |
||||
|
||||
require.Equal(t, tt.expected, res) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestCreateRoute(t *testing.T) { |
||||
tc := []struct { |
||||
name string |
||||
ruleUID string |
||||
filteredReceiverNames map[string]interface{} |
||||
expected *Route |
||||
expErr error |
||||
}{ |
||||
{ |
||||
name: "when a single receiver is passed in, the route should be simple and not nested", |
||||
ruleUID: "r_uid1", |
||||
filteredReceiverNames: map[string]interface{}{ |
||||
"recv1": struct{}{}, |
||||
}, |
||||
expected: &Route{ |
||||
Receiver: "recv1", |
||||
Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, |
||||
Routes: nil, |
||||
Continue: false, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "when multiple receivers are passed in, the route should be nested with continue=true", |
||||
ruleUID: "r_uid1", |
||||
filteredReceiverNames: map[string]interface{}{ |
||||
"recv1": struct{}{}, |
||||
"recv2": struct{}{}, |
||||
}, |
||||
expected: &Route{ |
||||
Receiver: "", |
||||
Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, |
||||
Routes: []*Route{ |
||||
{ |
||||
Receiver: "recv1", |
||||
Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, |
||||
Routes: nil, |
||||
Continue: true, |
||||
}, |
||||
{ |
||||
Receiver: "recv2", |
||||
Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, |
||||
Routes: nil, |
||||
Continue: true, |
||||
}, |
||||
}, |
||||
Continue: false, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tc { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
res, err := createRoute(tt.ruleUID, tt.filteredReceiverNames) |
||||
if tt.expErr != nil { |
||||
require.Error(t, err) |
||||
require.EqualError(t, err, tt.expErr.Error()) |
||||
return |
||||
} |
||||
|
||||
require.NoError(t, err) |
||||
|
||||
// Compare route slice separately since order is not guaranteed
|
||||
expRoutes := tt.expected.Routes |
||||
tt.expected.Routes = nil |
||||
actRoutes := res.Routes |
||||
res.Routes = nil |
||||
|
||||
require.Equal(t, tt.expected, res) |
||||
require.ElementsMatch(t, expRoutes, actRoutes) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func createNotChannel(t *testing.T, uid string, id int64, name string) *notificationChannel { |
||||
t.Helper() |
||||
return ¬ificationChannel{Uid: uid, ID: id, Name: name, Settings: simplejson.New()} |
||||
} |
||||
|
||||
func TestCreateReceivers(t *testing.T) { |
||||
tc := []struct { |
||||
name string |
||||
allChannels []*notificationChannel |
||||
defaultChannels []*notificationChannel |
||||
expRecvMap map[uidOrID]*PostableApiReceiver |
||||
expRecv []*PostableApiReceiver |
||||
expErr error |
||||
}{ |
||||
{ |
||||
name: "when given notification channels migrate them to receivers", |
||||
allChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1"), createNotChannel(t, "uid2", int64(2), "name2")}, |
||||
expRecvMap: map[uidOrID]*PostableApiReceiver{ |
||||
"uid1": { |
||||
Name: "name1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}}, |
||||
}, |
||||
"uid2": { |
||||
Name: "name2", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}}, |
||||
}, |
||||
int64(1): { |
||||
Name: "name1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}}, |
||||
}, |
||||
int64(2): { |
||||
Name: "name2", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}}, |
||||
}, |
||||
}, |
||||
expRecv: []*PostableApiReceiver{ |
||||
{ |
||||
Name: "name1", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}}, |
||||
}, |
||||
{ |
||||
Name: "name2", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tc { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
m := newTestMigration(t) |
||||
recvMap, recvs, err := m.createReceivers(tt.allChannels) |
||||
if tt.expErr != nil { |
||||
require.Error(t, err) |
||||
require.EqualError(t, err, tt.expErr.Error()) |
||||
return |
||||
} |
||||
|
||||
require.NoError(t, err) |
||||
|
||||
// We ignore certain fields for the purposes of this test
|
||||
for _, recv := range recvs { |
||||
for _, not := range recv.GrafanaManagedReceivers { |
||||
not.UID = "" |
||||
not.Settings = nil |
||||
not.SecureSettings = nil |
||||
} |
||||
} |
||||
|
||||
require.Equal(t, tt.expRecvMap, recvMap) |
||||
require.ElementsMatch(t, tt.expRecv, recvs) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestCreateDefaultRouteAndReceiver(t *testing.T) { |
||||
tc := []struct { |
||||
name string |
||||
amConfig *PostableUserConfig |
||||
defaultChannels []*notificationChannel |
||||
expRecv *PostableApiReceiver |
||||
expRoute *Route |
||||
expErr error |
||||
}{ |
||||
{ |
||||
name: "when given multiple default notification channels migrate them to a single receiver", |
||||
defaultChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1"), createNotChannel(t, "uid2", int64(2), "name2")}, |
||||
expRecv: &PostableApiReceiver{ |
||||
Name: "autogen-contact-point-default", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}, {Name: "name2"}}, |
||||
}, |
||||
expRoute: &Route{ |
||||
Receiver: "autogen-contact-point-default", |
||||
Routes: make([]*Route, 0), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "when given no default notification channels create a single empty receiver for default", |
||||
defaultChannels: []*notificationChannel{}, |
||||
expRecv: &PostableApiReceiver{ |
||||
Name: "autogen-contact-point-default", |
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, |
||||
}, |
||||
expRoute: &Route{ |
||||
Receiver: "autogen-contact-point-default", |
||||
Routes: make([]*Route, 0), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "when given a single default notification channels don't create a new default receiver", |
||||
defaultChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1")}, |
||||
expRecv: nil, |
||||
expRoute: &Route{ |
||||
Receiver: "name1", |
||||
Routes: make([]*Route, 0), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tc { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
m := newTestMigration(t) |
||||
recv, route, err := m.createDefaultRouteAndReceiver(tt.defaultChannels) |
||||
if tt.expErr != nil { |
||||
require.Error(t, err) |
||||
require.EqualError(t, err, tt.expErr.Error()) |
||||
return |
||||
} |
||||
|
||||
require.NoError(t, err) |
||||
|
||||
// We ignore certain fields for the purposes of this test
|
||||
if recv != nil { |
||||
for _, not := range recv.GrafanaManagedReceivers { |
||||
not.UID = "" |
||||
not.Settings = nil |
||||
not.SecureSettings = nil |
||||
} |
||||
} |
||||
|
||||
require.Equal(t, tt.expRecv, recv) |
||||
require.Equal(t, tt.expRoute, route) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue