From 1ed1242358c0d3639724e57d86d45903e7a779b2 Mon Sep 17 00:00:00 2001 From: George Robinson Date: Thu, 22 Feb 2024 15:58:56 +0000 Subject: [PATCH] Alerting: Basic support for time_intervals (#83216) This commit adds basic support for time_intervals, as mute_time_intervals is deprecated in Alertmanager and scheduled to be removed before 1.0. It does not add support for time_intervals in API or file provisioning, nor does it support exporting time intervals. This will be added in later commits to keep the changes as simple as possible. --- go.mod | 2 +- go.sum | 6 +- .../api/tooling/definitions/alertmanager.go | 14 ++ .../tooling/definitions/alertmanager_test.go | 163 +++++++++++++++++- pkg/services/ngalert/notifier/config.go | 4 + pkg/services/ngalert/notifier/validation.go | 18 +- .../api_alertmanager_configuration_test.go | 64 +++++++ 7 files changed, 257 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 7cb945f1267..7720d654506 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.6.0 // @grafana/backend-platform github.com/google/wire v0.5.0 // @grafana/backend-platform github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12 // @grafana/alerting-squad-backend + github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 // @grafana/alerting-squad-backend github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.23.1 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources diff --git a/go.sum b/go.sum index af3d2952d8f..23d3e67d150 100644 --- a/go.sum +++ b/go.sum @@ -2505,8 +2505,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12 h1:QepaY7wUP3U1hFiU1Lnv+tymnovzK21KQ/evMDpYsEw= -github.com/grafana/alerting v0.0.0-20240213130827-92f64f0f2a12/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= +github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2 h1:fmUMdtP7ditGgJFdXCwVxDrKnondHNNe0TkhN5YaIAI= +github.com/grafana/alerting v0.0.0-20240222104113-abfafef9a7d2/go.mod h1:brTFeACal/cSZAR8XO/4LPKs7rzNfS86okl6QjSP1eY= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= @@ -2530,8 +2530,6 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.211.0 h1:hYtieOoYvsv/BcFbtspml4OzfuYrv1d14nESdf13qxQ= -github.com/grafana/grafana-plugin-sdk-go v0.211.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/grafana-plugin-sdk-go v0.212.0 h1:ohgMktFAasLTzAhKhcIzk81O60E29Za6ly02GhEqGIU= github.com/grafana/grafana-plugin-sdk-go v0.212.0/go.mod h1:qsI4ktDf0lig74u8SLPJf9zRdVxWV/W4Wi+Ox6gifgs= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 26b540e4017..cc3b57713c4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -739,6 +739,8 @@ func (c *GettableApiAlertingConfig) GetReceivers() []*GettableApiReceiver { return c.Receivers } +func (c *GettableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } + func (c *GettableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { return c.MuteTimeIntervals } @@ -803,6 +805,7 @@ type Config struct { Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` Route *Route `yaml:"route,omitempty" json:"route,omitempty"` InhibitRules []config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` + TimeIntervals []config.TimeInterval `yaml:"time_intervals,omitempty" json:"time_intervals,omitempty"` MuteTimeIntervals []config.MuteTimeInterval `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty"` Templates []string `yaml:"templates" json:"templates"` } @@ -936,6 +939,15 @@ func (c *Config) UnmarshalJSON(b []byte) error { } tiNames := make(map[string]struct{}) + for _, ti := range c.TimeIntervals { + if ti.Name == "" { + return fmt.Errorf("missing name in time interval") + } + if _, ok := tiNames[ti.Name]; ok { + return fmt.Errorf("time interval %q is not unique", ti.Name) + } + tiNames[ti.Name] = struct{}{} + } for _, mt := range c.MuteTimeIntervals { if mt.Name == "" { return fmt.Errorf("missing name in mute time interval") @@ -976,6 +988,8 @@ func (c *PostableApiAlertingConfig) GetReceivers() []*PostableApiReceiver { return c.Receivers } +func (c *PostableApiAlertingConfig) GetTimeIntervals() []config.TimeInterval { return c.TimeIntervals } + func (c *PostableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval { return c.MuteTimeIntervals } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index c549eca7109..e38506ca5df 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -485,7 +485,50 @@ func Test_ConfigUnmashaling(t *testing.T) { err error }{ { - desc: "empty mute time name should error", + desc: "missing time interval name should error", + err: errors.New("missing name in time interval"), + input: ` + { + "route": { + "receiver": "grafana-default-email" + }, + "time_intervals": [ + { + "name": "", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "templates": null, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "uxwfZvtnz", + "name": "email receiver", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + }, + "secureFields": {} + } + ] + } + ] + } + `, + }, { + desc: "missing mute time interval name should error", err: errors.New("missing name in mute time interval"), input: ` { @@ -529,7 +572,64 @@ func Test_ConfigUnmashaling(t *testing.T) { `, }, { - desc: "not unique mute time names should error", + desc: "duplicate time interval names should error", + err: errors.New("time interval \"test1\" is not unique"), + input: ` + { + "route": { + "receiver": "grafana-default-email" + }, + "time_intervals": [ + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + }, + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "templates": null, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "uxwfZvtnz", + "name": "email receiver", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + }, + "secureFields": {} + } + ] + } + ] + } + `, + }, + { + desc: "duplicate mute time interval names should error", err: errors.New("mute time interval \"test1\" is not unique"), input: ` { @@ -585,6 +685,65 @@ func Test_ConfigUnmashaling(t *testing.T) { } `, }, + { + desc: "duplicate time and mute time interval names should error", + err: errors.New("mute time interval \"test1\" is not unique"), + input: ` + { + "route": { + "receiver": "grafana-default-email" + }, + "mute_time_intervals": [ + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "time_intervals": [ + { + "name": "test1", + "time_intervals": [ + { + "times": [ + { + "start_time": "00:00", + "end_time": "12:00" + } + ] + } + ] + } + ], + "templates": null, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "uxwfZvtnz", + "name": "email receiver", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + }, + "secureFields": {} + } + ] + } + ] + } + `, + }, { desc: "mute time intervals on root route should error", err: errors.New("root route must not have any mute time intervals"), diff --git a/pkg/services/ngalert/notifier/config.go b/pkg/services/ngalert/notifier/config.go index 86dfb404049..43b70760a2f 100644 --- a/pkg/services/ngalert/notifier/config.go +++ b/pkg/services/ngalert/notifier/config.go @@ -111,6 +111,10 @@ func (a AlertingConfiguration) InhibitRules() []alertingNotify.InhibitRule { return a.alertmanagerConfig.InhibitRules } +func (a AlertingConfiguration) TimeIntervals() []alertingNotify.TimeInterval { + return a.alertmanagerConfig.TimeIntervals +} + func (a AlertingConfiguration) MuteTimeIntervals() []alertingNotify.MuteTimeInterval { return a.alertmanagerConfig.MuteTimeIntervals } diff --git a/pkg/services/ngalert/notifier/validation.go b/pkg/services/ngalert/notifier/validation.go index 1f2cad0b01a..84964214a23 100644 --- a/pkg/services/ngalert/notifier/validation.go +++ b/pkg/services/ngalert/notifier/validation.go @@ -20,13 +20,14 @@ type NotificationSettingsValidator interface { // staticValidator is a NotificationSettingsValidator that uses static pre-fetched values for available receivers and mute timings. type staticValidator struct { - availableReceivers map[string]struct{} - availableMuteTimings map[string]struct{} + availableReceivers map[string]struct{} + availableTimeIntervals map[string]struct{} } // apiAlertingConfig contains the methods required to validate NotificationSettings and create autogen routes. type apiAlertingConfig[R receiver] interface { GetReceivers() []R + GetTimeIntervals() []config.TimeInterval GetMuteTimeIntervals() []config.MuteTimeInterval GetRoute() *definitions.Route } @@ -42,14 +43,17 @@ func NewNotificationSettingsValidator[R receiver](am apiAlertingConfig[R]) Notif availableReceivers[receiver.GetName()] = struct{}{} } - availableMuteTimings := make(map[string]struct{}) + availableTimeIntervals := make(map[string]struct{}) + for _, interval := range am.GetTimeIntervals() { + availableTimeIntervals[interval.Name] = struct{}{} + } for _, interval := range am.GetMuteTimeIntervals() { - availableMuteTimings[interval.Name] = struct{}{} + availableTimeIntervals[interval.Name] = struct{}{} } return staticValidator{ - availableReceivers: availableReceivers, - availableMuteTimings: availableMuteTimings, + availableReceivers: availableReceivers, + availableTimeIntervals: availableTimeIntervals, } } @@ -63,7 +67,7 @@ func (n staticValidator) Validate(settings models.NotificationSettings) error { errs = append(errs, fmt.Errorf("receiver '%s' does not exist", settings.Receiver)) } for _, interval := range settings.MuteTimeIntervals { - if _, ok := n.availableMuteTimings[interval]; !ok { + if _, ok := n.availableTimeIntervals[interval]; !ok { errs = append(errs, fmt.Errorf("mute time interval '%s' does not exist", interval)) } } diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go index 7ca9c969c48..f4e09604938 100644 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go @@ -12,6 +12,7 @@ import ( "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -192,6 +193,69 @@ func TestIntegrationAlertmanagerConfiguration(t *testing.T) { }}, }, }, + }, { + name: "configuration with time intervals", + cfg: apimodels.PostableUserConfig{ + AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ + Config: apimodels.Config{ + Route: &apimodels.Route{ + Receiver: "test", + Routes: []*apimodels.Route{{ + MuteTimeIntervals: []string{"weekends"}, + }}, + }, + TimeIntervals: []config.TimeInterval{{ + Name: "weekends", + TimeIntervals: []timeinterval.TimeInterval{{ + Weekdays: []timeinterval.WeekdayRange{{ + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 5, + }, + }}, + }}, + }}, + }, + Receivers: []*apimodels.PostableApiReceiver{{ + Receiver: config.Receiver{ + Name: "test", + }, + }}, + }, + }, + }, { + // TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be + // removed before version 1.0. Remove this test when support for mute time + // intervals is removed. + name: "configuration with mute time intervals", + cfg: apimodels.PostableUserConfig{ + AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ + Config: apimodels.Config{ + Route: &apimodels.Route{ + Receiver: "test", + Routes: []*apimodels.Route{{ + MuteTimeIntervals: []string{"weekends"}, + }}, + }, + MuteTimeIntervals: []config.MuteTimeInterval{{ + Name: "weekends", + TimeIntervals: []timeinterval.TimeInterval{{ + Weekdays: []timeinterval.WeekdayRange{{ + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 5, + }, + }}, + }}, + }}, + }, + Receivers: []*apimodels.PostableApiReceiver{{ + Receiver: config.Receiver{ + Name: "test", + }, + }}, + }, + }, }} for _, tc := range cases {