diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index da7211791e4..27b0de8b62a 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -223,7 +223,7 @@ func TestNotificationPolicyService(t *testing.T) { require.NoError(t, err) require.Equal(t, "grafana-default-email", tree.Receiver) require.Nil(t, tree.Routes) - require.Nil(t, tree.GroupBy) + require.Equal(t, []model.LabelName{models.FolderTitleLabel, model.AlertNameLabel}, tree.GroupBy) }) } diff --git a/pkg/services/sqlstore/migrations/ualert/channel.go b/pkg/services/sqlstore/migrations/ualert/channel.go index 27e0317c812..5f193037e55 100644 --- a/pkg/services/sqlstore/migrations/ualert/channel.go +++ b/pkg/services/sqlstore/migrations/ualert/channel.go @@ -7,8 +7,10 @@ import ( "fmt" "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/common/model" "github.com/grafana/grafana/pkg/components/simplejson" + ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/util" ) @@ -229,8 +231,9 @@ func (m *migration) createDefaultRouteAndReceiver(defaultChannels []*notificatio } defaultRoute := &Route{ - Receiver: defaultReceiverName, - Routes: make([]*Route, 0), + Receiver: defaultReceiverName, + Routes: make([]*Route, 0), + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, // To keep parity with pre-migration notifications. } return defaultReceiver, defaultRoute, nil @@ -438,10 +441,11 @@ type PostableApiAlertingConfig struct { } type Route struct { - Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` - Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` - Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` - Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` + Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` + Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` + Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` + GroupByStr []string `yaml:"group_by,omitempty" json:"group_by,omitempty"` } type Matchers labels.Matchers diff --git a/pkg/services/sqlstore/migrations/ualert/channel_test.go b/pkg/services/sqlstore/migrations/ualert/channel_test.go index 688b3d62eb1..93be8d65d46 100644 --- a/pkg/services/sqlstore/migrations/ualert/channel_test.go +++ b/pkg/services/sqlstore/migrations/ualert/channel_test.go @@ -3,9 +3,11 @@ package ualert import ( "testing" + "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" + ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" ) func TestFilterReceiversForAlert(t *testing.T) { @@ -144,10 +146,11 @@ func TestCreateRoute(t *testing.T) { "recv1": struct{}{}, }, expected: &Route{ - Receiver: "recv1", - Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, - Routes: nil, - Continue: false, + Receiver: "recv1", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: nil, + Continue: false, + GroupByStr: nil, }, }, { @@ -162,19 +165,22 @@ func TestCreateRoute(t *testing.T) { Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, Routes: []*Route{ { - Receiver: "recv1", - Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, - Routes: nil, - Continue: true, + Receiver: "recv1", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: nil, + Continue: true, + GroupByStr: nil, }, { - Receiver: "recv2", - Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, - Routes: nil, - Continue: true, + Receiver: "recv2", + Matchers: Matchers{{Type: 0, Name: "rule_uid", Value: "r_uid1"}}, + Routes: nil, + Continue: true, + GroupByStr: nil, }, }, - Continue: false, + Continue: false, + GroupByStr: nil, }, }, } @@ -294,8 +300,9 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) { GrafanaManagedReceivers: []*PostableGrafanaReceiver{{Name: "name1"}, {Name: "name2"}}, }, expRoute: &Route{ - Receiver: "autogen-contact-point-default", - Routes: make([]*Route, 0), + Receiver: "autogen-contact-point-default", + Routes: make([]*Route, 0), + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, }, { @@ -306,8 +313,9 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) { GrafanaManagedReceivers: []*PostableGrafanaReceiver{}, }, expRoute: &Route{ - Receiver: "autogen-contact-point-default", - Routes: make([]*Route, 0), + Receiver: "autogen-contact-point-default", + Routes: make([]*Route, 0), + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, }, { @@ -315,8 +323,9 @@ func TestCreateDefaultRouteAndReceiver(t *testing.T) { defaultChannels: []*notificationChannel{createNotChannel(t, "uid1", int64(1), "name1")}, expRecv: nil, expRoute: &Route{ - Receiver: "name1", - Routes: make([]*Route, 0), + Receiver: "name1", + Routes: make([]*Route, 0), + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, }, } diff --git a/pkg/services/sqlstore/migrations/ualert/migration_test.go b/pkg/services/sqlstore/migrations/ualert/migration_test.go index 2736eaf3eba..d162fdd4993 100644 --- a/pkg/services/sqlstore/migrations/ualert/migration_test.go +++ b/pkg/services/sqlstore/migrations/ualert/migration_test.go @@ -9,12 +9,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "xorm.io/xorm" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/datasources" + ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -156,7 +158,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, Routes: []*ualert.Route{ {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")}, // These Matchers are temporary and will be replaced below with generated rule_uid. {Matchers: createAlertNameMatchers("alert2"), Routes: []*ualert.Route{ @@ -177,7 +180,8 @@ func TestDashAlertMigration(t *testing.T) { int64(2): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "notifier6", + Receiver: "notifier6", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, Routes: []*ualert.Route{ {Matchers: createAlertNameMatchers("alert4"), Routes: []*ualert.Route{ {Receiver: "notifier4", Matchers: createAlertNameMatchers("alert4"), Continue: true}, @@ -209,7 +213,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -229,7 +234,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "notifier1", + Receiver: "notifier1", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -249,7 +255,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -272,7 +279,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -297,7 +305,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -322,7 +331,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, Routes: []*ualert.Route{ {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")}, {Matchers: createAlertNameMatchers("alert2"), Routes: []*ualert.Route{ @@ -350,7 +360,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -372,7 +383,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, }, Receivers: []*ualert.PostableApiReceiver{ {Name: "notifier1", GrafanaManagedReceivers: []*ualert.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}, @@ -395,7 +407,8 @@ func TestDashAlertMigration(t *testing.T) { int64(1): { AlertmanagerConfig: ualert.PostableApiAlertingConfig{ Route: &ualert.Route{ - Receiver: "autogen-contact-point-default", + Receiver: "autogen-contact-point-default", + GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel}, Routes: []*ualert.Route{ {Receiver: "notifier1", Matchers: createAlertNameMatchers("alert1")}, }, diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index 5762760c38d..e0153807502 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -26,7 +26,8 @@ const ( alertmanagerDefaultConfiguration = `{ "alertmanager_config": { "route": { - "receiver": "grafana-default-email" + "receiver": "grafana-default-email", + "group_by": ["grafana_folder", "alertname"] }, "receivers": [{ "name": "grafana-default-email", diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 72040631eba..9c34290fbf1 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -1832,7 +1832,8 @@ func TestAlertmanagerStatus(t *testing.T) { }, "config": { "route": { - "receiver": "grafana-default-email" + "receiver": "grafana-default-email", + "group_by": ["grafana_folder", "alertname"] }, "templates": null, "receivers": [{ diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index 83fe4b38d1c..a9e1883736b 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -25,7 +25,8 @@ const defaultAlertmanagerConfigJSON = ` "template_files": null, "alertmanager_config": { "route": { - "receiver": "grafana-default-email" + "receiver": "grafana-default-email", + "group_by": ["grafana_folder", "alertname"] }, "templates": null, "receivers": [{ diff --git a/public/app/features/alerting/unified/AmRoutes.test.tsx b/public/app/features/alerting/unified/AmRoutes.test.tsx index 819823f6756..96c08e6f39c 100644 --- a/public/app/features/alerting/unified/AmRoutes.test.tsx +++ b/public/app/features/alerting/unified/AmRoutes.test.tsx @@ -21,6 +21,7 @@ import { AccessControlAction } from 'app/types'; import AmRoutes from './AmRoutes'; import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager'; import { mockDataSource, MockDataSourceSrv, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks'; +import { defaultGroupBy } from './utils/amroutes'; import { getAllDataSources } from './utils/config'; import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; @@ -363,7 +364,7 @@ describe('AmRoutes', () => { receivers: [{ name: 'default' }], route: { continue: false, - group_by: ['severity', 'namespace'], + group_by: defaultGroupBy.concat(['severity', 'namespace']), receiver: 'default', routes: [], mute_time_intervals: [], diff --git a/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx b/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx index 3a98e732783..c0d5d24ce0d 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx @@ -10,6 +10,7 @@ import { optionalPositiveInteger, stringToSelectableValue, stringsToSelectableValues, + commonGroupByOptions, } from '../../utils/amroutes'; import { makeAMLink } from '../../utils/misc'; import { timeOptions } from '../../utils/time'; @@ -86,7 +87,7 @@ export const AmRootRouteForm: FC = ({ setValue('groupBy', [...field.value, opt]); }} onChange={(value) => onChange(mapMultiSelectValueToStrings(value))} - options={groupByOptions} + options={[...commonGroupByOptions, groupByOptions]} /> )} control={control} diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx index d95f984a09c..a58f085ed74 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx @@ -29,6 +29,7 @@ import { optionalPositiveInteger, stringToSelectableValue, stringsToSelectableValues, + commonGroupByOptions, } from '../../utils/amroutes'; import { timeOptions } from '../../utils/time'; @@ -179,7 +180,7 @@ export const AmRoutesExpandedForm: FC = ({ onCancel, setValue('groupBy', [...field.value, opt]); }} onChange={(value) => onChange(mapMultiSelectValueToStrings(value))} - options={groupByOptions} + options={[...commonGroupByOptions, groupByOptions]} /> )} control={control} diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts index 18f804aae39..57cde530c32 100644 --- a/public/app/features/alerting/unified/utils/amroutes.ts +++ b/public/app/features/alerting/unified/utils/amroutes.ts @@ -59,10 +59,20 @@ export const emptyArrayFieldMatcher: MatcherFieldValue = { operator: MatcherOperator.equal, }; +// Default route group_by labels for newly created routes. +export const defaultGroupBy = ['grafana_folder', 'alertname']; + +// Common route group_by options for multiselect drop-down +export const commonGroupByOptions = [ + { label: 'grafana_folder', value: 'grafana_folder' }, + { label: 'alertname', value: 'alertname' }, + { label: 'Disable (...)', value: '...' }, +]; + export const emptyRoute: FormAmRoute = { id: '', overrideGrouping: false, - groupBy: [], + groupBy: defaultGroupBy, object_matchers: [], routes: [], continue: false,