Alerting: Support for simplified notification settings in rule API (#81011)

* Add notification settings to storage\domain and API models. Settings are a slice to workaround XORM mapping
* Support validation of notification settings when rules are updated

* Implement route generator for Alertmanager configuration. That fetches all notification settings.
* Update multi-tenant Alertmanager to run the generator before applying the configuration.

* Add notification settings labels to state calculation
* update the Multi-tenant Alertmanager to provide validation for notification settings

* update GET API so only admins can see auto-gen
This commit is contained in:
Yuri Tseretyan
2024-02-15 09:45:10 -05:00
committed by GitHub
parent ff916d9c15
commit 1eebd2a4de
60 changed files with 3466 additions and 304 deletions

View File

@@ -29,15 +29,6 @@ func Provision(ctx context.Context, cfg ProvisionerConfig) error {
}
logger.Info("starting to provision alerting")
logger.Debug("read all alerting files", "file_count", len(files))
ruleProvisioner := NewAlertRuleProvisioner(
logger,
cfg.DashboardService,
cfg.DashboardProvService,
cfg.RuleService)
err = ruleProvisioner.Provision(ctx, files)
if err != nil {
return fmt.Errorf("alert rules: %w", err)
}
cpProvisioner := NewContactPointProvisoner(logger, cfg.ContactPointService)
err = cpProvisioner.Provision(ctx, files)
if err != nil {
@@ -62,10 +53,6 @@ func Provision(ctx context.Context, cfg ProvisionerConfig) error {
if err != nil {
return fmt.Errorf("notification policies: %w", err)
}
err = cpProvisioner.Unprovision(ctx, files)
if err != nil {
return fmt.Errorf("contact points: %w", err)
}
err = mtProvisioner.Unprovision(ctx, files)
if err != nil {
return fmt.Errorf("mute times: %w", err)
@@ -74,6 +61,19 @@ func Provision(ctx context.Context, cfg ProvisionerConfig) error {
if err != nil {
return fmt.Errorf("text templates: %w", err)
}
ruleProvisioner := NewAlertRuleProvisioner(
logger,
cfg.DashboardService,
cfg.DashboardProvService,
cfg.RuleService)
err = ruleProvisioner.Provision(ctx, files)
if err != nil {
return fmt.Errorf("alert rules: %w", err)
}
err = cpProvisioner.Unprovision(ctx, files) // Unprovision contact points after rules to make sure all references in rules are updated
if err != nil {
return fmt.Errorf("contact points: %w", err)
}
logger.Info("finished to provision alerting")
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
"github.com/grafana/grafana/pkg/util"
)
type RuleDelete struct {
@@ -61,18 +62,19 @@ func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (models.AlertRuleGroupWithFold
}
type AlertRuleV1 struct {
UID values.StringValue `json:"uid" yaml:"uid"`
Title values.StringValue `json:"title" yaml:"title"`
Condition values.StringValue `json:"condition" yaml:"condition"`
Data []QueryV1 `json:"data" yaml:"data"`
DashboardUID values.StringValue `json:"dasboardUid" yaml:"dashboardUid"`
PanelID values.Int64Value `json:"panelId" yaml:"panelId"`
NoDataState values.StringValue `json:"noDataState" yaml:"noDataState"`
ExecErrState values.StringValue `json:"execErrState" yaml:"execErrState"`
For values.StringValue `json:"for" yaml:"for"`
Annotations values.StringMapValue `json:"annotations" yaml:"annotations"`
Labels values.StringMapValue `json:"labels" yaml:"labels"`
IsPaused values.BoolValue `json:"isPaused" yaml:"isPaused"`
UID values.StringValue `json:"uid" yaml:"uid"`
Title values.StringValue `json:"title" yaml:"title"`
Condition values.StringValue `json:"condition" yaml:"condition"`
Data []QueryV1 `json:"data" yaml:"data"`
DashboardUID values.StringValue `json:"dasboardUid" yaml:"dashboardUid"`
PanelID values.Int64Value `json:"panelId" yaml:"panelId"`
NoDataState values.StringValue `json:"noDataState" yaml:"noDataState"`
ExecErrState values.StringValue `json:"execErrState" yaml:"execErrState"`
For values.StringValue `json:"for" yaml:"for"`
Annotations values.StringMapValue `json:"annotations" yaml:"annotations"`
Labels values.StringMapValue `json:"labels" yaml:"labels"`
IsPaused values.BoolValue `json:"isPaused" yaml:"isPaused"`
NotificationSettings *NotificationSettingsV1 `json:"notification_settings" yaml:"notification_settings"`
}
func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) {
@@ -130,6 +132,13 @@ func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no data set", alertRule.Title)
}
alertRule.IsPaused = rule.IsPaused.Value()
if rule.NotificationSettings != nil {
ns, err := rule.NotificationSettings.mapToModel()
if err != nil {
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
}
alertRule.NotificationSettings = append(alertRule.NotificationSettings, ns)
}
return alertRule, nil
}
@@ -169,3 +178,71 @@ func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) {
Model: rawMessage,
}, nil
}
type NotificationSettingsV1 struct {
Receiver values.StringValue `json:"receiver" yaml:"receiver"`
GroupBy []values.StringValue `json:"group_by,omitempty" yaml:"group_by"`
GroupWait values.StringValue `json:"group_wait,omitempty" yaml:"group_wait"`
GroupInterval values.StringValue `json:"group_interval,omitempty" yaml:"group_interval"`
RepeatInterval values.StringValue `json:"repeat_interval,omitempty" yaml:"repeat_interval"`
MuteTimeIntervals []values.StringValue `json:"mute_time_intervals,omitempty" yaml:"mute_time_intervals"`
}
func (nsV1 *NotificationSettingsV1) mapToModel() (models.NotificationSettings, error) {
if nsV1.Receiver.Value() == "" {
return models.NotificationSettings{}, fmt.Errorf("receiver must not be empty")
}
var gw, gi, ri *model.Duration
if nsV1.GroupWait.Value() != "" {
dur, err := model.ParseDuration(nsV1.GroupWait.Value())
if err != nil {
return models.NotificationSettings{}, fmt.Errorf("failed to parse group wait: %w", err)
}
gw = util.Pointer(dur)
}
if nsV1.GroupInterval.Value() != "" {
dur, err := model.ParseDuration(nsV1.GroupInterval.Value())
if err != nil {
return models.NotificationSettings{}, fmt.Errorf("failed to parse group interval: %w", err)
}
gi = util.Pointer(dur)
}
if nsV1.RepeatInterval.Value() != "" {
dur, err := model.ParseDuration(nsV1.RepeatInterval.Value())
if err != nil {
return models.NotificationSettings{}, fmt.Errorf("failed to parse repeat interval: %w", err)
}
ri = util.Pointer(dur)
}
var groupBy []string
if len(nsV1.GroupBy) > 0 {
groupBy = make([]string, 0, len(nsV1.GroupBy))
for _, value := range nsV1.GroupBy {
if value.Value() == "" {
continue
}
groupBy = append(groupBy, value.Value())
}
}
var mute []string
if len(nsV1.MuteTimeIntervals) > 0 {
mute = make([]string, 0, len(nsV1.MuteTimeIntervals))
for _, value := range nsV1.MuteTimeIntervals {
if value.Value() == "" {
continue
}
mute = append(mute, value.Value())
}
}
return models.NotificationSettings{
Receiver: nsV1.Receiver.Value(),
GroupBy: groupBy,
GroupWait: gw,
GroupInterval: gi,
RepeatInterval: ri,
MuteTimeIntervals: mute,
}, nil
}

View File

@@ -4,11 +4,13 @@ import (
"testing"
"time"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
"github.com/grafana/grafana/pkg/util"
)
func TestRuleGroup(t *testing.T) {
@@ -187,6 +189,109 @@ func TestRules(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ruleMapped.NoDataState, models.NoData)
})
t.Run("a rule with notification settings should map it correctly", func(t *testing.T) {
rule := validRuleV1(t)
rule.NotificationSettings = &NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
}
ruleMapped, err := rule.mapToModel(1)
require.NoError(t, err)
require.Len(t, ruleMapped.NotificationSettings, 1)
require.Equal(t, models.NotificationSettings{Receiver: "test-receiver"}, ruleMapped.NotificationSettings[0])
})
}
func TestNotificationsSettingsV1MapToModel(t *testing.T) {
tests := []struct {
name string
input NotificationSettingsV1
expected models.NotificationSettings
wantErr bool
}{
{
name: "Valid Input",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
GroupBy: []values.StringValue{stringToStringValue("test-group_by")},
GroupWait: stringToStringValue("1s"),
GroupInterval: stringToStringValue("2s"),
RepeatInterval: stringToStringValue("3s"),
MuteTimeIntervals: []values.StringValue{stringToStringValue("test-mute")},
},
expected: models.NotificationSettings{
Receiver: "test-receiver",
GroupBy: []string{"test-group_by"},
GroupWait: util.Pointer(model.Duration(1 * time.Second)),
GroupInterval: util.Pointer(model.Duration(2 * time.Second)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Second)),
MuteTimeIntervals: []string{"test-mute"},
},
},
{
name: "Skips empty elements in group_by",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
GroupBy: []values.StringValue{stringToStringValue("test-group_by1"), stringToStringValue(""), stringToStringValue("test-group_by2")},
},
expected: models.NotificationSettings{
Receiver: "test-receiver",
GroupBy: []string{"test-group_by1", "test-group_by2"},
},
},
{
name: "Skips empty elements in mute timings",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
MuteTimeIntervals: []values.StringValue{stringToStringValue("test-mute1"), stringToStringValue(""), stringToStringValue("test-mute2")},
},
expected: models.NotificationSettings{
Receiver: "test-receiver",
MuteTimeIntervals: []string{"test-mute1", "test-mute2"},
},
},
{
name: "Empty Receiver",
input: NotificationSettingsV1{
Receiver: stringToStringValue(""),
},
wantErr: true,
},
{
name: "Invalid GroupWait Duration",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
GroupWait: stringToStringValue("invalidDuration"),
},
wantErr: true,
},
{
name: "Invalid GroupInterval Duration",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
GroupInterval: stringToStringValue("invalidDuration"),
},
wantErr: true,
},
{
name: "Invalid RepeatInterval Duration",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
GroupInterval: stringToStringValue("invalidDuration"),
},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.input.mapToModel()
if tc.wantErr {
require.Error(t, err)
return
}
require.Equal(t, tc.expected, got)
})
}
}
func validRuleGroupV1(t *testing.T) AlertRuleGroupV1 {
@@ -238,3 +343,12 @@ func validRuleV1(t *testing.T) AlertRuleV1 {
Data: []QueryV1{{}},
}
}
func stringToStringValue(s string) values.StringValue {
result := values.StringValue{}
err := yaml.Unmarshal([]byte(s), &result)
if err != nil {
panic(err)
}
return result
}