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:
@@ -57,7 +57,7 @@ type API struct {
|
||||
TransactionManager provisioning.TransactionManager
|
||||
ProvenanceStore provisioning.ProvisioningStore
|
||||
RuleStore RuleStore
|
||||
AlertingStore AlertingStore
|
||||
AlertingStore store.AlertingStore
|
||||
AdminConfigStore store.AdminConfigurationStore
|
||||
DataProxy *datasourceproxy.DataSourceProxyService
|
||||
MultiOrgAlertmanager *notifier.MultiOrgAlertmanager
|
||||
@@ -115,6 +115,9 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
log: logger,
|
||||
cfg: &api.Cfg.UnifiedAlerting,
|
||||
authz: ruleAuthzService,
|
||||
amConfigStore: api.AlertingStore,
|
||||
amRefresher: api.MultiOrgAlertmanager,
|
||||
featureManager: api.FeatureManager,
|
||||
},
|
||||
), m)
|
||||
api.RegisterTestingApiEndpoints(NewTestingApi(
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -124,7 +125,8 @@ func (srv AlertmanagerSrv) RouteDeleteSilence(c *contextmodel.ReqContext, silenc
|
||||
}
|
||||
|
||||
func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *contextmodel.ReqContext) response.Response {
|
||||
config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID())
|
||||
canSeeAutogen := c.SignedInUser.HasRole(org.RoleAdmin)
|
||||
config, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), canSeeAutogen)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||
return ErrResp(http.StatusNotFound, err, "")
|
||||
@@ -264,7 +266,10 @@ func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *cont
|
||||
}
|
||||
|
||||
func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, body apimodels.PostableUserConfig) response.Response {
|
||||
currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID())
|
||||
// Remove autogenerated config from the user config before checking provenance guard and eventually saving it.
|
||||
// TODO: This and provenance guard should be moved to the notifier package.
|
||||
notifier.RemoveAutogenConfigIfExists(body.AlertmanagerConfig.Route)
|
||||
currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), false)
|
||||
// If a config is present and valid we proceed with the guard, otherwise we
|
||||
// just bypass the guard which is okay as we are anyway in an invalid state.
|
||||
if err == nil {
|
||||
@@ -272,7 +277,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, b
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
}
|
||||
err = srv.mam.ApplyAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), body)
|
||||
err = srv.mam.SaveAndApplyAlertmanagerConfiguration(c.Req.Context(), c.SignedInUser.GetOrgID(), body)
|
||||
if err == nil {
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"})
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -20,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
@@ -325,6 +329,84 @@ func TestAlertmanagerConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertmanagerAutogenConfig(t *testing.T) {
|
||||
createSutForAutogen := func(t *testing.T) (AlertmanagerSrv, map[int64]*ngmodels.AlertConfiguration) {
|
||||
sut := createSut(t)
|
||||
configs := map[int64]*ngmodels.AlertConfiguration{
|
||||
1: {AlertmanagerConfiguration: validConfig, OrgID: 1},
|
||||
2: {AlertmanagerConfiguration: validConfigWithoutAutogen, OrgID: 2},
|
||||
}
|
||||
sut.mam = createMultiOrgAlertmanager(t, configs)
|
||||
return sut, configs
|
||||
}
|
||||
|
||||
compare := func(t *testing.T, expectedAm string, testAm string) {
|
||||
test, err := notifier.Load([]byte(testAm))
|
||||
require.NoError(t, err)
|
||||
|
||||
exp, err := notifier.Load([]byte(expectedAm))
|
||||
require.NoError(t, err)
|
||||
|
||||
cOpt := []cmp.Option{
|
||||
cmpopts.IgnoreUnexported(apimodels.PostableUserConfig{}, apimodels.Route{}, labels.Matcher{}),
|
||||
cmpopts.IgnoreFields(apimodels.PostableGrafanaReceiver{}, "UID", "Settings"),
|
||||
}
|
||||
if !cmp.Equal(test, exp, cOpt...) {
|
||||
t.Errorf("Unexpected AM Config: %v", cmp.Diff(test, exp, cOpt...))
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("route POST config", func(t *testing.T) {
|
||||
t.Run("does not save autogen routes", func(t *testing.T) {
|
||||
sut, configs := createSutForAutogen(t)
|
||||
rc := createRequestCtxInOrg(1)
|
||||
request := createAmConfigRequest(t, validConfigWithAutogen)
|
||||
response := sut.RoutePostAlertingConfig(rc, request)
|
||||
require.Equal(t, 202, response.Status())
|
||||
|
||||
compare(t, validConfigWithoutAutogen, configs[1].AlertmanagerConfiguration)
|
||||
})
|
||||
|
||||
t.Run("provenance guard ignores autogen routes", func(t *testing.T) {
|
||||
sut := createSut(t)
|
||||
rc := createRequestCtxInOrg(1)
|
||||
request := createAmConfigRequest(t, validConfigWithoutAutogen)
|
||||
_ = sut.RoutePostAlertingConfig(rc, request)
|
||||
|
||||
setRouteProvenance(t, 1, sut.mam.ProvStore)
|
||||
request = createAmConfigRequest(t, validConfigWithAutogen)
|
||||
request.AlertmanagerConfig.Route.Provenance = apimodels.Provenance(ngmodels.ProvenanceAPI)
|
||||
response := sut.RoutePostAlertingConfig(rc, request)
|
||||
require.Equal(t, 202, response.Status())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("route GET config", func(t *testing.T) {
|
||||
t.Run("when admin return autogen routes", func(t *testing.T) {
|
||||
sut, _ := createSutForAutogen(t)
|
||||
|
||||
rc := createRequestCtxInOrg(2)
|
||||
rc.SignedInUser.OrgRole = org.RoleAdmin
|
||||
|
||||
response := sut.RouteGetAlertingConfig(rc)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
compare(t, validConfigWithAutogen, string(response.Body()))
|
||||
})
|
||||
|
||||
t.Run("when not admin return no autogen routes", func(t *testing.T) {
|
||||
sut, _ := createSutForAutogen(t)
|
||||
|
||||
rc := createRequestCtxInOrg(2)
|
||||
|
||||
response := sut.RouteGetAlertingConfig(rc)
|
||||
require.Equal(t, 200, response.Status())
|
||||
|
||||
compare(t, validConfigWithoutAutogen, string(response.Body()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteGetAlertingConfigHistory(t *testing.T) {
|
||||
sut := createSut(t)
|
||||
|
||||
@@ -633,7 +715,12 @@ func TestRouteCreateSilence(t *testing.T) {
|
||||
func createSut(t *testing.T) AlertmanagerSrv {
|
||||
t.Helper()
|
||||
|
||||
mam := createMultiOrgAlertmanager(t)
|
||||
configs := map[int64]*ngmodels.AlertConfiguration{
|
||||
1: {AlertmanagerConfiguration: validConfig, OrgID: 1},
|
||||
2: {AlertmanagerConfiguration: validConfig, OrgID: 2},
|
||||
3: {AlertmanagerConfiguration: brokenConfig, OrgID: 3},
|
||||
}
|
||||
mam := createMultiOrgAlertmanager(t, configs)
|
||||
log := log.NewNopLogger()
|
||||
return AlertmanagerSrv{
|
||||
mam: mam,
|
||||
@@ -653,14 +740,9 @@ func createAmConfigRequest(t *testing.T, config string) apimodels.PostableUserCo
|
||||
return request
|
||||
}
|
||||
|
||||
func createMultiOrgAlertmanager(t *testing.T) *notifier.MultiOrgAlertmanager {
|
||||
func createMultiOrgAlertmanager(t *testing.T, configs map[int64]*ngmodels.AlertConfiguration) *notifier.MultiOrgAlertmanager {
|
||||
t.Helper()
|
||||
|
||||
configs := map[int64]*ngmodels.AlertConfiguration{
|
||||
1: {AlertmanagerConfiguration: validConfig, OrgID: 1},
|
||||
2: {AlertmanagerConfiguration: validConfig, OrgID: 2},
|
||||
3: {AlertmanagerConfiguration: brokenConfig, OrgID: 3},
|
||||
}
|
||||
configStore := notifier.NewFakeConfigStore(t, configs)
|
||||
orgStore := notifier.NewFakeOrgStore(t, []int64{1, 2, 3})
|
||||
provStore := ngfakes.NewFakeProvisioningStore()
|
||||
@@ -679,7 +761,7 @@ func createMultiOrgAlertmanager(t *testing.T) *notifier.MultiOrgAlertmanager {
|
||||
}, // do not poll in tests.
|
||||
}
|
||||
|
||||
mam, err := notifier.NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService)
|
||||
mam, err := notifier.NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService, featuremgmt.WithManager(featuremgmt.FlagAlertingSimplifiedRouting))
|
||||
require.NoError(t, err)
|
||||
err = mam.LoadAndSyncAlertmanagersForOrgs(context.Background())
|
||||
require.NoError(t, err)
|
||||
@@ -710,6 +792,90 @@ var validConfig = `{
|
||||
}
|
||||
`
|
||||
|
||||
var validConfigWithoutAutogen = `{
|
||||
"template_files": {
|
||||
"a": "template"
|
||||
},
|
||||
"alertmanager_config": {
|
||||
"route": {
|
||||
"receiver": "some email",
|
||||
"routes": [{
|
||||
"receiver": "other email",
|
||||
"object_matchers": [["a", "=", "b"]]
|
||||
}]
|
||||
},
|
||||
"receivers": [{
|
||||
"name": "some email",
|
||||
"grafana_managed_receiver_configs": [{
|
||||
"name": "some email",
|
||||
"type": "email",
|
||||
"settings": {
|
||||
"addresses": "<some@email.com>"
|
||||
}
|
||||
}]
|
||||
},{
|
||||
"name": "other email",
|
||||
"grafana_managed_receiver_configs": [{
|
||||
"name": "other email",
|
||||
"type": "email",
|
||||
"settings": {
|
||||
"addresses": "<other@email.com>"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var validConfigWithAutogen = `{
|
||||
"template_files": {
|
||||
"a": "template"
|
||||
},
|
||||
"alertmanager_config": {
|
||||
"route": {
|
||||
"receiver": "some email",
|
||||
"routes": [{
|
||||
"receiver": "some email",
|
||||
"object_matchers": [["__grafana_autogenerated__", "=", "true"]],
|
||||
"routes": [{
|
||||
"receiver": "some email",
|
||||
"group_by": ["grafana_folder", "alertname"],
|
||||
"object_matchers": [["__grafana_receiver__", "=", "some email"]],
|
||||
"continue": false
|
||||
},{
|
||||
"receiver": "other email",
|
||||
"group_by": ["grafana_folder", "alertname"],
|
||||
"object_matchers": [["__grafana_receiver__", "=", "other email"]],
|
||||
"continue": false
|
||||
}]
|
||||
},{
|
||||
"receiver": "other email",
|
||||
"object_matchers": [["a", "=", "b"]]
|
||||
}]
|
||||
},
|
||||
"receivers": [{
|
||||
"name": "some email",
|
||||
"grafana_managed_receiver_configs": [{
|
||||
"name": "some email",
|
||||
"type": "email",
|
||||
"settings": {
|
||||
"addresses": "<some@email.com>"
|
||||
}
|
||||
}]
|
||||
},{
|
||||
"name": "other email",
|
||||
"grafana_managed_receiver_configs": [{
|
||||
"name": "other email",
|
||||
"type": "email",
|
||||
"settings": {
|
||||
"addresses": "<other@email.com>"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var validConfigWithSecureSetting = `{
|
||||
"template_files": {
|
||||
"a": "template"
|
||||
|
||||
@@ -525,17 +525,14 @@ func TestProvisioningApi(t *testing.T) {
|
||||
t.Run("yaml body content is the default", func(t *testing.T) {
|
||||
sut := createProvisioningSrvSut(t)
|
||||
rc := createTestRequestCtx()
|
||||
insertRule(t, sut, createTestAlertRule("rule1", 1))
|
||||
rule1 := createTestAlertRule("rule1", 1)
|
||||
rule1.NotificationSettings = nil
|
||||
insertRule(t, sut, rule1)
|
||||
insertRule(t, sut, createTestAlertRule("rule2", 1))
|
||||
|
||||
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder" +
|
||||
": Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n" +
|
||||
" condition: A\n data:\n - refId: A\n datasourceUid" +
|
||||
": \"\"\n model:\n conditions:\n - evaluator:\n" +
|
||||
" params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n"
|
||||
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
|
||||
|
||||
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
})
|
||||
@@ -543,14 +540,15 @@ func TestProvisioningApi(t *testing.T) {
|
||||
t.Run("json body content is as expected", func(t *testing.T) {
|
||||
sut := createProvisioningSrvSut(t)
|
||||
rc := createTestRequestCtx()
|
||||
insertRule(t, sut, createTestAlertRule("rule1", 1))
|
||||
rule1 := createTestAlertRule("rule1", 1)
|
||||
rule1.NotificationSettings = nil
|
||||
insertRule(t, sut, rule1)
|
||||
insertRule(t, sut, createTestAlertRule("rule2", 1))
|
||||
|
||||
rc.Context.Req.Header.Add("Accept", "application/json")
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
})
|
||||
@@ -566,7 +564,7 @@ func TestProvisioningApi(t *testing.T) {
|
||||
": Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n" +
|
||||
" condition: A\n data:\n - refId: A\n datasourceUid" +
|
||||
": \"\"\n model:\n conditions:\n - evaluator:\n" +
|
||||
" params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n"
|
||||
" params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
|
||||
|
||||
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
|
||||
|
||||
@@ -585,6 +583,7 @@ func TestProvisioningApi(t *testing.T) {
|
||||
}
|
||||
rule1.NoDataState = definitions.Alerting
|
||||
rule1.ExecErrState = definitions.ErrorErrState
|
||||
rule1.NotificationSettings = nil
|
||||
insertRule(t, sut, rule1)
|
||||
insertRule(t, sut, createTestAlertRule("rule2", 1))
|
||||
|
||||
@@ -641,6 +640,15 @@ func TestProvisioningApi(t *testing.T) {
|
||||
exec_err_state = "OK"
|
||||
for = "0s"
|
||||
is_paused = false
|
||||
|
||||
notification_settings {
|
||||
receiver = "Test-Receiver"
|
||||
group_by = ["alertname", "grafana_folder", "test"]
|
||||
group_wait = "1s"
|
||||
group_interval = "5s"
|
||||
repeat_interval = "5m"
|
||||
mute_time_intervals = ["test-mute"]
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -774,7 +782,7 @@ func TestProvisioningApi(t *testing.T) {
|
||||
rc := createTestRequestCtx()
|
||||
insertRule(t, sut, createTestAlertRule("rule1", 1))
|
||||
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
rc.Context.Req.Header.Add("Accept", "application/json")
|
||||
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
|
||||
@@ -789,7 +797,7 @@ func TestProvisioningApi(t *testing.T) {
|
||||
insertRule(t, sut, createTestAlertRule("rule1", 1))
|
||||
|
||||
rc.Context.Req.Header.Add("Accept", "application/yaml")
|
||||
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n"
|
||||
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
|
||||
|
||||
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
|
||||
|
||||
@@ -889,15 +897,19 @@ func TestProvisioningApi(t *testing.T) {
|
||||
t.Run("json body content is as expected", func(t *testing.T) {
|
||||
sut := createProvisioningSrvSut(t)
|
||||
rc := createTestRequestCtx()
|
||||
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa"))
|
||||
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb"))
|
||||
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb"))
|
||||
rule1 := createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa")
|
||||
rule1.NotificationSettings = nil
|
||||
rule2 := createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")
|
||||
rule1.NotificationSettings = &definitions.AlertRuleNotificationSettings{Receiver: "Email"}
|
||||
rule3 := createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")
|
||||
insertRule(t, sut, rule1)
|
||||
insertRule(t, sut, rule2)
|
||||
insertRule(t, sut, rule3)
|
||||
|
||||
rc.Context.Req.Header.Add("Accept", "application/json")
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Email"}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
response := sut.RouteGetAlertRulesExport(&rc)
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
})
|
||||
@@ -905,15 +917,19 @@ func TestProvisioningApi(t *testing.T) {
|
||||
t.Run("yaml body content is as expected", func(t *testing.T) {
|
||||
sut := createProvisioningSrvSut(t)
|
||||
rc := createTestRequestCtx()
|
||||
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa"))
|
||||
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb"))
|
||||
insertRule(t, sut, createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb"))
|
||||
rule1 := createTestAlertRuleWithFolderAndGroup("rule1", 1, "folder-uid", "groupa")
|
||||
rule1.NotificationSettings = nil
|
||||
rule2 := createTestAlertRuleWithFolderAndGroup("rule2", 1, "folder-uid", "groupb")
|
||||
rule1.NotificationSettings = &definitions.AlertRuleNotificationSettings{Receiver: "Email"}
|
||||
rule3 := createTestAlertRuleWithFolderAndGroup("rule3", 1, "folder-uid2", "groupb")
|
||||
insertRule(t, sut, rule1)
|
||||
insertRule(t, sut, rule2)
|
||||
insertRule(t, sut, rule3)
|
||||
|
||||
rc.Context.Req.Header.Add("Accept", "application/yaml")
|
||||
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n"
|
||||
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Email\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
|
||||
|
||||
response := sut.RouteGetAlertRulesExport(&rc)
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
})
|
||||
@@ -927,10 +943,9 @@ func TestProvisioningApi(t *testing.T) {
|
||||
|
||||
rc.Context.Req.Header.Add("Accept", "application/json")
|
||||
rc.Context.Req.Form.Set("folderUid", "folder-uid")
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
response := sut.RouteGetAlertRulesExport(&rc)
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
})
|
||||
@@ -945,10 +960,9 @@ func TestProvisioningApi(t *testing.T) {
|
||||
rc.Context.Req.Header.Add("Accept", "application/json")
|
||||
rc.Context.Req.Form.Set("folder_uid", "folder-uid")
|
||||
rc.Context.Req.Form.Add("folder_uid", "folder-uid2")
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
response := sut.RouteGetAlertRulesExport(&rc)
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
})
|
||||
@@ -964,10 +978,9 @@ func TestProvisioningApi(t *testing.T) {
|
||||
rc.Context.Req.Form.Set("folderUid", "folder-uid")
|
||||
rc.Context.Req.Form.Set("group", "groupa")
|
||||
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
response := sut.RouteGetAlertRulesExport(&rc)
|
||||
|
||||
require.Equal(t, 200, response.Status())
|
||||
require.Equal(t, expectedResponse, string(response.Body()))
|
||||
|
||||
@@ -1003,7 +1016,7 @@ func TestProvisioningApi(t *testing.T) {
|
||||
rc.Context.Req.Header.Add("Accept", "application/json")
|
||||
rc.Context.Req.Form.Set("ruleUid", "rule1")
|
||||
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false}]}]}`
|
||||
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":0,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
|
||||
|
||||
response := sut.RouteGetAlertRulesExport(&rc)
|
||||
|
||||
@@ -1629,10 +1642,10 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
|
||||
return ProvisioningSrv{
|
||||
log: env.log,
|
||||
policies: newFakeNotificationPolicyService(),
|
||||
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log),
|
||||
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store),
|
||||
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
|
||||
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
|
||||
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, 100, env.log),
|
||||
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.dashboardService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1819,6 +1832,14 @@ func createTestAlertRule(title string, orgID int64) definitions.ProvisionedAlert
|
||||
For: model.Duration(60),
|
||||
NoDataState: definitions.OK,
|
||||
ExecErrState: definitions.OkErrState,
|
||||
NotificationSettings: &definitions.AlertRuleNotificationSettings{
|
||||
Receiver: "Test-Receiver",
|
||||
GroupBy: []string{"alertname", "grafana_folder", "test"},
|
||||
GroupWait: util.Pointer(model.Duration(1 * time.Second)),
|
||||
GroupInterval: util.Pointer(model.Duration(5 * time.Second)),
|
||||
RepeatInterval: util.Pointer(model.Duration(5 * time.Minute)),
|
||||
MuteTimeIntervals: []string{"test-mute"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@@ -33,6 +35,14 @@ type ConditionValidator interface {
|
||||
Validate(ctx eval.EvaluationContext, condition ngmodels.Condition) error
|
||||
}
|
||||
|
||||
type AMConfigStore interface {
|
||||
GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*ngmodels.AlertConfiguration, error)
|
||||
}
|
||||
|
||||
type AMRefresher interface {
|
||||
ApplyConfig(ctx context.Context, orgId int64, dbConfig *ngmodels.AlertConfiguration) error
|
||||
}
|
||||
|
||||
type RulerSrv struct {
|
||||
xactManager provisioning.TransactionManager
|
||||
provenanceStore provisioning.ProvisioningStore
|
||||
@@ -42,6 +52,10 @@ type RulerSrv struct {
|
||||
cfg *setting.UnifiedAlertingSettings
|
||||
conditionValidator ConditionValidator
|
||||
authz RuleAccessControlService
|
||||
|
||||
amConfigStore AMConfigStore
|
||||
amRefresher AMRefresher
|
||||
featureManager featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -283,8 +297,11 @@ func (srv RulerSrv) checkGroupLimits(group apimodels.PostableRuleGroupConfig) er
|
||||
|
||||
// updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database.
|
||||
// All operations are performed in a single transaction
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey ngmodels.AlertRuleGroupKey, rules []*ngmodels.AlertRuleWithOptionals) response.Response {
|
||||
var finalChanges *store.GroupDelta
|
||||
var dbConfig *ngmodels.AlertConfiguration
|
||||
err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error {
|
||||
userNamespace, id := c.SignedInUser.GetNamespacedID()
|
||||
logger := srv.log.New("namespace_uid", groupKey.NamespaceUID, "group",
|
||||
@@ -309,6 +326,24 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey
|
||||
return err
|
||||
}
|
||||
|
||||
newOrUpdatedNotificationSettings := groupChanges.NewOrUpdatedNotificationSettings()
|
||||
if len(newOrUpdatedNotificationSettings) > 0 {
|
||||
dbConfig, err = srv.amConfigStore.GetLatestAlertmanagerConfiguration(c.Req.Context(), groupChanges.GroupKey.OrgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest configuration: %w", err)
|
||||
}
|
||||
cfg, err := notifier.Load([]byte(dbConfig.AlertmanagerConfiguration))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse configuration: %w", err)
|
||||
}
|
||||
validator := notifier.NewNotificationSettingsValidator(&cfg.AlertmanagerConfig)
|
||||
for _, s := range newOrUpdatedNotificationSettings {
|
||||
if err := validator.Validate(s); err != nil {
|
||||
return errors.Join(ngmodels.ErrAlertRuleFailedValidation, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := verifyProvisionedRulesNotAffected(c.Req.Context(), srv.provenanceStore, c.SignedInUser.GetOrgID(), groupChanges); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -392,6 +427,15 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
|
||||
}
|
||||
|
||||
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingSimplifiedRouting) && dbConfig != nil {
|
||||
// This isn't strictly necessary since the alertmanager config is periodically synced.
|
||||
err := srv.amRefresher.ApplyConfig(c.Req.Context(), groupKey.OrgID, dbConfig)
|
||||
if err != nil {
|
||||
srv.log.Warn("Failed to refresh Alertmanager config for org after change in notification settings", "org", c.SignedInUser.GetOrgID(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return changesToResponse(finalChanges)
|
||||
}
|
||||
|
||||
@@ -440,23 +484,25 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[stri
|
||||
if prov, exists := provenanceRecords[r.ResourceID()]; exists {
|
||||
provenance = prov
|
||||
}
|
||||
|
||||
gettableExtendedRuleNode := apimodels.GettableExtendedRuleNode{
|
||||
GrafanaManagedAlert: &apimodels.GettableGrafanaRule{
|
||||
ID: r.ID,
|
||||
OrgID: r.OrgID,
|
||||
Title: r.Title,
|
||||
Condition: r.Condition,
|
||||
Data: ApiAlertQueriesFromAlertQueries(r.Data),
|
||||
Updated: r.Updated,
|
||||
IntervalSeconds: r.IntervalSeconds,
|
||||
Version: r.Version,
|
||||
UID: r.UID,
|
||||
NamespaceUID: r.NamespaceUID,
|
||||
RuleGroup: r.RuleGroup,
|
||||
NoDataState: apimodels.NoDataState(r.NoDataState),
|
||||
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
|
||||
Provenance: apimodels.Provenance(provenance),
|
||||
IsPaused: r.IsPaused,
|
||||
ID: r.ID,
|
||||
OrgID: r.OrgID,
|
||||
Title: r.Title,
|
||||
Condition: r.Condition,
|
||||
Data: ApiAlertQueriesFromAlertQueries(r.Data),
|
||||
Updated: r.Updated,
|
||||
IntervalSeconds: r.IntervalSeconds,
|
||||
Version: r.Version,
|
||||
UID: r.UID,
|
||||
NamespaceUID: r.NamespaceUID,
|
||||
RuleGroup: r.RuleGroup,
|
||||
NoDataState: apimodels.NoDataState(r.NoDataState),
|
||||
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
|
||||
Provenance: apimodels.Provenance(provenance),
|
||||
IsPaused: r.IsPaused,
|
||||
NotificationSettings: AlertRuleNotificationSettingsFromNotificationSettings(r.NotificationSettings),
|
||||
},
|
||||
}
|
||||
forDuration := model.Duration(r.For)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
@@ -603,10 +604,24 @@ func createService(store *fakes.RuleStore) *RulerSrv {
|
||||
cfg: &setting.UnifiedAlertingSettings{
|
||||
BaseInterval: 10 * time.Second,
|
||||
},
|
||||
authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(setting.NewCfg())),
|
||||
authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(setting.NewCfg())),
|
||||
amConfigStore: &fakeAMRefresher{},
|
||||
amRefresher: &fakeAMRefresher{},
|
||||
featureManager: &featuremgmt.FeatureManager{},
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAMRefresher struct {
|
||||
}
|
||||
|
||||
func (f *fakeAMRefresher) ApplyConfig(ctx context.Context, orgId int64, dbConfig *models.AlertConfiguration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeAMRefresher) GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func createRequestContext(orgID int64, params map[string]string) *contextmodel.ReqContext {
|
||||
defaultPerms := map[int64]map[string][]string{orgID: {datasources.ActionQuery: []string{datasources.ScopeAll}}}
|
||||
return createRequestContextWithPerms(orgID, defaultPerms, params)
|
||||
|
||||
@@ -97,6 +97,13 @@ func validateRuleNode(
|
||||
ExecErrState: errorState,
|
||||
}
|
||||
|
||||
if ruleNode.GrafanaManagedAlert.NotificationSettings != nil {
|
||||
newAlertRule.NotificationSettings, err = validateNotificationSettings(ruleNode.GrafanaManagedAlert.NotificationSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
newAlertRule.For, err = validateForInterval(ruleNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -104,6 +111,10 @@ func validateRuleNode(
|
||||
|
||||
if ruleNode.ApiRuleNode != nil {
|
||||
newAlertRule.Annotations = ruleNode.ApiRuleNode.Annotations
|
||||
err = validateLabels(ruleNode.Labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newAlertRule.Labels = ruleNode.ApiRuleNode.Labels
|
||||
|
||||
err = newAlertRule.SetDashboardAndPanelFromAnnotations()
|
||||
@@ -114,6 +125,15 @@ func validateRuleNode(
|
||||
return &newAlertRule, nil
|
||||
}
|
||||
|
||||
func validateLabels(l map[string]string) error {
|
||||
for key := range l {
|
||||
if _, ok := ngmodels.LabelsUserCannotSpecify[key]; ok {
|
||||
return fmt.Errorf("system reserved labels cannot be defined in the rule. Label %s is the reserved", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCondition(condition string, queries []apimodels.AlertQuery) error {
|
||||
if condition == "" {
|
||||
return errors.New("condition cannot be empty")
|
||||
@@ -235,3 +255,21 @@ func validateRuleGroup(
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func validateNotificationSettings(n *apimodels.AlertRuleNotificationSettings) ([]ngmodels.NotificationSettings, error) {
|
||||
s := ngmodels.NotificationSettings{
|
||||
Receiver: n.Receiver,
|
||||
GroupBy: n.GroupBy,
|
||||
GroupWait: n.GroupWait,
|
||||
GroupInterval: n.GroupInterval,
|
||||
RepeatInterval: n.RepeatInterval,
|
||||
MuteTimeIntervals: n.MuteTimeIntervals,
|
||||
}
|
||||
|
||||
if err := s.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid notification settings: %w", err)
|
||||
}
|
||||
return []ngmodels.NotificationSettings{
|
||||
s,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -793,3 +793,117 @@ func TestValidateRuleNodeIntervalFailures(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleNodeNotificationSettings(t *testing.T) {
|
||||
cfg := config(t)
|
||||
|
||||
validNotificationSettings := models.NotificationSettingsGen(models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
notificationSettings models.NotificationSettings
|
||||
expErrorContains string
|
||||
}{
|
||||
{
|
||||
name: "valid notification settings",
|
||||
notificationSettings: validNotificationSettings(),
|
||||
},
|
||||
{
|
||||
name: "missing receiver is invalid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithReceiver("")),
|
||||
expErrorContains: "receiver",
|
||||
},
|
||||
{
|
||||
name: "group by empty is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy()),
|
||||
},
|
||||
{
|
||||
name: "group by ... is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy("...")),
|
||||
},
|
||||
{
|
||||
name: "group by with alert name and folder name labels is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel)),
|
||||
},
|
||||
{
|
||||
name: "group by missing alert name label is invalid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(models.FolderTitleLabel)),
|
||||
expErrorContains: model.AlertNameLabel,
|
||||
},
|
||||
{
|
||||
name: "group by missing folder name label is invalid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupBy(model.AlertNameLabel)),
|
||||
expErrorContains: models.FolderTitleLabel,
|
||||
},
|
||||
{
|
||||
name: "group wait empty is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(nil)),
|
||||
},
|
||||
{
|
||||
name: "group wait positive is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(util.Pointer(1*time.Second))),
|
||||
},
|
||||
{
|
||||
name: "group wait negative is invalid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupWait(util.Pointer(-1*time.Second))),
|
||||
expErrorContains: "group wait",
|
||||
},
|
||||
{
|
||||
name: "group interval empty is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(nil)),
|
||||
},
|
||||
{
|
||||
name: "group interval positive is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Second))),
|
||||
},
|
||||
{
|
||||
name: "group interval negative is invalid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithGroupInterval(util.Pointer(-1*time.Second))),
|
||||
expErrorContains: "group interval",
|
||||
},
|
||||
{
|
||||
name: "repeat interval empty is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(nil)),
|
||||
},
|
||||
{
|
||||
name: "repeat interval positive is valid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(util.Pointer(1*time.Second))),
|
||||
},
|
||||
{
|
||||
name: "repeat interval negative is invalid",
|
||||
notificationSettings: models.CopyNotificationSettings(validNotificationSettings(), models.NSMuts.WithRepeatInterval(util.Pointer(-1*time.Second))),
|
||||
expErrorContains: "repeat interval",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := validRule()
|
||||
r.GrafanaManagedAlert.NotificationSettings = AlertRuleNotificationSettingsFromNotificationSettings([]models.NotificationSettings{tt.notificationSettings})
|
||||
_, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder(), cfg)
|
||||
|
||||
if tt.expErrorContains != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tt.expErrorContains)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleNodeReservedLabels(t *testing.T) {
|
||||
cfg := config(t)
|
||||
|
||||
for label := range models.LabelsUserCannotSpecify {
|
||||
t.Run(label, func(t *testing.T) {
|
||||
r := validRule()
|
||||
r.ApiRuleNode.Labels = map[string]string{
|
||||
label: "true",
|
||||
}
|
||||
_, err := validateRuleNode(&r, util.GenerateShortUID(), cfg.BaseInterval*time.Duration(rand.Int63n(10)+1), rand.Int63(), randFolder(), cfg)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, label)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext,
|
||||
now,
|
||||
rule,
|
||||
results,
|
||||
state.GetRuleExtraLabels(rule, folder.Fullpath, includeFolder),
|
||||
state.GetRuleExtraLabels(log.New("testing"), rule, folder.Fullpath, includeFolder),
|
||||
)
|
||||
|
||||
alerts := make([]*amv2.PostableAlert, 0, len(transitions))
|
||||
|
||||
@@ -15,43 +15,45 @@ import (
|
||||
// AlertRuleFromProvisionedAlertRule converts definitions.ProvisionedAlertRule to models.AlertRule
|
||||
func AlertRuleFromProvisionedAlertRule(a definitions.ProvisionedAlertRule) (models.AlertRule, error) {
|
||||
return models.AlertRule{
|
||||
ID: a.ID,
|
||||
UID: a.UID,
|
||||
OrgID: a.OrgID,
|
||||
NamespaceUID: a.FolderUID,
|
||||
RuleGroup: a.RuleGroup,
|
||||
Title: a.Title,
|
||||
Condition: a.Condition,
|
||||
Data: AlertQueriesFromApiAlertQueries(a.Data),
|
||||
Updated: a.Updated,
|
||||
NoDataState: models.NoDataState(a.NoDataState), // TODO there must be a validation
|
||||
ExecErrState: models.ExecutionErrorState(a.ExecErrState), // TODO there must be a validation
|
||||
For: time.Duration(a.For),
|
||||
Annotations: a.Annotations,
|
||||
Labels: a.Labels,
|
||||
IsPaused: a.IsPaused,
|
||||
ID: a.ID,
|
||||
UID: a.UID,
|
||||
OrgID: a.OrgID,
|
||||
NamespaceUID: a.FolderUID,
|
||||
RuleGroup: a.RuleGroup,
|
||||
Title: a.Title,
|
||||
Condition: a.Condition,
|
||||
Data: AlertQueriesFromApiAlertQueries(a.Data),
|
||||
Updated: a.Updated,
|
||||
NoDataState: models.NoDataState(a.NoDataState), // TODO there must be a validation
|
||||
ExecErrState: models.ExecutionErrorState(a.ExecErrState), // TODO there must be a validation
|
||||
For: time.Duration(a.For),
|
||||
Annotations: a.Annotations,
|
||||
Labels: a.Labels,
|
||||
IsPaused: a.IsPaused,
|
||||
NotificationSettings: NotificationSettingsFromAlertRuleNotificationSettings(a.NotificationSettings),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ProvisionedAlertRuleFromAlertRule converts models.AlertRule to definitions.ProvisionedAlertRule and sets provided provenance status
|
||||
func ProvisionedAlertRuleFromAlertRule(rule models.AlertRule, provenance models.Provenance) definitions.ProvisionedAlertRule {
|
||||
return definitions.ProvisionedAlertRule{
|
||||
ID: rule.ID,
|
||||
UID: rule.UID,
|
||||
OrgID: rule.OrgID,
|
||||
FolderUID: rule.NamespaceUID,
|
||||
RuleGroup: rule.RuleGroup,
|
||||
Title: rule.Title,
|
||||
For: model.Duration(rule.For),
|
||||
Condition: rule.Condition,
|
||||
Data: ApiAlertQueriesFromAlertQueries(rule.Data),
|
||||
Updated: rule.Updated,
|
||||
NoDataState: definitions.NoDataState(rule.NoDataState), // TODO there may be a validation
|
||||
ExecErrState: definitions.ExecutionErrorState(rule.ExecErrState), // TODO there may be a validation
|
||||
Annotations: rule.Annotations,
|
||||
Labels: rule.Labels,
|
||||
Provenance: definitions.Provenance(provenance), // TODO validate enum conversion?
|
||||
IsPaused: rule.IsPaused,
|
||||
ID: rule.ID,
|
||||
UID: rule.UID,
|
||||
OrgID: rule.OrgID,
|
||||
FolderUID: rule.NamespaceUID,
|
||||
RuleGroup: rule.RuleGroup,
|
||||
Title: rule.Title,
|
||||
For: model.Duration(rule.For),
|
||||
Condition: rule.Condition,
|
||||
Data: ApiAlertQueriesFromAlertQueries(rule.Data),
|
||||
Updated: rule.Updated,
|
||||
NoDataState: definitions.NoDataState(rule.NoDataState), // TODO there may be a validation
|
||||
ExecErrState: definitions.ExecutionErrorState(rule.ExecErrState), // TODO there may be a validation
|
||||
Annotations: rule.Annotations,
|
||||
Labels: rule.Labels,
|
||||
Provenance: definitions.Provenance(provenance), // TODO validate enum conversion?
|
||||
IsPaused: rule.IsPaused,
|
||||
NotificationSettings: AlertRuleNotificationSettingsFromNotificationSettings(rule.NotificationSettings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,16 +177,17 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE
|
||||
}
|
||||
|
||||
result := definitions.AlertRuleExport{
|
||||
UID: rule.UID,
|
||||
Title: rule.Title,
|
||||
For: model.Duration(rule.For),
|
||||
Condition: rule.Condition,
|
||||
Data: data,
|
||||
DashboardUID: rule.DashboardUID,
|
||||
PanelID: rule.PanelID,
|
||||
NoDataState: definitions.NoDataState(rule.NoDataState),
|
||||
ExecErrState: definitions.ExecutionErrorState(rule.ExecErrState),
|
||||
IsPaused: rule.IsPaused,
|
||||
UID: rule.UID,
|
||||
Title: rule.Title,
|
||||
For: model.Duration(rule.For),
|
||||
Condition: rule.Condition,
|
||||
Data: data,
|
||||
DashboardUID: rule.DashboardUID,
|
||||
PanelID: rule.PanelID,
|
||||
NoDataState: definitions.NoDataState(rule.NoDataState),
|
||||
ExecErrState: definitions.ExecutionErrorState(rule.ExecErrState),
|
||||
IsPaused: rule.IsPaused,
|
||||
NotificationSettings: AlertRuleNotificationSettingsExportFromNotificationSettings(rule.NotificationSettings),
|
||||
}
|
||||
if rule.For.Seconds() > 0 {
|
||||
result.ForString = util.Pointer(model.Duration(rule.For).String())
|
||||
@@ -373,3 +376,61 @@ func MuteTimingIntervalToMuteTimeIntervalHclExport(m definitions.MuteTimeInterva
|
||||
err = j.Unmarshal(mdata, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// AlertRuleNotificationSettingsFromNotificationSettings converts []models.NotificationSettings to definitions.AlertRuleNotificationSettings
|
||||
func AlertRuleNotificationSettingsFromNotificationSettings(ns []models.NotificationSettings) *definitions.AlertRuleNotificationSettings {
|
||||
if len(ns) == 0 {
|
||||
return nil
|
||||
}
|
||||
m := ns[0]
|
||||
return &definitions.AlertRuleNotificationSettings{
|
||||
Receiver: m.Receiver,
|
||||
GroupBy: m.GroupBy,
|
||||
GroupWait: m.GroupWait,
|
||||
GroupInterval: m.GroupInterval,
|
||||
RepeatInterval: m.RepeatInterval,
|
||||
MuteTimeIntervals: m.MuteTimeIntervals,
|
||||
}
|
||||
}
|
||||
|
||||
// AlertRuleNotificationSettingsFromNotificationSettings converts []models.NotificationSettings to definitions.AlertRuleNotificationSettingsExport
|
||||
func AlertRuleNotificationSettingsExportFromNotificationSettings(ns []models.NotificationSettings) *definitions.AlertRuleNotificationSettingsExport {
|
||||
if len(ns) == 0 {
|
||||
return nil
|
||||
}
|
||||
m := ns[0]
|
||||
|
||||
toStringIfNotNil := func(d *model.Duration) *string {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
s := d.String()
|
||||
return &s
|
||||
}
|
||||
|
||||
return &definitions.AlertRuleNotificationSettingsExport{
|
||||
Receiver: m.Receiver,
|
||||
GroupBy: m.GroupBy,
|
||||
GroupWait: toStringIfNotNil(m.GroupWait),
|
||||
GroupInterval: toStringIfNotNil(m.GroupInterval),
|
||||
RepeatInterval: toStringIfNotNil(m.RepeatInterval),
|
||||
MuteTimeIntervals: m.MuteTimeIntervals,
|
||||
}
|
||||
}
|
||||
|
||||
// NotificationSettingsFromAlertRuleNotificationSettings converts definitions.AlertRuleNotificationSettings to []models.NotificationSettings
|
||||
func NotificationSettingsFromAlertRuleNotificationSettings(ns *definitions.AlertRuleNotificationSettings) []models.NotificationSettings {
|
||||
if ns == nil {
|
||||
return nil
|
||||
}
|
||||
return []models.NotificationSettings{
|
||||
{
|
||||
Receiver: ns.Receiver,
|
||||
GroupBy: ns.GroupBy,
|
||||
GroupWait: ns.GroupWait,
|
||||
GroupInterval: ns.GroupInterval,
|
||||
RepeatInterval: ns.RepeatInterval,
|
||||
MuteTimeIntervals: ns.MuteTimeIntervals,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,5 +77,14 @@ resource "grafana_rule_group" "rule_group_0000" {
|
||||
no_data_state = "NoData"
|
||||
exec_err_state = "Alerting"
|
||||
is_paused = false
|
||||
|
||||
notification_settings {
|
||||
receiver = "Test-Receiver"
|
||||
group_by = ["alertname", "grafana_folder", "test"]
|
||||
group_wait = "1s"
|
||||
group_interval = "5s"
|
||||
repeat_interval = "5m"
|
||||
mute_time_intervals = ["test-mute"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,15 @@
|
||||
"noDataState": "NoData",
|
||||
"execErrState": "Alerting",
|
||||
"for": "0s",
|
||||
"isPaused": false
|
||||
"isPaused": false,
|
||||
"notification_settings":{
|
||||
"receiver":"Test-Receiver",
|
||||
"group_by":["alertname","grafana_folder","test"],
|
||||
"group_wait":"1s",
|
||||
"group_interval":"5s",
|
||||
"repeat_interval":"5m",
|
||||
"mute_time_intervals":["test-mute"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -83,3 +83,14 @@ groups:
|
||||
execErrState: Alerting
|
||||
for: 0s
|
||||
isPaused: false
|
||||
notification_settings:
|
||||
receiver: Test-Receiver
|
||||
group_by:
|
||||
- alertname
|
||||
- grafana_folder
|
||||
- test
|
||||
group_wait: 1s
|
||||
group_interval: 5s
|
||||
repeat_interval: 5m
|
||||
mute_time_intervals:
|
||||
- test-mute
|
||||
|
||||
@@ -109,7 +109,15 @@
|
||||
}
|
||||
],
|
||||
"no_data_state": "NoData",
|
||||
"exec_err_state": "Alerting"
|
||||
"exec_err_state": "Alerting",
|
||||
"notification_settings":{
|
||||
"receiver":"Test-Receiver",
|
||||
"group_by":["alertname","grafana_folder","test"],
|
||||
"group_wait":"1s",
|
||||
"group_interval":"5s",
|
||||
"repeat_interval":"5m",
|
||||
"mute_time_intervals":["test-mute"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -735,6 +735,18 @@ type GettableApiAlertingConfig struct {
|
||||
Receivers []*GettableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
|
||||
}
|
||||
|
||||
func (c *GettableApiAlertingConfig) GetReceivers() []*GettableApiReceiver {
|
||||
return c.Receivers
|
||||
}
|
||||
|
||||
func (c *GettableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval {
|
||||
return c.MuteTimeIntervals
|
||||
}
|
||||
|
||||
func (c *GettableApiAlertingConfig) GetRoute() *Route {
|
||||
return c.Route
|
||||
}
|
||||
|
||||
func (c *GettableApiAlertingConfig) UnmarshalJSON(b []byte) error {
|
||||
type plain GettableApiAlertingConfig
|
||||
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
|
||||
@@ -960,6 +972,18 @@ type PostableApiAlertingConfig struct {
|
||||
Receivers []*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
|
||||
}
|
||||
|
||||
func (c *PostableApiAlertingConfig) GetReceivers() []*PostableApiReceiver {
|
||||
return c.Receivers
|
||||
}
|
||||
|
||||
func (c *PostableApiAlertingConfig) GetMuteTimeIntervals() []config.MuteTimeInterval {
|
||||
return c.MuteTimeIntervals
|
||||
}
|
||||
|
||||
func (c *PostableApiAlertingConfig) GetRoute() *Route {
|
||||
return c.Route
|
||||
}
|
||||
|
||||
func (c *PostableApiAlertingConfig) UnmarshalJSON(b []byte) error {
|
||||
type plain PostableApiAlertingConfig
|
||||
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
|
||||
@@ -1047,6 +1071,12 @@ func AllReceivers(route *config.Route) (res []string) {
|
||||
if route == nil {
|
||||
return res
|
||||
}
|
||||
// TODO: Consider removing this check when new resource-specific AM APIs are implemented.
|
||||
// Skip autogenerated routes. This helps cover the case where an admin POSTs the autogenerated route back to us.
|
||||
// For example, when deleting a contact point that is unused but still referenced in the autogenerated route.
|
||||
if isAutogeneratedRoot(route) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if route.Receiver != "" {
|
||||
res = append(res, route.Receiver)
|
||||
@@ -1058,6 +1088,14 @@ func AllReceivers(route *config.Route) (res []string) {
|
||||
return res
|
||||
}
|
||||
|
||||
// autogeneratedRouteLabel a label name used to distinguish alerts that are supposed to be handled by the autogenerated policy. Only expected value is `true`.
|
||||
const autogeneratedRouteLabel = "__grafana_autogenerated__"
|
||||
|
||||
// isAutogeneratedRoot returns true if the route is the root of an autogenerated route.
|
||||
func isAutogeneratedRoot(route *config.Route) bool {
|
||||
return len(route.Matchers) == 1 && route.Matchers[0].Name == autogeneratedRouteLabel
|
||||
}
|
||||
|
||||
type RawMessage json.RawMessage // This type alias adds YAML marshaling to the json.RawMessage.
|
||||
|
||||
// MarshalJSON returns m as the JSON encoding of m.
|
||||
@@ -1195,6 +1233,10 @@ func (r *GettableApiReceiver) Type() ReceiverType {
|
||||
return AlertmanagerReceiverType
|
||||
}
|
||||
|
||||
func (r *GettableApiReceiver) GetName() string {
|
||||
return r.Receiver.Name
|
||||
}
|
||||
|
||||
type PostableApiReceiver struct {
|
||||
config.Receiver `yaml:",inline"`
|
||||
PostableGrafanaReceivers `yaml:",inline"`
|
||||
@@ -1263,6 +1305,10 @@ func (r *PostableApiReceiver) Type() ReceiverType {
|
||||
return AlertmanagerReceiverType
|
||||
}
|
||||
|
||||
func (r *PostableApiReceiver) GetName() string {
|
||||
return r.Receiver.Name
|
||||
}
|
||||
|
||||
type GettableGrafanaReceivers struct {
|
||||
GrafanaManagedReceivers []*GettableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -340,6 +341,78 @@ func Test_ApiAlertingConfig_Marshaling(t *testing.T) {
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "success undefined am receiver in autogenerated route is ignored",
|
||||
input: PostableApiAlertingConfig{
|
||||
Config: Config{
|
||||
Route: &Route{
|
||||
Receiver: "am",
|
||||
Routes: []*Route{
|
||||
{
|
||||
Matchers: config.Matchers{
|
||||
{
|
||||
Name: autogeneratedRouteLabel,
|
||||
Type: labels.MatchEqual,
|
||||
Value: "true",
|
||||
},
|
||||
},
|
||||
Routes: []*Route{
|
||||
{
|
||||
Receiver: "unmentioned",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []*PostableApiReceiver{
|
||||
{
|
||||
Receiver: config.Receiver{
|
||||
Name: "am",
|
||||
EmailConfigs: []*config.EmailConfig{{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "success undefined graf receiver in autogenerated route is ignored",
|
||||
input: PostableApiAlertingConfig{
|
||||
Config: Config{
|
||||
Route: &Route{
|
||||
Receiver: "graf",
|
||||
Routes: []*Route{
|
||||
{
|
||||
Matchers: config.Matchers{
|
||||
{
|
||||
Name: autogeneratedRouteLabel,
|
||||
Type: labels.MatchEqual,
|
||||
Value: "true",
|
||||
},
|
||||
},
|
||||
Routes: []*Route{
|
||||
{
|
||||
Receiver: "unmentioned",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []*PostableApiReceiver{
|
||||
{
|
||||
Receiver: config.Receiver{
|
||||
Name: "graf",
|
||||
},
|
||||
PostableGrafanaReceivers: PostableGrafanaReceivers{
|
||||
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
encoded, err := json.Marshal(tc.input)
|
||||
|
||||
@@ -13,12 +13,15 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// groupByAll is a special value defined by alertmanager that can be used in a Route's GroupBy field to aggregate by all possible labels.
|
||||
const groupByAll = "..."
|
||||
|
||||
// Validate normalizes a possibly nested Route r, and returns errors if r is invalid.
|
||||
func (r *Route) validateChild() error {
|
||||
r.GroupBy = nil
|
||||
r.GroupByAll = false
|
||||
for _, l := range r.GroupByStr {
|
||||
if l == "..." {
|
||||
if l == groupByAll {
|
||||
r.GroupByAll = true
|
||||
} else {
|
||||
r.GroupBy = append(r.GroupBy, model.LabelName(l))
|
||||
|
||||
@@ -406,34 +406,80 @@ const (
|
||||
ErrorErrState ExecutionErrorState = "Error"
|
||||
)
|
||||
|
||||
// swagger: model
|
||||
type AlertRuleNotificationSettings struct {
|
||||
// Name of the receiver to send notifications to.
|
||||
// required: true
|
||||
// example: grafana-default-email
|
||||
Receiver string `json:"receiver"`
|
||||
|
||||
// Optional settings
|
||||
|
||||
// Override the labels by which incoming alerts are grouped together. For example, multiple alerts coming in for
|
||||
// cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels
|
||||
// use the special value '...' as the sole label name.
|
||||
// This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what
|
||||
// you want, unless you have a very low alert volume or your upstream notification system performs its own grouping.
|
||||
// Must include 'alertname' and 'grafana_folder' if not using '...'.
|
||||
// default: ["alertname", "grafana_folder"]
|
||||
// example: ["alertname", "grafana_folder", "cluster"]
|
||||
GroupBy []string `json:"group_by,omitempty"`
|
||||
|
||||
// Override how long to initially wait to send a notification for a group of alerts. Allows to wait for an
|
||||
// inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)
|
||||
// example: 30s
|
||||
GroupWait *model.Duration `json:"group_wait,omitempty"`
|
||||
|
||||
// Override how long to wait before sending a notification about new alerts that are added to a group of alerts for
|
||||
// which an initial notification has already been sent. (Usually ~5m or more.)
|
||||
// example: 5m
|
||||
GroupInterval *model.Duration `json:"group_interval,omitempty"`
|
||||
|
||||
// Override how long to wait before sending a notification again if it has already been sent successfully for an
|
||||
// alert. (Usually ~3h or more).
|
||||
// Note that this parameter is implicitly bound by Alertmanager's `--data.retention` configuration flag.
|
||||
// Notifications will be resent after either repeat_interval or the data retention period have passed, whichever
|
||||
// occurs first. `repeat_interval` should not be less than `group_interval`.
|
||||
// example: 4h
|
||||
RepeatInterval *model.Duration `json:"repeat_interval,omitempty"`
|
||||
|
||||
// Override the times when notifications should be muted. These must match the name of a mute time interval defined
|
||||
// in the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but
|
||||
// otherwise acts normally.
|
||||
// example: ["maintenance"]
|
||||
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type PostableGrafanaRule struct {
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Condition string `json:"condition" yaml:"condition"`
|
||||
Data []AlertQuery `json:"data" yaml:"data"`
|
||||
UID string `json:"uid" yaml:"uid"`
|
||||
NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"`
|
||||
ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"`
|
||||
IsPaused *bool `json:"is_paused" yaml:"is_paused"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Condition string `json:"condition" yaml:"condition"`
|
||||
Data []AlertQuery `json:"data" yaml:"data"`
|
||||
UID string `json:"uid" yaml:"uid"`
|
||||
NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"`
|
||||
ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"`
|
||||
IsPaused *bool `json:"is_paused" yaml:"is_paused"`
|
||||
NotificationSettings *AlertRuleNotificationSettings `json:"notification_settings" yaml:"notification_settings"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type GettableGrafanaRule struct {
|
||||
ID int64 `json:"id" yaml:"id"`
|
||||
OrgID int64 `json:"orgId" yaml:"orgId"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Condition string `json:"condition" yaml:"condition"`
|
||||
Data []AlertQuery `json:"data" yaml:"data"`
|
||||
Updated time.Time `json:"updated" yaml:"updated"`
|
||||
IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"`
|
||||
Version int64 `json:"version" yaml:"version"`
|
||||
UID string `json:"uid" yaml:"uid"`
|
||||
NamespaceUID string `json:"namespace_uid" yaml:"namespace_uid"`
|
||||
RuleGroup string `json:"rule_group" yaml:"rule_group"`
|
||||
NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"`
|
||||
ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"`
|
||||
Provenance Provenance `json:"provenance,omitempty" yaml:"provenance,omitempty"`
|
||||
IsPaused bool `json:"is_paused" yaml:"is_paused"`
|
||||
ID int64 `json:"id" yaml:"id"`
|
||||
OrgID int64 `json:"orgId" yaml:"orgId"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
Condition string `json:"condition" yaml:"condition"`
|
||||
Data []AlertQuery `json:"data" yaml:"data"`
|
||||
Updated time.Time `json:"updated" yaml:"updated"`
|
||||
IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"`
|
||||
Version int64 `json:"version" yaml:"version"`
|
||||
UID string `json:"uid" yaml:"uid"`
|
||||
NamespaceUID string `json:"namespace_uid" yaml:"namespace_uid"`
|
||||
RuleGroup string `json:"rule_group" yaml:"rule_group"`
|
||||
NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"`
|
||||
ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"`
|
||||
Provenance Provenance `json:"provenance,omitempty" yaml:"provenance,omitempty"`
|
||||
IsPaused bool `json:"is_paused" yaml:"is_paused"`
|
||||
NotificationSettings *AlertRuleNotificationSettings `json:"notification_settings,omitempty" yaml:"notification_settings,omitempty"`
|
||||
}
|
||||
|
||||
// AlertQuery represents a single query associated with an alert definition.
|
||||
|
||||
@@ -156,6 +156,8 @@ type ProvisionedAlertRule struct {
|
||||
Provenance Provenance `json:"provenance,omitempty"`
|
||||
// example: false
|
||||
IsPaused bool `json:"isPaused"`
|
||||
// example: {"receiver":"email","group_by":["alertname","grafana_folder","cluster"],"group_wait":"30s","group_interval":"1m","repeat_interval":"4d","mute_time_intervals":["Weekends","Holidays"]}
|
||||
NotificationSettings *AlertRuleNotificationSettings `json:"notification_settings"`
|
||||
}
|
||||
|
||||
// swagger:route GET /v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RouteGetAlertRuleGroup
|
||||
@@ -246,10 +248,11 @@ type AlertRuleExport struct {
|
||||
// ForString is used to:
|
||||
// - Only export the for field for HCL if it is non-zero.
|
||||
// - Format the Prometheus model.Duration type properly for HCL.
|
||||
ForString *string `json:"-" yaml:"-" hcl:"for"`
|
||||
Annotations *map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty" hcl:"annotations"`
|
||||
Labels *map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" hcl:"labels"`
|
||||
IsPaused bool `json:"isPaused" yaml:"isPaused" hcl:"is_paused"`
|
||||
ForString *string `json:"-" yaml:"-" hcl:"for"`
|
||||
Annotations *map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty" hcl:"annotations"`
|
||||
Labels *map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" hcl:"labels"`
|
||||
IsPaused bool `json:"isPaused" yaml:"isPaused" hcl:"is_paused"`
|
||||
NotificationSettings *AlertRuleNotificationSettingsExport `json:"notification_settings,omitempty" yaml:"notification_settings,omitempty" hcl:"notification_settings,block"`
|
||||
}
|
||||
|
||||
// AlertQueryExport is the provisioned export of models.AlertQuery.
|
||||
@@ -266,3 +269,14 @@ type RelativeTimeRangeExport struct {
|
||||
FromSeconds int64 `json:"from" yaml:"from" hcl:"from"`
|
||||
ToSeconds int64 `json:"to" yaml:"to" hcl:"to"`
|
||||
}
|
||||
|
||||
// AlertRuleNotificationSettingsExport is the provisioned export of models.NotificationSettings.
|
||||
type AlertRuleNotificationSettingsExport struct {
|
||||
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"receiver"`
|
||||
|
||||
GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"`
|
||||
GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"`
|
||||
GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"`
|
||||
RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"`
|
||||
MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_time_intervals"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user