Skip to content

Commit

Permalink
Use channel names in label instead of uids
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobsonMT committed Nov 19, 2023
1 parent 8e164e9 commit 107199b
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 108 deletions.
37 changes: 21 additions & 16 deletions pkg/services/ngalert/migration/alert_rule.go
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/grafana/grafana/pkg/util"
)

func addLabelsAndAnnotations(l log.Logger, alert *legacymodels.Alert, dashboardUID string, channels []string) (data.Labels, data.Labels) {
func addLabelsAndAnnotations(l log.Logger, alert *legacymodels.Alert, dashboardUID string, channels []*legacymodels.AlertNotification) (data.Labels, data.Labels) {
tags := alert.GetTagsFromSettings()
lbls := make(data.Labels, len(tags)+len(channels)+1)

Expand All @@ -29,7 +29,7 @@ func addLabelsAndAnnotations(l log.Logger, alert *legacymodels.Alert, dashboardU
// Add a label for routing
lbls[ngmodels.MigratedUseLegacyChannelsLabel] = "true"
for _, c := range channels {
lbls[fmt.Sprintf(ngmodels.MigratedContactLabelTemplate, c)] = "true"
lbls[contactLabel(c.Name)] = "true"
}

annotations := make(data.Labels, 4)
Expand Down Expand Up @@ -60,7 +60,7 @@ func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, alert *l
return nil, fmt.Errorf("transform conditions: %w", err)
}

channels := om.extractChannelUIDs(ctx, l, alert.OrgID, parsedSettings)
channels := om.extractChannels(l, parsedSettings)

lbls, annotations := addLabelsAndAnnotations(l, alert, info.DashboardUID, channels)

Expand Down Expand Up @@ -298,22 +298,27 @@ func truncate(daName string, length int) string {
return daName
}

// extractChannelUIDs extracts the notification channel UIDs from the given legacy dashboard alert parsed settings.
func (om *OrgMigration) extractChannelUIDs(ctx context.Context, l log.Logger, orgID int64, parsedSettings dashAlertSettings) (channelUids []string) {
// Extracting channel UID/ID.
for _, ui := range parsedSettings.Notifications {
// extractChannels extracts notification channels from the given legacy dashboard alert parsed settings.
func (om *OrgMigration) extractChannels(l log.Logger, parsedSettings dashAlertSettings) []*legacymodels.AlertNotification {
// Extracting channels.
channels := make([]*legacymodels.AlertNotification, 0, len(parsedSettings.Notifications))
for _, key := range parsedSettings.Notifications {
// Either id or uid can be defined in the dashboard alert notification settings. See alerting.NewRuleFromDBAlert.
if ui.ID > 0 {
uid, err := om.migrationStore.GetAlertNotificationUidWithId(ctx, orgID, ui.ID)
if err != nil {
l.Warn("Failed to get alert notification UID, skipping", "notificationId", ui.ID, "err", err)
if key.ID > 0 {
if c, ok := om.channelCache.GetChannelByID(key.ID); ok {
channels = append(channels, c)
continue
}
}

if key.UID != "" {
if c, ok := om.channelCache.GetChannelByUID(key.UID); ok {
channels = append(channels, c)
continue
}
channelUids = append(channelUids, uid)
} else if ui.UID != "" {
channelUids = append(channelUids, ui.UID)
}
}

return channelUids
l.Warn("Failed to get alert notification, skipping", "notificationKey", key)
}
return channels
}
25 changes: 16 additions & 9 deletions pkg/services/ngalert/migration/channel.go
Expand Up @@ -133,21 +133,23 @@ func (om *OrgMigration) createReceiver(channel *legacymodels.AlertNotification)

// createRoute creates a route from a legacy notification channel, and matches using a label based on the channel UID.
func createRoute(channel *legacymodels.AlertNotification, receiverName string) (*apimodels.Route, error) {
// We create a matchers based on channel UID so that we only need a single route per channel.
// All routes are stored in a nested route under the root. This is so we can keep the migrated channels separate
// We create a matchers based on channel name so that we only need a single route per channel.
// All channel routes are nested in a single route under the root. This is so we can keep the migrated channels separate
// and organized.
// The matchers are created using a label that is unique to the channel UID. So, each migrated alert rule can define
// one label per migrated channel that it should send to.
// Default channels are matched using a catch-all matcher, because in legacy alerting they are attached to all
// alerts automatically.
// Since default channels are attached to all alerts in legacy, we use a catch-all matcher after migration instead
// of a specific label matcher.
//
// For example, if an alert needs to send to channel1 and channel2 it will have two labels:
// For example, if an alert needs to send to channel1 and channel2 it will have one label to route to the nested
// policy and two channel-specific labels to route to the correct contact points:
// - __use_legacy_channels__="true"
// - __contact_channel1__="true"
// - __contact_channel2__="true"
//
// These will match two routes as they are all defined with Continue=true.
// If an alert needs to send to channel1 and the default channel, it will have one label to route to the nested
// policy and one channel-specific label to route to channel1, and a catch-all policy will ensure it also routes to
// the default channel.

label := fmt.Sprintf(ngmodels.MigratedContactLabelTemplate, channel.UID)
label := contactLabel(channel.Name)
mat, err := labels.NewMatcher(labels.MatchEqual, label, "true")
if err != nil {
return nil, err
Expand All @@ -171,6 +173,11 @@ func createRoute(channel *legacymodels.AlertNotification, receiverName string) (
}, nil
}

// contactLabel creates a label matcher key used to route alerts to a contact point.
func contactLabel(name string) string {
return fmt.Sprintf(ngmodels.MigratedContactLabelTemplate, name)
}

var secureKeysToMigrate = map[string][]string{
"slack": {"url", "token"},
"pagerduty": {"integrationKey"},
Expand Down
72 changes: 35 additions & 37 deletions pkg/services/ngalert/migration/channel_test.go
Expand Up @@ -34,24 +34,24 @@ func TestCreateRoute(t *testing.T) {
}{
{
name: "when a receiver is passed in, the route should exact match based on channel uid with continue=true",
channel: &legacymodels.AlertNotification{UID: "uid1"},
recv: createPostableApiReceiver("uid1", "recv1", nil),
channel: &legacymodels.AlertNotification{UID: "uid1", Name: "recv1"},
recv: createPostableApiReceiver("uid1", "recv1"),
expected: &apimodels.Route{
Receiver: "recv1",
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}},
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("recv1"), Value: "true"}},
Routes: nil,
Continue: true,
GroupByStr: nil,
RepeatInterval: durationPointer(DisabledRepeatInterval),
},
},
{
name: "notification channel should be escaped for regex in the matcher",
channel: &legacymodels.AlertNotification{UID: "uid1"},
recv: createPostableApiReceiver("uid1", `. ^ $ * + - ? ( ) [ ] { } \ |`, nil),
name: "notification channel labels matcher should work with special characters",
channel: &legacymodels.AlertNotification{UID: "uid1", Name: `. ^ $ * + - ? ( ) [ ] { } \ |`},
recv: createPostableApiReceiver("uid1", `. ^ $ * + - ? ( ) [ ] { } \ |`),
expected: &apimodels.Route{
Receiver: `. ^ $ * + - ? ( ) [ ] { } \ |`,
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}},
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel(`. ^ $ * + - ? ( ) [ ] { } \ |`), Value: "true"}},
Routes: nil,
Continue: true,
GroupByStr: nil,
Expand All @@ -60,11 +60,11 @@ func TestCreateRoute(t *testing.T) {
},
{
name: "when a channel has sendReminder=true, the route should use the frequency in repeat interval",
channel: &legacymodels.AlertNotification{SendReminder: true, Frequency: time.Duration(42) * time.Hour, UID: "uid1"},
recv: createPostableApiReceiver("uid1", "recv1", nil),
channel: &legacymodels.AlertNotification{SendReminder: true, Frequency: time.Duration(42) * time.Hour, UID: "uid1", Name: "recv1"},
recv: createPostableApiReceiver("uid1", "recv1"),
expected: &apimodels.Route{
Receiver: "recv1",
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}},
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("recv1"), Value: "true"}},
Routes: nil,
Continue: true,
GroupByStr: nil,
Expand All @@ -73,11 +73,11 @@ func TestCreateRoute(t *testing.T) {
},
{
name: "when a channel has sendReminder=false, the route should ignore the frequency in repeat interval and use DisabledRepeatInterval",
channel: &legacymodels.AlertNotification{SendReminder: false, Frequency: time.Duration(42) * time.Hour, UID: "uid1"},
recv: createPostableApiReceiver("uid1", "recv1", nil),
channel: &legacymodels.AlertNotification{SendReminder: false, Frequency: time.Duration(42) * time.Hour, UID: "uid1", Name: "recv1"},
recv: createPostableApiReceiver("uid1", "recv1"),
expected: &apimodels.Route{
Receiver: "recv1",
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}},
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("recv1"), Value: "true"}},
Routes: nil,
Continue: true,
GroupByStr: nil,
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestCreateReceivers(t *testing.T) {
{
name: "when given notification channels migrate them to receivers",
channel: createNotChannel(t, "uid1", int64(1), "name1", false, 0),
expRecv: createPostableApiReceiver("uid1", "name1", []string{"name1"}),
expRecv: createPostableApiReceiver("uid1", "name1"),
},
{
name: "when given hipchat return discontinued error",
Expand Down Expand Up @@ -374,16 +374,16 @@ func TestSetupAlertmanagerConfig(t *testing.T) {
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
Continue: true,
Routes: []*apimodels.Route{
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
},
},
},
}},
Receivers: []*apimodels.PostableApiReceiver{
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{}}},
createPostableApiReceiver("uid1", "notifier1", []string{"notifier1"}),
createPostableApiReceiver("uid2", "notifier2", []string{"notifier2"}),
createPostableApiReceiver("uid1", "notifier1"),
createPostableApiReceiver("uid2", "notifier2"),
},
},
},
Expand All @@ -402,15 +402,15 @@ func TestSetupAlertmanagerConfig(t *testing.T) {
Continue: true,
Routes: []*apimodels.Route{
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
},
},
},
}},
Receivers: []*apimodels.PostableApiReceiver{
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{}}},
createPostableApiReceiver("uid1", "notifier1", []string{"notifier1"}),
createPostableApiReceiver("uid2", "notifier2", []string{"notifier2"}),
createPostableApiReceiver("uid1", "notifier1"),
createPostableApiReceiver("uid2", "notifier2"),
},
},
},
Expand All @@ -428,16 +428,16 @@ func TestSetupAlertmanagerConfig(t *testing.T) {
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
Continue: true,
Routes: []*apimodels.Route{
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(42)},
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: fmt.Sprintf(ngModels.MigratedContactLabelTemplate, "uid2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(43)},
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(42)},
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(43)},
},
},
},
}},
Receivers: []*apimodels.PostableApiReceiver{
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{}}},
createPostableApiReceiver("uid1", "notifier1", []string{"notifier1"}),
createPostableApiReceiver("uid2", "notifier2", []string{"notifier2"})},
createPostableApiReceiver("uid1", "notifier1"),
createPostableApiReceiver("uid2", "notifier2")},
},
},
},
Expand Down Expand Up @@ -469,23 +469,21 @@ func TestSetupAlertmanagerConfig(t *testing.T) {
}
}

func createPostableApiReceiver(uid string, name string, integrationNames []string) *apimodels.PostableApiReceiver {
integrations := make([]*apimodels.PostableGrafanaReceiver, 0, len(integrationNames))
for _, integrationName := range integrationNames {
integrations = append(integrations, &apimodels.PostableGrafanaReceiver{
UID: uid,
Type: "email",
Name: integrationName,
Settings: apimodels.RawMessage("{}"),
SecureSettings: map[string]string{},
})
}
func createPostableApiReceiver(uid string, name string) *apimodels.PostableApiReceiver {
return &apimodels.PostableApiReceiver{
Receiver: config.Receiver{
Name: name,
},
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
GrafanaManagedReceivers: integrations,
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
{
UID: uid,
Type: "email",
Name: name,
Settings: apimodels.RawMessage("{}"),
SecureSettings: map[string]string{},
},
},
},
}
}
Expand Down

0 comments on commit 107199b

Please sign in to comment.