Alerting: Support simplified routing receivers in Prometheus conversion API (#105135)
Adds ability to set notifications settings using the Prometheus conversion API.
The API now supports a new optional header: X-Grafana-Alerting-Notification-Settings which can be used to specify notification settings.
The value of the header is the AlertRuleNotificationSettings structure in JSON:
mimirtool rules load alerts.yaml --extra-headers 'X-Grafana-Alerting-Notification-Settings: {"receiver": "my-webhook", "group_by": ["cluster", "pod"]}'
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/validation"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/prom"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
@@ -41,6 +43,10 @@ const (
|
||||
// These headers control the paused state of newly created rules. By default, rules are not paused.
|
||||
recordingRulesPausedHeader = "X-Grafana-Alerting-Recording-Rules-Paused"
|
||||
alertRulesPausedHeader = "X-Grafana-Alerting-Alert-Rules-Paused"
|
||||
|
||||
// notificationSettingsHeader is the header that specifies the notification settings to be used for the rules.
|
||||
// The value should be a JSON-encoded AlertRuleNotificationSettings object.
|
||||
notificationSettingsHeader = "X-Grafana-Alerting-Notification-Settings"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,7 +55,7 @@ var (
|
||||
errutil.WithPublicMessage(fmt.Sprintf("Missing datasource UID header: %s", datasourceUIDHeader)),
|
||||
).Errorf("missing datasource UID header")
|
||||
|
||||
errInvalidHeaderValueMsg = "Invalid value for header {{.Public.Header}}: must be 'true' or 'false'"
|
||||
errInvalidHeaderValueMsg = "Invalid value for header {{.Public.Header}}: {{.Public.Error}}"
|
||||
errInvalidHeaderValueBase = errutil.ValidationFailed("alerting.invalidHeaderValue").MustTemplate(errInvalidHeaderValueMsg, errutil.WithPublic(errInvalidHeaderValueMsg))
|
||||
|
||||
errRecordingRulesNotEnabled = errutil.ValidationFailed(
|
||||
@@ -63,8 +69,8 @@ var (
|
||||
).Errorf("recording rules target datasources configuration not enabled")
|
||||
)
|
||||
|
||||
func errInvalidHeaderValue(header string) error {
|
||||
return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header}})
|
||||
func errInvalidHeaderValue(header string, err error) error {
|
||||
return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header, "Error": err}})
|
||||
}
|
||||
|
||||
// ConvertPrometheusSrv converts Prometheus rules to Grafana rules
|
||||
@@ -369,6 +375,12 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
|
||||
// to ensure we can return them in this API in Prometheus format.
|
||||
keepOriginalRuleDefinition := provenance == models.ProvenanceConvertedPrometheus
|
||||
|
||||
notificationSettings, err := parseNotificationSettingsHeader(c)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse notification settings header", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
// 2. Convert Prometheus Rules to GMA
|
||||
grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces))
|
||||
for ns, rgs := range promNamespaces {
|
||||
@@ -394,7 +406,18 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
|
||||
}
|
||||
}
|
||||
|
||||
grafanaGroup, err := srv.convertToGrafanaRuleGroup(c, ds, tds, namespace.UID, rg, pauseRecordingRules, pauseAlertRules, keepOriginalRuleDefinition, logger)
|
||||
grafanaGroup, err := srv.convertToGrafanaRuleGroup(
|
||||
c,
|
||||
ds,
|
||||
tds,
|
||||
namespace.UID,
|
||||
rg,
|
||||
pauseRecordingRules,
|
||||
pauseAlertRules,
|
||||
keepOriginalRuleDefinition,
|
||||
notificationSettings,
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
|
||||
return errorToResponse(err)
|
||||
@@ -442,6 +465,7 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
||||
pauseRecordingRules bool,
|
||||
pauseAlertRules bool,
|
||||
keepOriginalRuleDefinition bool,
|
||||
notificationSettings []models.NotificationSettings,
|
||||
logger log.Logger,
|
||||
) (*models.AlertRuleGroup, error) {
|
||||
logger.Info("Converting Prometheus rules to Grafana rules", "rules", len(promGroup.Rules), "folder_uid", namespaceUID, "datasource_uid", ds.UID, "datasource_type", ds.Type)
|
||||
@@ -479,6 +503,7 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
||||
},
|
||||
KeepOriginalRuleDefinition: util.Pointer(keepOriginalRuleDefinition),
|
||||
EvaluationOffset: &srv.cfg.PrometheusConversion.RuleQueryOffset,
|
||||
NotificationSettings: notificationSettings,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -503,7 +528,7 @@ func parseBooleanHeader(header string, headerName string) (bool, error) {
|
||||
}
|
||||
val, err := strconv.ParseBool(header)
|
||||
if err != nil {
|
||||
return false, errInvalidHeaderValue(headerName)
|
||||
return false, errInvalidHeaderValue(headerName, errors.New("must be 'true' or 'false'"))
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -597,3 +622,23 @@ func getProvenance(ctx *contextmodel.ReqContext) models.Provenance {
|
||||
}
|
||||
return models.ProvenanceConvertedPrometheus
|
||||
}
|
||||
|
||||
func parseNotificationSettingsHeader(ctx *contextmodel.ReqContext) ([]models.NotificationSettings, error) {
|
||||
var notificationSettings []models.NotificationSettings
|
||||
notificationSettingsJSON := ctx.Req.Header.Get(notificationSettingsHeader)
|
||||
|
||||
if notificationSettingsJSON != "" {
|
||||
var settings apimodels.AlertRuleNotificationSettings
|
||||
var err error
|
||||
|
||||
if err := json.Unmarshal([]byte(notificationSettingsJSON), &settings); err != nil {
|
||||
return nil, errInvalidHeaderValue(notificationSettingsHeader, errors.New("invalid JSON"))
|
||||
}
|
||||
notificationSettings, err = validation.ValidateNotificationSettings(&settings)
|
||||
if err != nil {
|
||||
return nil, errInvalidHeaderValue(notificationSettingsHeader, err)
|
||||
}
|
||||
}
|
||||
|
||||
return notificationSettings, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -425,6 +426,97 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
require.NotNil(t, remaining[0].Record)
|
||||
require.Equal(t, targetDSUID, remaining[0].Record.TargetDatasourceUID)
|
||||
})
|
||||
|
||||
t.Run("sets notification settings for rules if specified", func(t *testing.T) {
|
||||
srv, _, ruleStore, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
receiver := "test-receiver"
|
||||
groupBy := []string{"cluster", "pod"}
|
||||
settings := apimodels.AlertRuleNotificationSettings{
|
||||
Receiver: receiver,
|
||||
GroupBy: groupBy,
|
||||
}
|
||||
settingsJSON, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
rc.Req.Header.Set(notificationSettingsHeader, string(settingsJSON))
|
||||
|
||||
simpleGroup := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "TestAlert",
|
||||
Expr: "up == 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
|
||||
createdRules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
||||
OrgID: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, createdRules, 1)
|
||||
require.Len(t, createdRules[0].NotificationSettings, 1)
|
||||
require.Equal(t, receiver, createdRules[0].NotificationSettings[0].Receiver)
|
||||
require.Equal(t, groupBy, createdRules[0].NotificationSettings[0].GroupBy)
|
||||
})
|
||||
|
||||
t.Run("returns error when notification settings header contains invalid JSON", func(t *testing.T) {
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
rc.Req.Header.Set(notificationSettingsHeader, "{invalid json")
|
||||
|
||||
simpleGroup := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "TestAlert",
|
||||
Expr: "up == 0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusBadRequest, response.Status())
|
||||
require.Contains(t, string(response.Body()), "Invalid value for header X-Grafana-Alerting-Notification-Settings")
|
||||
})
|
||||
|
||||
t.Run("returns error when notification settings contain invalid values", func(t *testing.T) {
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
settings := apimodels.AlertRuleNotificationSettings{
|
||||
Receiver: "", // empty receiver is invalid
|
||||
}
|
||||
settingsJSON, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
rc.Req.Header.Set(notificationSettingsHeader, string(settingsJSON))
|
||||
|
||||
simpleGroup := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "TestAlert",
|
||||
Expr: "up == 0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusBadRequest, response.Status())
|
||||
require.Contains(t, string(response.Body()), "Invalid value for header X-Grafana-Alerting-Notification-Settings")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
|
||||
|
||||
@@ -314,6 +314,16 @@
|
||||
},
|
||||
"AlertRuleNotificationSettings": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"description": "Override the times when notifications should not be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent\nat the time that matches any interval.",
|
||||
"example": [
|
||||
"maintenance"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_by": {
|
||||
"default": [
|
||||
"alertname",
|
||||
@@ -341,7 +351,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"example": [
|
||||
"maintenance"
|
||||
],
|
||||
@@ -368,6 +378,12 @@
|
||||
},
|
||||
"AlertRuleNotificationSettingsExport": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_by": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -2325,6 +2341,12 @@
|
||||
},
|
||||
"NotificationPolicyExport": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -3710,6 +3732,12 @@
|
||||
"RouteExport": {
|
||||
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -4889,7 +4917,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup",
|
||||
"type": "object"
|
||||
@@ -5176,7 +5203,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence",
|
||||
"type": "object"
|
||||
@@ -6661,4 +6687,4 @@
|
||||
}
|
||||
},
|
||||
"swagger": "2.0"
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,12 @@ type RouteConvertPrometheusPostRuleGroupParams struct {
|
||||
RecordingRulesPaused bool `json:"x-grafana-alerting-recording-rules-paused"`
|
||||
// in: header
|
||||
AlertRulesPaused bool `json:"x-grafana-alerting-alert-rules-paused"`
|
||||
// in: header
|
||||
TargetDatasourceUID string `json:"x-grafana-alerting-target-datasource-uid"`
|
||||
// in: header
|
||||
FolderUID string `json:"x-grafana-alerting-folder-uid"`
|
||||
// in: header
|
||||
NotificationReceiver string `json:"x-grafana-alerting-notification-receiver"`
|
||||
// in:body
|
||||
Body PrometheusRuleGroup
|
||||
}
|
||||
|
||||
@@ -314,6 +314,16 @@
|
||||
},
|
||||
"AlertRuleNotificationSettings": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"description": "Override the times when notifications should not be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent\nat the time that matches any interval.",
|
||||
"example": [
|
||||
"maintenance"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_by": {
|
||||
"default": [
|
||||
"alertname",
|
||||
@@ -341,7 +351,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"example": [
|
||||
"maintenance"
|
||||
],
|
||||
@@ -368,6 +378,12 @@
|
||||
},
|
||||
"AlertRuleNotificationSettingsExport": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_by": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -2325,6 +2341,12 @@
|
||||
},
|
||||
"NotificationPolicyExport": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -3710,6 +3732,12 @@
|
||||
"RouteExport": {
|
||||
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -4888,7 +4916,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup",
|
||||
"type": "object"
|
||||
@@ -5050,7 +5077,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert",
|
||||
"type": "object"
|
||||
@@ -5175,6 +5201,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence",
|
||||
"type": "object"
|
||||
@@ -6644,6 +6671,21 @@
|
||||
"name": "x-grafana-alerting-alert-rules-paused",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-grafana-alerting-target-datasource-uid",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-grafana-alerting-folder-uid",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-grafana-alerting-notification-receiver",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
@@ -6919,6 +6961,21 @@
|
||||
"name": "x-grafana-alerting-alert-rules-paused",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-grafana-alerting-target-datasource-uid",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-grafana-alerting-folder-uid",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-grafana-alerting-notification-receiver",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
@@ -9533,4 +9590,4 @@
|
||||
}
|
||||
},
|
||||
"swagger": "2.0"
|
||||
}
|
||||
}
|
||||
@@ -1204,6 +1204,21 @@
|
||||
"name": "x-grafana-alerting-alert-rules-paused",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-grafana-alerting-target-datasource-uid",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-grafana-alerting-folder-uid",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-grafana-alerting-notification-receiver",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
@@ -1479,6 +1494,21 @@
|
||||
"name": "x-grafana-alerting-alert-rules-paused",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-grafana-alerting-target-datasource-uid",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-grafana-alerting-folder-uid",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-grafana-alerting-notification-receiver",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
@@ -4416,6 +4446,16 @@
|
||||
"receiver"
|
||||
],
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"description": "Override the times when notifications should not be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent\nat the time that matches any interval.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
"group_by": {
|
||||
"description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.",
|
||||
"type": "array",
|
||||
@@ -4443,7 +4483,7 @@
|
||||
"example": "30s"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -4468,6 +4508,12 @@
|
||||
"type": "object",
|
||||
"title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"group_by": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -6427,6 +6473,12 @@
|
||||
"type": "object",
|
||||
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -7811,6 +7863,12 @@
|
||||
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -8988,7 +9046,6 @@
|
||||
}
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -9150,7 +9207,6 @@
|
||||
}
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -9275,6 +9331,7 @@
|
||||
}
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -9566,4 +9623,4 @@
|
||||
"type": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func validateAlertingRuleFields(in *apimodels.PostableExtendedRuleNode, newRule
|
||||
}
|
||||
|
||||
if in.GrafanaManagedAlert.NotificationSettings != nil {
|
||||
newRule.NotificationSettings, err = validateNotificationSettings(in.GrafanaManagedAlert.NotificationSettings)
|
||||
newRule.NotificationSettings, err = ValidateNotificationSettings(in.GrafanaManagedAlert.NotificationSettings)
|
||||
if err != nil {
|
||||
return ngmodels.AlertRule{}, err
|
||||
}
|
||||
@@ -391,7 +391,7 @@ func ValidateRuleGroup(
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func validateNotificationSettings(n *apimodels.AlertRuleNotificationSettings) ([]ngmodels.NotificationSettings, error) {
|
||||
func ValidateNotificationSettings(n *apimodels.AlertRuleNotificationSettings) ([]ngmodels.NotificationSettings, error) {
|
||||
s := ngmodels.NotificationSettings{
|
||||
Receiver: n.Receiver,
|
||||
GroupBy: n.GroupBy,
|
||||
|
||||
@@ -58,6 +58,7 @@ type Config struct {
|
||||
KeepOriginalRuleDefinition *bool
|
||||
RecordingRules RulesConfig
|
||||
AlertRules RulesConfig
|
||||
NotificationSettings []models.NotificationSettings
|
||||
}
|
||||
|
||||
// RulesConfig contains configuration that applies to either recording or alerting rules.
|
||||
@@ -266,6 +267,10 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom
|
||||
MissingSeriesEvalsToResolve: util.Pointer(1),
|
||||
}
|
||||
|
||||
if !isRecordingRule {
|
||||
result.NotificationSettings = p.cfg.NotificationSettings
|
||||
}
|
||||
|
||||
if p.cfg.KeepOriginalRuleDefinition != nil && *p.cfg.KeepOriginalRuleDefinition {
|
||||
result.Metadata.PrometheusStyleRule = &models.PrometheusStyleRule{
|
||||
OriginalRuleDefinition: string(originalRuleDefinition),
|
||||
|
||||
@@ -811,6 +811,66 @@ func TestPrometheusRulesToGrafana_KeepOriginalRuleDefinition(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrometheusRulesToGrafana_NotificationSettings(t *testing.T) {
|
||||
orgID := int64(1)
|
||||
namespace := "namespace"
|
||||
|
||||
promGroup := PrometheusRuleGroup{
|
||||
Name: "test-group",
|
||||
Rules: []PrometheusRule{
|
||||
{
|
||||
Alert: "test-alert",
|
||||
Expr: "up == 0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
notificationSettings []models.NotificationSettings
|
||||
}{
|
||||
{
|
||||
name: "with notification settings specified",
|
||||
notificationSettings: []models.NotificationSettings{
|
||||
{
|
||||
Receiver: "test-receiver",
|
||||
GroupBy: []string{"alertname", "instance"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "without notification settings",
|
||||
notificationSettings: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 1 * time.Minute,
|
||||
NotificationSettings: tc.notificationSettings,
|
||||
}
|
||||
|
||||
converter, err := NewConverter(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, grafanaGroup.Rules, 1)
|
||||
|
||||
if tc.notificationSettings != nil {
|
||||
require.NotNil(t, grafanaGroup.Rules[0].NotificationSettings)
|
||||
require.Len(t, grafanaGroup.Rules[0].NotificationSettings, len(tc.notificationSettings))
|
||||
require.Equal(t, tc.notificationSettings, grafanaGroup.Rules[0].NotificationSettings)
|
||||
} else {
|
||||
require.Nil(t, grafanaGroup.Rules[0].NotificationSettings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryModelContainsRequiredParameters(t *testing.T) {
|
||||
cfg := Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
prommodel "github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationSettingsHeader = "X-Grafana-Alerting-Notification-Settings"
|
||||
)
|
||||
|
||||
func TestIntegrationConvertPrometheusNotificationSettings(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
// Setup Grafana and its Database
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{"grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"},
|
||||
EnableRecordingRules: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
Password: "admin",
|
||||
Login: "admin",
|
||||
})
|
||||
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
|
||||
ds := adminClient.CreateDatasource(t, "prometheus")
|
||||
|
||||
namespace := "test-notification-settings"
|
||||
namespaceUID := util.GenerateShortUID()
|
||||
adminClient.CreateFolder(t, namespaceUID, namespace)
|
||||
|
||||
alertRuleGroup := apimodels.PrometheusRuleGroup{
|
||||
Name: "test-group-notification-settings",
|
||||
Interval: prommodel.Duration(60 * time.Second),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "TestAlert",
|
||||
Expr: "vector(1) > 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "Test alert with notification settings",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("rules should use notification settings from header", func(t *testing.T) {
|
||||
receiver := "test-receiver"
|
||||
|
||||
receiverSettings, err := simplejson.NewJson([]byte(`{
|
||||
"url":"https://localhost/webhook"
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
adminClient.EnsureReceiver(t,
|
||||
apimodels.EmbeddedContactPoint{
|
||||
Name: receiver,
|
||||
Type: "webhook",
|
||||
Settings: receiverSettings,
|
||||
},
|
||||
)
|
||||
|
||||
groupBy := []string{"alertname", "instance", "job"}
|
||||
settings := apimodels.AlertRuleNotificationSettings{
|
||||
Receiver: receiver,
|
||||
GroupBy: groupBy,
|
||||
}
|
||||
settingsJSON, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
headers := map[string]string{
|
||||
notificationSettingsHeader: string(settingsJSON),
|
||||
}
|
||||
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, alertRuleGroup, headers)
|
||||
|
||||
group, _, _ := adminClient.GetRulesGroupWithStatus(t, namespaceUID, alertRuleGroup.Name)
|
||||
|
||||
require.Len(t, group.Rules, 1)
|
||||
rule := group.Rules[0]
|
||||
|
||||
require.NotNil(t, rule.GrafanaManagedAlert.NotificationSettings)
|
||||
require.Equal(t, receiver, rule.GrafanaManagedAlert.NotificationSettings.Receiver)
|
||||
require.Equal(t, groupBy, rule.GrafanaManagedAlert.NotificationSettings.GroupBy)
|
||||
})
|
||||
|
||||
t.Run("invalid JSON in notification settings header should return error", func(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
notificationSettingsHeader: "{invalid json",
|
||||
}
|
||||
|
||||
_, status, body := adminClient.RawConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, alertRuleGroup, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, body)
|
||||
require.Contains(t, body, "Invalid value for header X-Grafana-Alerting-Notification-Settings")
|
||||
})
|
||||
|
||||
t.Run("empty receiver in notification settings should return error", func(t *testing.T) {
|
||||
settings := apimodels.AlertRuleNotificationSettings{
|
||||
Receiver: "",
|
||||
GroupBy: []string{"alertname"},
|
||||
}
|
||||
settingsJSON, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
headers := map[string]string{
|
||||
notificationSettingsHeader: string(settingsJSON),
|
||||
}
|
||||
|
||||
_, status, body := adminClient.RawConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, alertRuleGroup, headers)
|
||||
requireStatusCode(t, http.StatusBadRequest, status, body)
|
||||
require.Contains(t, body, "Invalid value for header X-Grafana-Alerting-Notification-Settings")
|
||||
})
|
||||
}
|
||||
+29
-3
@@ -12760,6 +12760,16 @@
|
||||
"receiver"
|
||||
],
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"description": "Override the times when notifications should not be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent\nat the time that matches any interval.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
"group_by": {
|
||||
"description": "Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for\ncluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels\nuse the special value '...' as the sole label name.\nThis effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what\nyou want, unless you have a very low alert volume or your upstream notification system performs its own grouping.\nMust include 'alertname' and 'grafana_folder' if not using '...'.",
|
||||
"type": "array",
|
||||
@@ -12787,7 +12797,7 @@
|
||||
"example": "30s"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -12812,6 +12822,12 @@
|
||||
"type": "object",
|
||||
"title": "AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"group_by": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -17539,6 +17555,12 @@
|
||||
"type": "object",
|
||||
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -20120,6 +20142,12 @@
|
||||
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -22864,7 +22892,6 @@
|
||||
}
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -23194,7 +23221,6 @@
|
||||
}
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
+29
-3
@@ -2785,6 +2785,16 @@
|
||||
},
|
||||
"AlertRuleNotificationSettings": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"description": "Override the times when notifications should not be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent\nat the time that matches any interval.",
|
||||
"example": [
|
||||
"maintenance"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_by": {
|
||||
"default": [
|
||||
"alertname",
|
||||
@@ -2812,7 +2822,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"mute_time_intervals": {
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"description": "Override the times when notifications should be muted. These must match the name of a mute time interval defined\nin the alertmanager configuration time_intervals section. When muted it will not send any notifications, but\notherwise acts normally.",
|
||||
"example": [
|
||||
"maintenance"
|
||||
],
|
||||
@@ -2839,6 +2849,12 @@
|
||||
},
|
||||
"AlertRuleNotificationSettingsExport": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"group_by": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -7566,6 +7582,12 @@
|
||||
},
|
||||
"NotificationPolicyExport": {
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -10148,6 +10170,12 @@
|
||||
"RouteExport": {
|
||||
"description": "RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in\nprovisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.",
|
||||
"properties": {
|
||||
"active_time_intervals": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"continue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -12892,7 +12920,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/alertGroup"
|
||||
},
|
||||
@@ -13220,7 +13247,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/gettableSilence"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user