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:
Alexander Akhmetov
2025-05-12 22:07:02 +02:00
committed by GitHub
parent e965b85e19
commit c17b019ab1
12 changed files with 559 additions and 25 deletions
@@ -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) {
+30 -4
View File
@@ -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
}
+61 -4
View File
@@ -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"
}
}
+61 -4
View File
@@ -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,
+5
View File
@@ -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),
+60
View File
@@ -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
View File
@@ -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
View File
@@ -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"
},