mirror of
https://github.com/grafana/grafana.git
synced 2025-09-25 21:34:01 +08:00

* 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.
472 lines
15 KiB
Go
472 lines
15 KiB
Go
package ualert
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/prometheus/alertmanager/pkg/labels"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
)
|
|
|
|
func TestFilterReceiversForAlert(t *testing.T) {
|
|
tc := []struct {
|
|
name string
|
|
channelIds []uidOrID
|
|
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",
|
|
channelIds: []uidOrID{"uid1", "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",
|
|
channelIds: []uidOrID{"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",
|
|
channelIds: []uidOrID{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",
|
|
channelIds: []uidOrID{"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.channelIds, tt.receivers, tt.defaultReceivers)
|
|
|
|
require.Equal(t, tt.expected, res)
|
|
})
|
|
}
|
|
}
|
|
|
|
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",
|
|
channel: ¬ificationChannel{},
|
|
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),
|
|
},
|
|
},
|
|
{
|
|
name: "notification channel should be escaped for regex in the matcher",
|
|
channel: ¬ificationChannel{},
|
|
recv: &PostableApiReceiver{
|
|
Name: `. ^ $ * + - ? ( ) [ ] { } \ |`,
|
|
},
|
|
expected: &Route{
|
|
Receiver: `. ^ $ * + - ? ( ) [ ] { } \ |`,
|
|
ObjectMatchers: ObjectMatchers{{Type: 2, Name: ContactLabel, Value: `.*"\. \^ \$ \* \+ - \? \( \) \[ \] \{ \} \\ \|".*`}},
|
|
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: ¬ificationChannel{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: ¬ificationChannel{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(channelReceiver{
|
|
channel: tt.channel,
|
|
receiver: tt.recv,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Order of nested routes is not guaranteed.
|
|
cOpt := []cmp.Option{
|
|
cmpopts.SortSlices(func(a, b *Route) bool {
|
|
if a.Receiver != b.Receiver {
|
|
return a.Receiver < b.Receiver
|
|
}
|
|
return a.ObjectMatchers[0].Value < b.ObjectMatchers[0].Value
|
|
}),
|
|
cmpopts.IgnoreUnexported(Route{}, labels.Matcher{}),
|
|
}
|
|
|
|
if !cmp.Equal(tt.expected, res, cOpt...) {
|
|
t.Errorf("Unexpected Route: %v", cmp.Diff(tt.expected, res, cOpt...))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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 createNotChannelWithReminder(t *testing.T, uid string, id int64, name string, frequency model.Duration) *notificationChannel {
|
|
t.Helper()
|
|
return ¬ificationChannel{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 []channelReceiver
|
|
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: []channelReceiver{
|
|
{
|
|
channel: createNotChannel(t, "uid1", int64(1), "name1"),
|
|
receiver: &PostableApiReceiver{
|
|
Name: "name1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}},
|
|
},
|
|
},
|
|
{
|
|
channel: createNotChannel(t, "uid2", int64(2), "name2"),
|
|
receiver: &PostableApiReceiver{
|
|
Name: "name2",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name2"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when given notification channel contains double quote sanitize with underscore",
|
|
allChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name\"1")},
|
|
expRecvMap: map[uidOrID]*PostableApiReceiver{
|
|
"uid1": {
|
|
Name: "name_1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
|
|
},
|
|
int64(1): {
|
|
Name: "name_1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
|
|
},
|
|
},
|
|
expRecv: []channelReceiver{
|
|
{
|
|
channel: createNotChannel(t, "uid1", int64(1), "name\"1"),
|
|
receiver: &PostableApiReceiver{
|
|
Name: "name_1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when given notification channels collide after sanitization add short hash to end",
|
|
allChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name\"1"), createNotChannel(t, "uid2", int64(2), "name_1")},
|
|
expRecvMap: map[uidOrID]*PostableApiReceiver{
|
|
"uid1": {
|
|
Name: "name_1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
|
|
},
|
|
"uid2": {
|
|
Name: "name_1_dba13d",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1_dba13d"}},
|
|
},
|
|
int64(1): {
|
|
Name: "name_1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
|
|
},
|
|
int64(2): {
|
|
Name: "name_1_dba13d",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1_dba13d"}},
|
|
},
|
|
},
|
|
expRecv: []channelReceiver{
|
|
{
|
|
channel: createNotChannel(t, "uid1", int64(1), "name\"1"),
|
|
receiver: &PostableApiReceiver{
|
|
Name: "name_1",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1"}},
|
|
},
|
|
},
|
|
{
|
|
channel: createNotChannel(t, "uid2", int64(2), "name_1"),
|
|
receiver: &PostableApiReceiver{
|
|
Name: "name_1_dba13d",
|
|
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name_1_dba13d"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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.receiver.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),
|
|
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)),
|
|
},
|
|
},
|
|
{
|
|
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),
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
RepeatInterval: nil,
|
|
},
|
|
},
|
|
{
|
|
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),
|
|
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)),
|
|
},
|
|
},
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func durationPointer(d model.Duration) *model.Duration {
|
|
return &d
|
|
}
|