Alerting: Generate simplified routing routes with old fingerprint function (#111893)
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user