Alerting: Generate simplified routing routes with old fingerprint function (#111893)

This commit is contained in:
Alexander Akhmetov
2025-10-01 18:45:36 +02:00
committed by GitHub
parent 1bd7725a10
commit 0da9d49896
12 changed files with 359 additions and 88 deletions
@@ -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
*/
+7
View File
@@ -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",
+1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
226 pluginsAutoUpdate experimental @grafana/plugins-platform-backend false false false
227 alertingListViewV2PreviewToggle privatePreview @grafana/alerting-squad false false true
228 alertRuleUseFiredAtForStartsAt experimental @grafana/alerting-squad false false false
229 alertingGenerateSimplifiedRoutingWithOldHashes experimental @grafana/alerting-squad false false false
230 alertingBulkActionsInUI GA @grafana/alerting-squad false false true
231 kubernetesAuthzApis experimental @grafana/identity-access-team false false false
232 kubernetesAuthZHandlerRedirect experimental @grafana/identity-access-team false false false
+4
View File
@@ -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"
+13
View File
@@ -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",
@@ -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())
}
@@ -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
+1 -1
View File
@@ -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.
@@ -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
}
@@ -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
}
@@ -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{
@@ -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)