From 65e55b70a9c9025d0344834b9fe9ba2115af7c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 29 Apr 2022 00:51:41 +0200 Subject: [PATCH] Alerting: indicate whether an alertrule is provisioned (#48458) (#48471) (cherry picked from commit 9e21e4d1c1fdc609ea57a589945af6ec43f4c9e4) --- pkg/services/ngalert/CHANGELOG.md | 18 ++++++-- pkg/services/ngalert/api/api.go | 2 + pkg/services/ngalert/api/api_ruler.go | 37 ++++++++++++---- pkg/services/ngalert/api/api_ruler_test.go | 44 +++++++++++++++++++ pkg/services/ngalert/api/fork_ruler.go | 2 +- .../api/tooling/definitions/cortex-ruler.go | 1 + pkg/services/ngalert/ngalert.go | 1 + 7 files changed, 92 insertions(+), 13 deletions(-) diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index effe368ac44..027a7d0101f 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -45,10 +45,20 @@ Scopes must have an order to ensure consistency and ease of search, this helps u ## Grafana Alerting - main / unreleased -- [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216 -- [CHANGE] Notification URL points to alert view page instead of alert edit page. #47752 +- [BUGFIX] Use `NaN` and do not panic when captured alert values are empty #48370 - [FEATURE] Indicate whether routes are provisioned when GETting Alertmanager configuration #47857 - [FEATURE] Indicate whether contact point is provisioned when GETting Alertmanager configuration #48323 -- [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670 -- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 +- [FEATURE] Indicate whether alert rule is provisioned when GETting the rule #48458 + +## 8.5.1 + +- [BUGFIX] Silences: Invalid silences created through the API made grafana panic, they are now validated. #46892 - [ENHANCEMENT] Migration: Migrate each legacy notification channel to its own contact point, use nested routes to reproduce multi-channel alerts #47291 + +## 8.5.0 + +- [CHANGE] Prometheus Compatible API: Use float-like values for `api/prometheus/grafana/api/v1/alerts` and `api/prometheus/grafana/api/v1/rules` instead of the evaluation string #47216 +- [CHANGE] Notification URL points to alert view page instead of alert edit page. #47752 +- [BUGFIX] (Legacy) Templates: Parse notification templates using all the matches of the alert rule when going from `Alerting` to `OK` in legacy alerting #47355 +- [BUGFIX] Scheduler: Fix state manager to support OK option of `AlertRule.ExecErrState` #47670 +- [ENHANCEMENT] Templates: Enable the use of classic condition values in templates #46971 diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 4562f951d1a..7da24a5d636 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -67,6 +67,7 @@ type API struct { QuotaService *quota.QuotaService Schedule schedule.ScheduleService TransactionManager provisioning.TransactionManager + ProvenanceStore provisioning.ProvisioningStore RuleStore store.RuleStore InstanceStore store.InstanceStore AlertingStore AlertingStore @@ -109,6 +110,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { QuotaService: api.QuotaService, scheduleService: api.Schedule, store: api.RuleStore, + provenanceStore: api.ProvenanceStore, xactManager: api.TransactionManager, log: logger, cfg: &api.Cfg.UnifiedAlerting, diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index f8f9a43a10f..d7a08fe8b4a 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -30,6 +30,7 @@ import ( type RulerSrv struct { xactManager provisioning.TransactionManager + provenanceStore provisioning.ProvisioningStore store store.RuleStore DatasourceCache datasources.CacheService QuotaService *quota.QuotaService @@ -143,6 +144,11 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } + provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get provenance for rule group") + } + for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue @@ -154,11 +160,11 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, namespace.Id), + toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords), }, } } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id)) + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords)) ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig } } @@ -170,7 +176,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. return response.JSON(http.StatusAccepted, result) } -func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { +func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Response { namespaceTitle := web.Params(c.Req)[":Namespace"] namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false) if err != nil { @@ -194,12 +200,17 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } + provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") + } + for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue } ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second) - ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id)) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords)) } result := apimodels.RuleGroupConfigResponse{ @@ -255,6 +266,11 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } + provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") + } + for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue @@ -273,7 +289,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, folder.Id), + toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords), }, } } else { @@ -284,11 +300,11 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, folder.Id), + toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords), }, } } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id)) + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords)) configs[namespace][r.RuleGroup] = ruleGroupConfig } } @@ -437,7 +453,11 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"}) } -func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimodels.GettableExtendedRuleNode { +func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableExtendedRuleNode { + provenance := ngmodels.ProvenanceNone + if prov, exists := provenanceRecords[r.ResourceID()]; exists { + provenance = prov + } gettableExtendedRuleNode := apimodels.GettableExtendedRuleNode{ GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ ID: r.ID, @@ -454,6 +474,7 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimode RuleGroup: r.RuleGroup, NoDataState: apimodels.NoDataState(r.NoDataState), ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), + Provenance: provenance, }, } gettableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{ diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 1d10b29c80d..f0bfbb70c0a 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/schedule" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" @@ -550,6 +551,48 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) { assert.Emptyf(t, expectedRules, "not all expected rules were returned") }) }) + t.Run("should return the provenance of the alert rules", func(t *testing.T) { + orgID := rand.Int63() + folder := randFolder() + ruleStore := store.NewFakeRuleStore(t) + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) + expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder))) + ruleStore.PutRule(context.Background(), expectedRules...) + ac := acMock.New().WithDisabled() + + svc := createService(ac, ruleStore, nil) + + // add provenance to the first generated rule + rule := &models.AlertRule{ + UID: expectedRules[0].UID, + } + err := svc.provenanceStore.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) + require.NoError(t, err) + + response := svc.RouteGetNamespaceRulesConfig(createRequestContext(orgID, "", map[string]string{ + ":Namespace": folder.Title, + })) + + require.Equal(t, http.StatusAccepted, response.Status()) + result := &apimodels.NamespaceConfigResponse{} + require.NoError(t, json.Unmarshal(response.Body(), result)) + require.NotNil(t, result) + found := false + for namespace, groups := range *result { + require.Equal(t, folder.Title, namespace) + for _, group := range groups { + for _, actualRule := range group.Rules { + if actualRule.GrafanaManagedAlert.UID == expectedRules[0].UID { + require.Equal(t, models.ProvenanceAPI, actualRule.GrafanaManagedAlert.Provenance) + found = true + } else { + require.Equal(t, models.ProvenanceNone, actualRule.GrafanaManagedAlert.Provenance) + } + } + } + } + require.True(t, found) + }) } func createService(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedule.ScheduleService) *RulerSrv { @@ -558,6 +601,7 @@ func createService(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedu store: store, DatasourceCache: nil, QuotaService: nil, + provenanceStore: provisioning.NewFakeProvisioningStore(), scheduleService: scheduler, log: log.New("test"), cfg: nil, diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/fork_ruler.go index 69411ba9ce8..d43d31c632c 100644 --- a/pkg/services/ngalert/api/fork_ruler.go +++ b/pkg/services/ngalert/api/fork_ruler.go @@ -122,7 +122,7 @@ func (f *ForkedRulerApi) forkRouteGetNamespaceGrafanaRulesConfig(ctx *models.Req } func (f *ForkedRulerApi) forkRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { - return f.GrafanaRuler.RouteGetRulegGroupConfig(ctx) + return f.GrafanaRuler.RouteGetRulesGroupConfig(ctx) } func (f *ForkedRulerApi) forkRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index f8404905e9d..bc323d5dfb1 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -386,4 +386,5 @@ type GettableGrafanaRule struct { 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 models.Provenance `json:"provenance,omitempty" yaml:"provenance,omitempty"` } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 477842c86c6..a348491d562 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -156,6 +156,7 @@ func (ng *AlertNG) init() error { RuleStore: store, AlertingStore: store, AdminConfigStore: store, + ProvenanceStore: store, MultiOrgAlertmanager: ng.MultiOrgAlertmanager, StateManager: ng.stateManager, AccessControl: ng.accesscontrol,