mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user