diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 5fd9f7960c5..ac2354d8d6d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1009,6 +1009,11 @@ export interface FeatureToggles { */ alertRuleUseFiredAtForStartsAt?: boolean; /** + * Generate simplified routing with old hashes + * @default false + */ + alertingGenerateSimplifiedRoutingWithOldHashes?: boolean; + /** * Enables the alerting bulk actions in the UI * @default true */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9e368a4bc5c..6ee6382e1d1 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1739,6 +1739,13 @@ var ( Owner: grafanaAlertingSquad, Expression: "false", }, + { + Name: "alertingGenerateSimplifiedRoutingWithOldHashes", + Description: "Generate simplified routing with old hashes", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + Expression: "false", + }, { Name: "alertingBulkActionsInUI", Description: "Enables the alerting bulk actions in the UI", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4e489bf84f9..846bd800a53 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -226,6 +226,7 @@ tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,fal pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false +alertingGenerateSimplifiedRoutingWithOldHashes,experimental,@grafana/alerting-squad,false,false,false alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthZHandlerRedirect,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 097cd66ab45..710f0404365 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -915,6 +915,10 @@ const ( // Use FiredAt for StartsAt when sending alerts to Alertmaanger FlagAlertRuleUseFiredAtForStartsAt = "alertRuleUseFiredAtForStartsAt" + // FlagAlertingGenerateSimplifiedRoutingWithOldHashes + // Generate simplified routing with old hashes + FlagAlertingGenerateSimplifiedRoutingWithOldHashes = "alertingGenerateSimplifiedRoutingWithOldHashes" + // FlagAlertingBulkActionsInUI // Enables the alerting bulk actions in the UI FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 7e32061c9d5..2e027cce650 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -307,6 +307,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "alertingGenerateSimplifiedRoutingWithOldHashes", + "resourceVersion": "1759334385382", + "creationTimestamp": "2025-10-01T15:59:45Z" + }, + "spec": { + "description": "Generate simplified routing with old hashes", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "expression": "false" + } + }, { "metadata": { "name": "alertingImportAlertmanagerAPI", diff --git a/pkg/services/ngalert/models/notifications.go b/pkg/services/ngalert/models/notifications.go index 36e4df6ae0d..f3793f9a881 100644 --- a/pkg/services/ngalert/models/notifications.go +++ b/pkg/services/ngalert/models/notifications.go @@ -199,3 +199,42 @@ func (s *NotificationSettings) Fingerprint() data.Fingerprint { return data.Fingerprint(h.Sum64()) } + +// FingerprintOld calculates a hash value using the old algorithm (before the separator fix in the Fingerpring function). +// This is used temporarily during migration to support existing alerts with old hash labels. +// TODO: Remove this method once the migration is complete. +func (s *NotificationSettings) FingerprintOld() data.Fingerprint { + h := fnv.New64() + tmp := make([]byte, 8) + + writeString := func(s string) { + // save on extra slice allocation when string is converted to bytes. + _, _ = h.Write(unsafe.Slice(unsafe.StringData(s), len(s))) //nolint:gosec + // ignore errors returned by Write method because fnv never returns them. + _, _ = h.Write([]byte{255}) // use an invalid utf-8 sequence as separator + } + writeDuration := func(d *model.Duration) { + if d == nil { + _, _ = h.Write([]byte{255}) + } else { + binary.LittleEndian.PutUint64(tmp, uint64(*d)) + _, _ = h.Write(tmp) + _, _ = h.Write([]byte{255}) + } + } + + writeString(s.Receiver) + for _, gb := range s.NormalizedGroupBy() { + writeString(gb) + } + writeDuration(s.GroupWait) + writeDuration(s.GroupInterval) + writeDuration(s.RepeatInterval) + for _, interval := range s.MuteTimeIntervals { + writeString(interval) + } + for _, interval := range s.ActiveTimeIntervals { + writeString(interval) + } + return data.Fingerprint(h.Sum64()) +} diff --git a/pkg/services/ngalert/models/notifications_test.go b/pkg/services/ngalert/models/notifications_test.go index 2e7df039895..b27e4e0e817 100644 --- a/pkg/services/ngalert/models/notifications_test.go +++ b/pkg/services/ngalert/models/notifications_test.go @@ -201,6 +201,55 @@ func TestNotificationSettingsLabels(t *testing.T) { } } +func TestNotificationSettings_OldFingerprint(t *testing.T) { + testCases := []struct { + name string + notificationSettings NotificationSettings + expectedOldFingerprint string + }{ + { + name: "default notification settings with hardcoded default group by", + notificationSettings: NotificationSettings{ + Receiver: "receiver name", + GroupBy: DefaultNotificationSettingsGroupBy, + }, + expectedOldFingerprint: "6027cdeaff62ba3f", + }, + { + name: "custom notification settings", + notificationSettings: NotificationSettings{ + Receiver: "receiver name", + GroupBy: []string{"label1", "label2"}, + GroupWait: util.Pointer(model.Duration(1 * time.Minute)), + GroupInterval: util.Pointer(model.Duration(2 * time.Minute)), + RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)), + MuteTimeIntervals: []string{"maintenance1", "maintenance2"}, + }, + expectedOldFingerprint: "47164c92f2986a35", + }, + { + name: "custom notification settings with active time interval", + notificationSettings: NotificationSettings{ + Receiver: "receiver name", + GroupBy: []string{"label1", "label2"}, + GroupWait: util.Pointer(model.Duration(1 * time.Minute)), + GroupInterval: util.Pointer(model.Duration(2 * time.Minute)), + RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)), + MuteTimeIntervals: []string{"maintenance1", "maintenance2"}, + ActiveTimeIntervals: []string{"active1", "active2"}, + }, + expectedOldFingerprint: "a173df6210e43af0", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + oldFp := tt.notificationSettings.FingerprintOld() + require.Equal(t, tt.expectedOldFingerprint, oldFp.String()) + }) + } +} + func TestNotificationSettings_TimeIntervals(t *testing.T) { // Create notification settings with default settings and usign the same // time interval, but in one case as a mute time interval and in another case diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 2f18840d50a..a014048a46a 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -220,7 +220,7 @@ func (ng *AlertNG) init() error { Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout, } autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, skipInvalid bool) error { - return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, skipInvalid) + return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, skipInvalid, ng.FeatureToggles) } // This function will be used by the MOA to create new Alertmanagers. diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 8c833c14a2b..06c5b5e0cce 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -56,6 +56,7 @@ type alertmanager struct { DefaultConfiguration string decryptFn alertingNotify.GetDecryptedValueFn crypto Crypto + features featuremgmt.FeatureToggles } // maintenanceOptions represent the options for components that need maintenance on a frequency within the Alertmanager. @@ -155,6 +156,7 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A logger: l.New("component", "alertmanager", opts.TenantKey, opts.TenantID), // similar to what the base does decryptFn: decryptFn, crypto: crypto, + features: featureToggles, } return am, nil @@ -344,7 +346,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable templates := alertingNotify.PostableAPITemplatesToTemplateDefinitions(cfg.GetMergedTemplateDefinitions()) // Now add autogenerated config to the route. - err = AddAutogenConfig(ctx, am.logger, am.Store, am.Base.TenantID(), &amConfig, skipInvalid) + err = AddAutogenConfig(ctx, am.logger, am.Store, am.Base.TenantID(), &amConfig, skipInvalid, am.features) if err != nil { return false, err } diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 25a24cb5441..9f101621ef8 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -133,7 +133,7 @@ func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Contex // Otherwise, broken settings (e.g. a receiver that doesn't exist) will cause the config returned here to be // different than the config currently in-use. // TODO: Preferably, we'd be getting the config directly from the in-memory AM so adding the autogen config would not be necessary. - err := AddAutogenConfig(ctx, moa.logger, moa.configStore, org, &cfg.AlertmanagerConfig, true) + err := AddAutogenConfig(ctx, moa.logger, moa.configStore, org, &cfg.AlertmanagerConfig, true, moa.featureManager) if err != nil { return definitions.GettableUserConfig{}, err } diff --git a/pkg/services/ngalert/notifier/autogen_alertmanager.go b/pkg/services/ngalert/notifier/autogen_alertmanager.go index 55f0201fcb3..4ab8b35b5f2 100644 --- a/pkg/services/ngalert/notifier/autogen_alertmanager.go +++ b/pkg/services/ngalert/notifier/autogen_alertmanager.go @@ -12,6 +12,7 @@ import ( "golang.org/x/exp/maps" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -22,8 +23,8 @@ type autogenRuleStore interface { // AddAutogenConfig creates the autogenerated configuration and adds it to the given apiAlertingConfig. // If skipInvalid is true, then invalid notification settings are skipped, otherwise an error is returned. -func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], skipInvalid bool) error { - autogenRoute, err := newAutogeneratedRoute(ctx, logger, store, orgId, cfg, skipInvalid) +func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], skipInvalid bool, features featuremgmt.FeatureToggles) error { + autogenRoute, err := newAutogeneratedRoute(ctx, logger, store, orgId, cfg, skipInvalid, features) if err != nil { return err } @@ -39,7 +40,7 @@ func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store // newAutogeneratedRoute creates a new autogenerated route based on the notification settings for the given org. // cfg is used to construct the settings validator and to ensure we create a dedicated route for each receiver. // skipInvalid is used to skip invalid settings instead of returning an error. -func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], skipInvalid bool) (autogeneratedRoute, error) { +func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], skipInvalid bool, features featuremgmt.FeatureToggles) (autogeneratedRoute, error) { settings, err := store.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgId}) if err != nil { return autogeneratedRoute{}, fmt.Errorf("failed to list alert rules: %w", err) @@ -76,7 +77,7 @@ func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, s if len(notificationSettings) == 0 { return autogeneratedRoute{}, nil } - newAutogenRoute, err := generateRouteFromSettings(cfg.GetRoute().Receiver, notificationSettings) + newAutogenRoute, err := generateRouteFromSettings(cfg.GetRoute().Receiver, notificationSettings, features) if err != nil { return autogeneratedRoute{}, fmt.Errorf("failed to create autogenerated route: %w", err) } @@ -91,7 +92,7 @@ type autogeneratedRoute struct { // 1. with matcher by label models.AutogeneratedRouteLabel equals 'true'. // 2. with matcher by receiver name. // 3. with matcher by unique combination of optional settings. It is created only if there are optional settings. -func generateRouteFromSettings(defaultReceiver string, settings map[data.Fingerprint]models.NotificationSettings) (autogeneratedRoute, error) { +func generateRouteFromSettings(defaultReceiver string, settings map[data.Fingerprint]models.NotificationSettings, features featuremgmt.FeatureToggles) (autogeneratedRoute, error) { keys := maps.Keys(settings) // sort keys to make sure that the hash we calculate using it is stable slices.Sort(keys) @@ -135,26 +136,34 @@ func generateRouteFromSettings(defaultReceiver string, settings map[data.Fingerp if s.IsAllDefault() { continue } - settingMatcher, err := labels.NewMatcher(labels.MatchEqual, models.AutogeneratedRouteSettingsHashLabel, fingerprint.String()) - if err != nil { - return autogeneratedRoute{}, err - } + normalized := s.NormalizedGroupBy() groupByAll, groupBy := toGroupBy(normalized...) - receiverRoute.Routes = append(receiverRoute.Routes, &definitions.Route{ - Receiver: s.Receiver, - ObjectMatchers: definitions.ObjectMatchers{settingMatcher}, - Continue: false, // Only a single setting-specific route should match. - GroupByStr: normalized, - GroupBy: groupBy, - GroupByAll: groupByAll, - MuteTimeIntervals: s.MuteTimeIntervals, - ActiveTimeIntervals: s.ActiveTimeIntervals, - GroupWait: s.GroupWait, - GroupInterval: s.GroupInterval, - RepeatInterval: s.RepeatInterval, - }) + fingerprints := []data.Fingerprint{fingerprint} + if features.IsEnabledGlobally(featuremgmt.FlagAlertingGenerateSimplifiedRoutingWithOldHashes) { + fingerprints = append([]data.Fingerprint{s.FingerprintOld()}, fingerprints...) + } + + for _, fp := range fingerprints { + matcher, err := labels.NewMatcher(labels.MatchEqual, models.AutogeneratedRouteSettingsHashLabel, fp.String()) + if err != nil { + return autogeneratedRoute{}, err + } + receiverRoute.Routes = append(receiverRoute.Routes, &definitions.Route{ + Receiver: s.Receiver, + ObjectMatchers: definitions.ObjectMatchers{matcher}, + Continue: false, // Only a single setting-specific route should match. + GroupByStr: normalized, + GroupBy: groupBy, + GroupByAll: groupByAll, + MuteTimeIntervals: s.MuteTimeIntervals, + ActiveTimeIntervals: s.ActiveTimeIntervals, + GroupWait: s.GroupWait, + GroupInterval: s.GroupInterval, + RepeatInterval: s.RepeatInterval, + }) + } } return autogeneratedRoute{ diff --git a/pkg/services/ngalert/notifier/autogen_alertmanager_test.go b/pkg/services/ngalert/notifier/autogen_alertmanager_test.go index 78239c06479..6fec524ff1a 100644 --- a/pkg/services/ngalert/notifier/autogen_alertmanager_test.go +++ b/pkg/services/ngalert/notifier/autogen_alertmanager_test.go @@ -13,12 +13,15 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/services/featuremgmt" "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) { + withOldHashFeature := featuremgmt.WithFeatures(featuremgmt.FlagAlertingGenerateSimplifiedRoutingWithOldHashes) + rootRoute := func() *definitions.Route { return &definitions.Route{ Receiver: "default", @@ -69,6 +72,7 @@ func TestAddAutogenConfig(t *testing.T) { existingConfig *definitions.PostableApiAlertingConfig storeSettings []models.NotificationSettings skipInvalid bool + featureToggles featuremgmt.FeatureToggles expRoute *definitions.Route expErrorContains string }{ @@ -76,12 +80,14 @@ func TestAddAutogenConfig(t *testing.T) { name: "no settings or receivers, no change", existingConfig: configGen(nil, nil), storeSettings: []models.NotificationSettings{}, + featureToggles: withOldHashFeature, expRoute: rootRoute(), }, { name: "no settings but some receivers, add default routes for receivers", existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3"}, nil), storeSettings: []models.NotificationSettings{}, + featureToggles: withOldHashFeature, expRoute: withChildRoutes(rootRoute(), &definitions.Route{ Receiver: "default", ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"), @@ -109,6 +115,7 @@ func TestAddAutogenConfig(t *testing.T) { { name: "settings with custom options, add option-specific routes", existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3", "receiver4", "receiver5"}, []string{"maintenance", "active"}), + featureToggles: withOldHashFeature, 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))), @@ -130,50 +137,117 @@ func TestAddAutogenConfig(t *testing.T) { Receiver: "default", ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"), Routes: []*definitions.Route{ - withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{ - Receiver: "receiver1", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "02466789dc88da23"), - 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"}, - ActiveTimeIntervals: []string{"active"}, - }, &definitions.Route{ - Receiver: "receiver1", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), - GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), - }), - withChildRoutes(basicContactRoute("receiver2"), &definitions.Route{ - Receiver: "receiver2", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "63ad04d6c21c3aec"), - GroupWait: util.Pointer(model.Duration(2 * time.Minute)), - }), - withChildRoutes(basicContactRoute("receiver5"), &definitions.Route{ - Receiver: "receiver5", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "8cd5f9adeac58123"), - ActiveTimeIntervals: []string{"active"}, - }, &definitions.Route{ - Receiver: "receiver5", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "f0770544f1741cf6"), - MuteTimeIntervals: []string{"maintenance"}, - }), - withChildRoutes(basicContactRoute("receiver4"), &definitions.Route{ - Receiver: "receiver4", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "9bbbec5f72627ae5"), - GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"}, - }), - withChildRoutes(basicContactRoute("receiver3"), &definitions.Route{ - Receiver: "receiver3", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "fbcacbfae385a901"), - RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)), - }), + withChildRoutes(basicContactRoute("receiver1"), + // Old hash for complex settings + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "f134b8faf7db083c"), + 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"}, + ActiveTimeIntervals: []string{"active"}, + }, + // New hash for complex settings + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "02466789dc88da23"), + 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"}, + ActiveTimeIntervals: []string{"active"}, + }, + // Old hash for simple GroupInterval + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"), + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + // New hash for simple GroupInterval + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + ), + withChildRoutes(basicContactRoute("receiver2"), + // Old hash + &definitions.Route{ + Receiver: "receiver2", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "27e1d1717c9ef621"), + GroupWait: util.Pointer(model.Duration(2 * time.Minute)), + }, + // New hash + &definitions.Route{ + Receiver: "receiver2", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "63ad04d6c21c3aec"), + GroupWait: util.Pointer(model.Duration(2 * time.Minute)), + }, + ), + withChildRoutes(basicContactRoute("receiver5"), + // Old hash for active intervals + &definitions.Route{ + Receiver: "receiver5", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "cd6cd2089632453c"), + ActiveTimeIntervals: []string{"active"}, + }, + // New hash for active intervals + &definitions.Route{ + Receiver: "receiver5", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "8cd5f9adeac58123"), + ActiveTimeIntervals: []string{"active"}, + }, + // Old hash for mute intervals + &definitions.Route{ + Receiver: "receiver5", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "030d6474aec0b553"), + MuteTimeIntervals: []string{"maintenance"}, + }, + // New hash for mute intervals + &definitions.Route{ + Receiver: "receiver5", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "f0770544f1741cf6"), + MuteTimeIntervals: []string{"maintenance"}, + }, + ), + withChildRoutes(basicContactRoute("receiver4"), + // Old hash + &definitions.Route{ + Receiver: "receiver4", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "b3a2fa5e615dcc7e"), + GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"}, + }, + // New hash + &definitions.Route{ + Receiver: "receiver4", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "9bbbec5f72627ae5"), + GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"}, + }, + ), + withChildRoutes(basicContactRoute("receiver3"), + // Old hash + &definitions.Route{ + Receiver: "receiver3", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "9e282ef0193d830a"), + RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)), + }, + // New hash + &definitions.Route{ + Receiver: "receiver3", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "fbcacbfae385a901"), + 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), + featureToggles: withOldHashFeature, storeSettings: []models.NotificationSettings{ models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute)), models.NSMuts.WithGroupBy()), }, @@ -181,18 +255,29 @@ func TestAddAutogenConfig(t *testing.T) { Receiver: "default", ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"), Routes: []*definitions.Route{ - withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{ - Receiver: "receiver1", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), - GroupByStr: nil, - GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), - }), + withChildRoutes(basicContactRoute("receiver1"), + // Old hash + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"), + GroupByStr: nil, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + // New hash + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), + 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), + featureToggles: withOldHashFeature, 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...)), @@ -201,17 +286,36 @@ func TestAddAutogenConfig(t *testing.T) { Receiver: "default", ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"), Routes: []*definitions.Route{ - withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{ - Receiver: "receiver1", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "828092ed6f427a00"), // Different hash. - GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel}, - GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), - }, &definitions.Route{ - Receiver: "receiver1", - ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), - GroupByStr: nil, - GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), - }), + withChildRoutes(basicContactRoute("receiver1"), + // Old hash for explicit default groupBy + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "e1f3a275a8918385"), + GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel}, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + // New hash for explicit default groupBy + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "828092ed6f427a00"), + GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel}, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + // Old hash for nil groupBy + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"), + GroupByStr: nil, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + // New hash for nil groupBy + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), + GroupByStr: nil, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + ), }, }), }, @@ -223,16 +327,27 @@ func TestAddAutogenConfig(t *testing.T) { 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...)), }, + featureToggles: withOldHashFeature, 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, "828092ed6f427a00"), - GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel}, - GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), - }), + withChildRoutes(basicContactRoute("receiver1"), + // Old hash + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "e1f3a275a8918385"), + GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel}, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + // New hash + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "828092ed6f427a00"), + GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel}, + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + ), }, }), }, @@ -244,7 +359,8 @@ func TestAddAutogenConfig(t *testing.T) { 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, + featureToggles: withOldHashFeature, + skipInvalid: true, expRoute: withChildRoutes(rootRoute(), &definitions.Route{ Receiver: "default", ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"), @@ -276,6 +392,27 @@ func TestAddAutogenConfig(t *testing.T) { skipInvalid: false, expErrorContains: "group wait", }, + { + name: "without feature toggle, only new hash route is generated", + existingConfig: configGen([]string{"receiver1"}, nil), + storeSettings: []models.NotificationSettings{ + models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute))), + }, + expRoute: withChildRoutes(rootRoute(), &definitions.Route{ + Receiver: "default", + ObjectMatchers: matcher(models.AutogeneratedRouteLabel, "true"), + Routes: []*definitions.Route{ + withChildRoutes(basicContactRoute("receiver1"), + // Only new hash route + &definitions.Route{ + Receiver: "receiver1", + ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "efc87d76ccc550bc"), + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + }, + ), + }, + }), + }, } for _, tt := range testCases { @@ -290,7 +427,12 @@ func TestAddAutogenConfig(t *testing.T) { 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) + features := tt.featureToggles + if features == nil { + features = featuremgmt.WithFeatures() + } + + err := AddAutogenConfig(context.Background(), &logtest.Fake{}, store, orgId, tt.existingConfig, tt.skipInvalid, features) if tt.expErrorContains != "" { require.Error(t, err) require.ErrorContains(t, err, tt.expErrorContains)