diff --git a/go.mod b/go.mod index 0d75d717874..f4b90096247 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.2.0 github.com/gosimple/slug v1.9.0 - github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee + github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 github.com/grafana/grafana-plugin-sdk-go v0.90.0 diff --git a/go.sum b/go.sum index 8b91e70478d..25693b28d39 100644 --- a/go.sum +++ b/go.sum @@ -796,6 +796,10 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= +github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27 h1:DuyuEAHJeI+CMxIyzCVhmHcIeK+sjqberhDUfrgd3PY= +github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= +github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb h1:Hj25Whc/TRv0hSLm5VN0FJ5R4yZ6M4ycRcBgu7bsEAc= +github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8 h1:okhEX26LU7AGN/3C8NDWfdjBmKclvoFvJz9o/LsNcK8= github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee h1:jpZdUOta4PK3CH3+2UCuzqn1SGZ+dQj+dWH45B0c1aI= diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index 0810c149e98..f087af4d870 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -16,6 +16,7 @@ type FolderService interface { GetFolders(limit int64) ([]*models.Folder, error) GetFolderByID(id int64) (*models.Folder, error) GetFolderByUID(uid string) (*models.Folder, error) + GetFolderBySlug(slug string) (*models.Folder, error) CreateFolder(title, uid string) (*models.Folder, error) UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error DeleteFolder(uid string) (*models.Folder, error) @@ -96,6 +97,24 @@ func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, erro return dashToFolder(dashFolder), nil } +func (dr *dashboardServiceImpl) GetFolderBySlug(slug string) (*models.Folder, error) { + query := models.GetDashboardQuery{OrgId: dr.orgId, Slug: slug} + dashFolder, err := getFolder(query) + if err != nil { + return nil, toFolderError(err) + } + + g := guardian.New(dashFolder.Id, dr.orgId, dr.user) + if canView, err := g.CanView(); err != nil || !canView { + if err != nil { + return nil, toFolderError(err) + } + return nil, models.ErrFolderAccessDenied + } + + return dashToFolder(dashFolder), nil +} + func (dr *dashboardServiceImpl) CreateFolder(title, uid string) (*models.Folder, error) { dashFolder := models.NewDashboardFolder(title) dashFolder.OrgId = dr.orgId diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 5ff786512bf..71e47be9108 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -44,6 +44,7 @@ type API struct { DataService *tsdb.Service Schedule schedule.ScheduleService Store store.Store + RuleStore store.RuleStore AlertingStore store.AlertingStore DataProxy *datasourceproxy.DatasourceProxyService Alertmanager Alertmanager @@ -68,7 +69,7 @@ func (api *API) RegisterAPIEndpoints() { api.RegisterRulerApiEndpoints(NewForkedRuler( api.DatasourceCache, NewLotexRuler(proxy, logger), - RulerApiMock{log: logger}, + RulerSrv{store: api.RuleStore, log: logger}, )) api.RegisterTestingApiEndpoints(TestingApiMock{log: logger}) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go new file mode 100644 index 00000000000..ab23be265a8 --- /dev/null +++ b/pkg/services/ngalert/api/api_ruler.go @@ -0,0 +1,219 @@ +package api + +import ( + "fmt" + "net/http" + "time" + + "github.com/grafana/grafana/pkg/services/ngalert/store" + + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/util" + "github.com/prometheus/common/model" +) + +type RulerSrv struct { + store store.RuleStore + log log.Logger +} + +func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response { + namespace := c.Params(":Namespace") + namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) + if err != nil { + return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + } + if err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespaceUID); err != nil { + return response.Error(http.StatusInternalServerError, "failed to delete namespace alert rules", err) + } + return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"}) +} + +func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response { + namespace := c.Params(":Namespace") + namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) + if err != nil { + return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + } + ruleGroup := c.Params(":Groupname") + if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespaceUID, ruleGroup); err != nil { + return response.Error(http.StatusInternalServerError, "failed to delete group alert rules", err) + } + return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"}) +} + +func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response { + namespace := c.Params(":Namespace") + namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) + if err != nil { + return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + } + + q := ngmodels.ListNamespaceAlertRulesQuery{ + OrgID: c.SignedInUser.OrgId, + NamespaceUID: namespaceUID, + } + if err := srv.store.GetNamespaceAlertRules(&q); err != nil { + return response.Error(http.StatusInternalServerError, "failed to update rule group", err) + } + + result := apimodels.NamespaceConfigResponse{} + ruleGroupConfigs := make(map[string]apimodels.GettableRuleGroupConfig) + for _, r := range q.Result { + ruleGroupConfig, ok := ruleGroupConfigs[r.RuleGroup] + if !ok { + ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) + ruleGroupConfigs[r.RuleGroup] = apimodels.GettableRuleGroupConfig{ + Name: r.RuleGroup, + Interval: ruleGroupInterval, + Rules: []apimodels.GettableExtendedRuleNode{ + toGettableExtendedRuleNode(*r), + }, + } + } else { + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r)) + ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig + } + } + + for _, ruleGroupConfig := range ruleGroupConfigs { + result[namespace] = append(result[namespace], ruleGroupConfig) + } + + return response.JSON(http.StatusAccepted, result) +} + +func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { + namespace := c.Params(":Namespace") + namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) + if err != nil { + return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + } + + ruleGroup := c.Params(":Groupname") + q := ngmodels.ListRuleGroupAlertRulesQuery{ + OrgID: c.SignedInUser.OrgId, + NamespaceUID: namespaceUID, + RuleGroup: ruleGroup, + } + if err := srv.store.GetRuleGroupAlertRules(&q); err != nil { + return response.Error(http.StatusInternalServerError, "failed to get group alert rules", err) + } + + var ruleGroupInterval model.Duration + ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(q.Result)) + for _, r := range q.Result { + ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r)) + } + + result := apimodels.RuleGroupConfigResponse{ + GettableRuleGroupConfig: apimodels.GettableRuleGroupConfig{ + Name: ruleGroup, + Interval: ruleGroupInterval, + Rules: ruleNodes, + }, + } + return response.JSON(http.StatusAccepted, result) +} + +func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { + q := ngmodels.ListAlertRulesQuery{ + OrgID: c.SignedInUser.OrgId, + } + if err := srv.store.GetOrgAlertRules(&q); err != nil { + return response.Error(http.StatusInternalServerError, "failed to get alert rules", err) + } + + configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig) + for _, r := range q.Result { + namespace, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser) + if err != nil { + return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", r.NamespaceUID), err) + } + _, ok := configs[namespace] + if !ok { + ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) + configs[namespace] = make(map[string]apimodels.GettableRuleGroupConfig) + configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{ + Name: r.RuleGroup, + Interval: ruleGroupInterval, + Rules: []apimodels.GettableExtendedRuleNode{ + toGettableExtendedRuleNode(*r), + }, + } + } else { + ruleGroupConfig, ok := configs[namespace][r.RuleGroup] + if !ok { + ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) + configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{ + Name: r.RuleGroup, + Interval: ruleGroupInterval, + Rules: []apimodels.GettableExtendedRuleNode{ + toGettableExtendedRuleNode(*r), + }, + } + } else { + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r)) + configs[namespace][r.RuleGroup] = ruleGroupConfig + } + } + } + + result := apimodels.NamespaceConfigResponse{} + for namespace, m := range configs { + for _, ruleGroupConfig := range m { + result[namespace] = append(result[namespace], ruleGroupConfig) + } + } + return response.JSON(http.StatusAccepted, result) +} + +func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig) response.Response { + namespace := c.Params(":Namespace") + namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) + if err != nil { + return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + } + + // TODO check permissions + // TODO check quota + // TODO validate UID uniqueness in the payload + + ruleGroup := ruleGroupConfig.Name + + if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{ + OrgID: c.SignedInUser.OrgId, + NamespaceUID: namespaceUID, + RuleGroup: ruleGroup, + RuleGroupConfig: ruleGroupConfig, + }); err != nil { + return response.Error(http.StatusInternalServerError, "failed to update rule group", err) + } + + return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"}) +} + +func toGettableExtendedRuleNode(r ngmodels.AlertRule) apimodels.GettableExtendedRuleNode { + return apimodels.GettableExtendedRuleNode{ + GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ + ID: r.ID, + OrgID: r.OrgID, + Title: r.Title, + Condition: r.Condition, + Data: 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), + }, + } +} diff --git a/pkg/services/ngalert/api/api_ruler_base.go b/pkg/services/ngalert/api/api_ruler_base.go index 07c1769bbc0..c7c6e46b276 100644 --- a/pkg/services/ngalert/api/api_ruler_base.go +++ b/pkg/services/ngalert/api/api_ruler_base.go @@ -23,7 +23,7 @@ type RulerApiService interface { RouteGetNamespaceRulesConfig(*models.ReqContext) response.Response RouteGetRulegGroupConfig(*models.ReqContext) response.Response RouteGetRulesConfig(*models.ReqContext) response.Response - RoutePostNameRulesConfig(*models.ReqContext, apimodels.RuleGroupConfig) response.Response + RoutePostNameRulesConfig(*models.ReqContext, apimodels.PostableRuleGroupConfig) response.Response } type RulerApiBase struct { @@ -37,7 +37,7 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApiService) { group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), routing.Wrap(srv.RouteGetNamespaceRulesConfig)) group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname}"), routing.Wrap(srv.RouteGetRulegGroupConfig)) group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules"), routing.Wrap(srv.RouteGetRulesConfig)) - group.Post(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), binding.Bind(apimodels.RuleGroupConfig{}), routing.Wrap(srv.RoutePostNameRulesConfig)) + group.Post(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), binding.Bind(apimodels.PostableRuleGroupConfig{}), routing.Wrap(srv.RoutePostNameRulesConfig)) }) } @@ -83,7 +83,7 @@ func (base RulerApiBase) RouteGetRulesConfig(c *models.ReqContext) response.Resp return response.Error(http.StatusNotImplemented, "", nil) } -func (base RulerApiBase) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.RuleGroupConfig) response.Response { +func (base RulerApiBase) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.PostableRuleGroupConfig) response.Response { recipient := c.Params(":Recipient") base.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient) namespace := c.Params(":Namespace") diff --git a/pkg/services/ngalert/api/api_ruler_mock.go b/pkg/services/ngalert/api/api_ruler_mock.go deleted file mode 100644 index 1e78b56af89..00000000000 --- a/pkg/services/ngalert/api/api_ruler_mock.go +++ /dev/null @@ -1,295 +0,0 @@ -/*Package api contains mock API implementation of unified alerting - * - * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) - * - * Need to remove unused imports. - */ -package api - -import ( - "encoding/json" - "net/http" - "time" - - apimodels "github.com/grafana/alerting-api/pkg/api" - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/util" -) - -var prometheusAlert = []ngmodels.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasource": "gdev-prometheus", - "datasourceUid": "000000002", - "expr": "http_request_duration_microseconds_count", - "hide": false, - "interval": "", - "intervalMs": 1000, - "legendFormat": "", - "maxDataPoints": 100, - "refId": "query" - }`), - RefID: "query", - RelativeTimeRange: ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(time.Duration(5) * time.Hour), - To: ngmodels.Duration(time.Duration(3) * time.Hour), - }, - }, - { - Model: json.RawMessage(`{ - "datasource": "__expr__", - "datasourceUid": "-100", - "expression": "query", - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 100, - "reducer": "mean", - "refId": "reduced", - "type": "reduce" - }`), - RefID: "reduced", - RelativeTimeRange: ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(time.Duration(5) * time.Hour), - To: ngmodels.Duration(time.Duration(3) * time.Hour), - }, - }, - { - Model: json.RawMessage(`{ - "datasource": "__expr__", - "datasourceUid": "-100", - "expression": "$reduced > 10", - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 100, - "refId": "condition", - "type": "math" - }`), - RefID: "condition", - RelativeTimeRange: ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(time.Duration(5) * time.Hour), - To: ngmodels.Duration(time.Duration(3) * time.Hour), - }, - }, -} - -var testAlert = []ngmodels.AlertQuery{ - { - Model: json.RawMessage(`{ - "alias": "just-testing", - "datasource": "000000004", - "datasourceUid": "000000004", - "intervalMs": 1000, - "maxDataPoints": 100, - "orgId": 0, - "refId": "A", - "scenarioId": "csv_metric_values", - "stringInput": "1,20,90,30,5,0" - }`), - RefID: "A", - RelativeTimeRange: ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(time.Duration(5) * time.Hour), - To: ngmodels.Duration(time.Duration(3) * time.Hour), - }, - }, - { - Model: json.RawMessage(`{ - "datasource": "__expr__", - "datasourceUid": "__expr__", - "expression": "$A", - "intervalMs": 2000, - "maxDataPoints": 200, - "orgId": 0, - "reducer": "mean", - "refId": "B", - "type": "reduce" - }`), - RefID: "B", - RelativeTimeRange: ngmodels.RelativeTimeRange{ - From: ngmodels.Duration(time.Duration(5) * time.Hour), - To: ngmodels.Duration(time.Duration(3) * time.Hour), - }, - }, -} - -type RulerApiMock struct { - log log.Logger -} - -func (mock RulerApiMock) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteDeleteNamespaceRulesConfig: ", "Recipient", recipient) - namespace := c.Params(":Namespace") - mock.log.Info("RouteDeleteNamespaceRulesConfig: ", "Namespace", namespace) - return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"}) -} - -func (mock RulerApiMock) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteDeleteRuleGroupConfig: ", "Recipient", recipient) - namespace := c.Params(":Namespace") - mock.log.Info("RouteDeleteRuleGroupConfig: ", "Namespace", namespace) - groupname := c.Params(":Groupname") - mock.log.Info("RouteDeleteRuleGroupConfig: ", "Groupname", groupname) - return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"}) -} - -func (mock RulerApiMock) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetNamespaceRulesConfig: ", "Recipient", recipient) - namespace := c.Params(":Namespace") - mock.log.Info("RouteGetNamespaceRulesConfig: ", "Namespace", namespace) - result := apimodels.NamespaceConfigResponse{ - namespace: []apimodels.RuleGroupConfig{ - { - Name: "group1", - Interval: 60, - Rules: []apimodels.ExtendedRuleNode{ - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "rule 1-1", - Condition: "condition", - Data: prometheusAlert, - }, - }, - }, - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "rule 1-2", - Condition: "B", - Data: testAlert, - }, - }, - }, - }, - }, - }, - } - return response.JSON(http.StatusAccepted, result) -} - -func (mock RulerApiMock) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetRulegGroupConfig: ", "Recipient", recipient) - namespace := c.Params(":Namespace") - mock.log.Info("RouteGetRulegGroupConfig: ", "Namespace", namespace) - groupname := c.Params(":Groupname") - mock.log.Info("RouteGetRulegGroupConfig: ", "Groupname", groupname) - result := apimodels.RuleGroupConfigResponse{ - RuleGroupConfig: apimodels.RuleGroupConfig{ - Name: groupname, - Interval: 60, - Rules: []apimodels.ExtendedRuleNode{ - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "something completely different", - Condition: "A", - Data: testAlert, - }, - }, - }, - }, - }, - } - return response.JSON(http.StatusAccepted, result) -} - -func (mock RulerApiMock) RouteGetRulesConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetRulesConfig: ", "Recipient", recipient) - result := apimodels.NamespaceConfigResponse{ - "namespace1": []apimodels.RuleGroupConfig{ - { - Name: "group1", - Interval: 60, - Rules: []apimodels.ExtendedRuleNode{ - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "rule 1-1", - Condition: "A", - Data: testAlert, - }, - }, - }, - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "rule 1-2", - Condition: "A", - Data: testAlert, - }, - }, - }, - }, - }, - { - Name: "group2", - Interval: 60, - Rules: []apimodels.ExtendedRuleNode{ - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "rule 2-1", - Condition: "A", - Data: prometheusAlert, - }, - }, - }, - { - GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ - NoDataState: apimodels.NoData, - ExecutionErrorState: apimodels.AlertingErrState, - UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ - UID: "UID", - OrgID: 1, - Title: "rule 2-2", - Condition: "A", - Data: testAlert, - }, - }, - }, - }, - }, - }, - } - return response.JSON(http.StatusAccepted, result) -} - -func (mock RulerApiMock) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.RuleGroupConfig) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient) - namespace := c.Params(":Namespace") - mock.log.Info("RoutePostNameRulesConfig: ", "Namespace", namespace) - mock.log.Info("RoutePostNameRulesConfig: ", "body", body) - return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules created"}) -} diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/fork_ruler.go index a8cf29f459b..6f8ac48bf68 100644 --- a/pkg/services/ngalert/api/fork_ruler.go +++ b/pkg/services/ngalert/api/fork_ruler.go @@ -98,7 +98,7 @@ func (r *ForkedRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respo } } -func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response { +func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig) response.Response { backendType, err := backendType(ctx, r.DatasourceCache) if err != nil { return response.Error(400, err.Error(), nil) diff --git a/pkg/services/ngalert/api/lotex.go b/pkg/services/ngalert/api/lotex.go index b7ac8fc20d1..8aacbd447bd 100644 --- a/pkg/services/ngalert/api/lotex.go +++ b/pkg/services/ngalert/api/lotex.go @@ -133,7 +133,7 @@ func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respon ) } -func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response { +func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig) response.Response { legacyRulerPrefix, err := r.getPrefix(ctx) if err != nil { return response.Error(500, err.Error(), nil) diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101.json b/pkg/services/ngalert/api/test-data/post-rulegroup-101.json new file mode 100644 index 00000000000..e46833e1aa0 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101.json @@ -0,0 +1,120 @@ +{ + "name": "group101", + "interval": "10s", + "rules": [ + { + "grafana_alert": { + "title": "prom query with SSE - 2", + "condition": "condition", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "gdev-prometheus", + "datasourceUid": "000000002", + "expr": "http_request_duration_microseconds_count", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "$reduced > 10", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + }, + { + "grafana_alert": { + "title": "reduced testdata query - 2", + "condition": "B", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "alias": "just-testing", + "datasource": "000000004", + "datasourceUid": "000000004", + "intervalMs": 1000, + "maxDataPoints": 100, + "orgId": 0, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + }, + { + "refId": "B", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "__expr__", + "expression": "$A", + "intervalMs": 2000, + "maxDataPoints": 200, + "orgId": 0, + "reducer": "mean", + "refId": "B", + "type": "reduce" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + } + ] +} \ No newline at end of file diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-42.json b/pkg/services/ngalert/api/test-data/post-rulegroup-42.json new file mode 100644 index 00000000000..f16b3c3be71 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-42.json @@ -0,0 +1,120 @@ +{ + "name": "group42", + "interval": "10s", + "rules": [ + { + "grafana_alert": { + "title": "prom query with SSE", + "condition": "condition", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "gdev-prometheus", + "datasourceUid": "000000002", + "expr": "http_request_duration_microseconds_count", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "$reduced > 10", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + }, + { + "grafana_alert": { + "title": "reduced testdata query", + "condition": "B", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "alias": "just-testing", + "datasource": "000000004", + "datasourceUid": "000000004", + "intervalMs": 1000, + "maxDataPoints": 100, + "orgId": 0, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + }, + { + "refId": "B", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "__expr__", + "expression": "$A", + "intervalMs": 2000, + "maxDataPoints": 200, + "orgId": 0, + "reducer": "mean", + "refId": "B", + "type": "reduce" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + } + ] +} \ No newline at end of file diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-data.json b/pkg/services/ngalert/api/test-data/post-rulegroup-data.json deleted file mode 100644 index a1aadd6cf4d..00000000000 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-data.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "group42", - "interval": "10s", - "rules": [ - { - "expr": "", - "grafana_alert": { - "title": "something completely different", - "condition": "A", - "data": [ - { - "refId": "A", - "queryType": "", - "relativeTimeRange": { - "from": 18000, - "to": 10800 - }, - "model": { - "datasource": "__expr__", - "type": "math", - "expression": "2 + 2 > 1" - } - } - ], - "no_data_state": "NoData", - "exec_err_state": "Alerting" - } - } - ] -} \ No newline at end of file diff --git a/pkg/services/ngalert/api/test-data/ruler.http b/pkg/services/ngalert/api/test-data/ruler.http new file mode 100644 index 00000000000..576a012d6f7 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/ruler.http @@ -0,0 +1,246 @@ +@grafanaRecipient = grafana + +// should point to an existing folder named alerting +@namespace1 = alerting + +// create group42 under unknown namespace - it should fail +POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/unknown +content-type: application/json + +< ./post-rulegroup-42.json + +### +// create group42 +POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} +content-type: application/json + +< ./post-rulegroup-42.json + +### +// create group101 +POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} +content-type: application/json + +< ./post-rulegroup-101.json + +### +// get group42 rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 + +### +// get group101 rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group101 + +### +// get namespace rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} + +### +// get org rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules + +### +// delete group42 rules +DELETE http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 + +### +// get namespace rules - only group101 should be listed +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} + +### +// delete namespace rules +DELETE http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} + +### +// get namespace rules - no rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} + +### +// recreate group42 +POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} +content-type: application/json + +{ + "name": "group42", + "interval": "10s", + "rules": [ + { + "grafana_alert": { + "title": "prom query with SSE", + "condition": "condition", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "gdev-prometheus", + "datasourceUid": "000000002", + "expr": "http_request_duration_microseconds_count", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "$reduced > 10", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + } + ] +} + + +### +// get group42 rules +# @name getRule +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 + +### +@ruleUID = {{getRule.response.body.$.rules[0].grafana_alert.uid}} + +### +// update group42 interval and condition threshold + +POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} +content-type: application/json + +{ + "name": "group42", + "interval": "20s", + "rules": [ + { + "grafana_alert": { + "title": "prom query with SSE", + "condition": "condition", + "uid": "{{ruleUID}}", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "gdev-prometheus", + "datasourceUid": "000000002", + "expr": "http_request_duration_microseconds_count", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "$reduced > 42", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + } + ] +} + +### +// get group42 rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 + +### +// update group42 - delete all rules +POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} +content-type: application/json + +{ + "name": "group42", + "interval": "20s", + "rules": [] +} + +### +// get group42 rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 + +### +// get namespace rules +GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go new file mode 100644 index 00000000000..2098e48dd7b --- /dev/null +++ b/pkg/services/ngalert/models/alert_rule.go @@ -0,0 +1,155 @@ +package models + +import ( + "errors" + "fmt" + "time" +) + +var ( + // ErrAlertRuleNotFound is an error for an unknown alert rule. + ErrAlertRuleNotFound = fmt.Errorf("could not find alert rule") + // ErrAlertRuleFailedGenerateUniqueUID is an error for failure to generate alert rule UID + ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID") +) + +type NoDataState string + +func (noDataState NoDataState) String() string { + return string(noDataState) +} + +const ( + Alerting NoDataState = "Alerting" + NoData NoDataState = "NoData" + KeepLastState NoDataState = "KeepLastState" + OK NoDataState = "OK" +) + +type ExecutionErrorState string + +func (executionErrorState ExecutionErrorState) String() string { + return string(executionErrorState) +} + +const ( + AlertingErrState ExecutionErrorState = "Alerting" + KeepLastStateErrState ExecutionErrorState = "KeepLastState" +) + +// AlertRule is the model for alert rules in unified alerting. +type AlertRule struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + Title string + Condition string + Data []AlertQuery + Updated time.Time + IntervalSeconds int64 + Version int64 + UID string `xorm:"uid"` + NamespaceUID string `xorm:"namespace_uid"` + RuleGroup string + NoDataState NoDataState + ExecErrState ExecutionErrorState +} + +// AlertRuleKey is the alert definition identifier +type AlertRuleKey struct { + OrgID int64 + UID string +} + +func (k AlertRuleKey) String() string { + return fmt.Sprintf("{orgID: %d, UID: %s}", k.OrgID, k.UID) +} + +// GetKey returns the alert definitions identifier +func (alertRule *AlertRule) GetKey() AlertRuleKey { + return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID} +} + +// PreSave sets default values and loads the updated model for each alert query. +func (alertRule *AlertRule) PreSave(timeNow func() time.Time) error { + for i, q := range alertRule.Data { + err := q.PreSave() + if err != nil { + return fmt.Errorf("invalid alert query %s: %w", q.RefID, err) + } + alertRule.Data[i] = q + } + alertRule.Updated = timeNow() + return nil +} + +// AlertRuleVersion is the model for alert rule versions in unified alerting. +type AlertRuleVersion struct { + ID int64 `xorm:"pk autoincr 'id'"` + RuleOrgID int64 `xorm:"rule_org_id"` + RuleUID string `xorm:"rule_uid"` + RuleNamespaceUID string `xorm:"rule_namespace_uid"` + RuleGroup string + ParentVersion int64 + RestoredFrom int64 + Version int64 + + Created time.Time + Title string + Condition string + Data []AlertQuery + IntervalSeconds int64 + NoDataState NoDataState + ExecErrState ExecutionErrorState +} + +// GetAlertRuleByUIDQuery is the query for retrieving/deleting an alert rule by UID and organisation ID. +type GetAlertRuleByUIDQuery struct { + UID string + OrgID int64 + + Result *AlertRule +} + +// ListAlertRulesQuery is the query for listing alert rules +type ListAlertRulesQuery struct { + OrgID int64 + + Result []*AlertRule +} + +// ListNamespaceAlertRulesQuery is the query for listing namespace alert rules +type ListNamespaceAlertRulesQuery struct { + OrgID int64 + // Namespace is the folder slug + NamespaceUID string + + Result []*AlertRule +} + +// ListRuleGroupAlertRulesQuery is the query for listing rule group alert rules +type ListRuleGroupAlertRulesQuery struct { + OrgID int64 + // Namespace is the folder slug + NamespaceUID string + RuleGroup string + + Result []*AlertRule +} + +// Condition contains backend expressions and queries and the RefID +// of the query or expression that will be evaluated. +type Condition struct { + // Condition is the RefID of the query or expression from + // the Data property to get the results for. + Condition string `json:"condition"` + OrgID int64 `json:"-"` + + // Data is an array of data source queries and/or server side expressions. + Data []AlertQuery `json:"data"` +} + +// IsValid checks the condition's validity. +func (c Condition) IsValid() bool { + // TODO search for refIDs in QueriesAndExpressions + return len(c.Data) != 0 +} diff --git a/pkg/services/ngalert/models/models.go b/pkg/services/ngalert/models/legacy.go similarity index 87% rename from pkg/services/ngalert/models/models.go rename to pkg/services/ngalert/models/legacy.go index 8288b7bc394..75dd686b5a0 100644 --- a/pkg/services/ngalert/models/models.go +++ b/pkg/services/ngalert/models/legacy.go @@ -14,6 +14,7 @@ var ( ) // AlertDefinition is the model for alert definitions in Alerting NG. +// Legacy model; It will be removed in v8 type AlertDefinition struct { ID int64 `xorm:"pk autoincr 'id'" json:"id"` OrgID int64 `xorm:"org_id" json:"orgId"` @@ -56,6 +57,7 @@ func (alertDefinition *AlertDefinition) PreSave(timeNow func() time.Time) error } // AlertDefinitionVersion is the model for alert definition versions in Alerting NG. +// Legacy model; It will be removed in v8 type AlertDefinitionVersion struct { ID int64 `xorm:"pk autoincr 'id'"` AlertDefinitionID int64 `xorm:"alert_definition_id"` @@ -72,6 +74,7 @@ type AlertDefinitionVersion struct { } // GetAlertDefinitionByUIDQuery is the query for retrieving/deleting an alert definition by UID and organisation ID. +// Legacy model; It will be removed in v8 type GetAlertDefinitionByUIDQuery struct { UID string OrgID int64 @@ -80,12 +83,14 @@ type GetAlertDefinitionByUIDQuery struct { } // DeleteAlertDefinitionByUIDCommand is the command for deleting an alert definition +// Legacy model; It will be removed in v8 type DeleteAlertDefinitionByUIDCommand struct { UID string OrgID int64 } // SaveAlertDefinitionCommand is the query for saving a new alert definition. +// Legacy model; It will be removed in v8 type SaveAlertDefinitionCommand struct { Title string `json:"title"` OrgID int64 `json:"-"` @@ -97,6 +102,7 @@ type SaveAlertDefinitionCommand struct { } // UpdateAlertDefinitionCommand is the query for updating an existing alert definition. +// Legacy model; It will be removed in v8 type UpdateAlertDefinitionCommand struct { Title string `json:"title"` OrgID int64 `json:"-"` @@ -108,14 +114,8 @@ type UpdateAlertDefinitionCommand struct { Result *AlertDefinition } -// EvalAlertConditionCommand is the command for evaluating a condition -type EvalAlertConditionCommand struct { - Condition string `json:"condition"` - Data []AlertQuery `json:"data"` - Now time.Time `json:"now"` -} - // ListAlertDefinitionsQuery is the query for listing alert definitions +// Legacy model; It will be removed in v8 type ListAlertDefinitionsQuery struct { OrgID int64 `json:"-"` @@ -123,6 +123,7 @@ type ListAlertDefinitionsQuery struct { } // UpdateAlertDefinitionPausedCommand is the command for updating an alert definitions +// Legacy model; It will be removed in v8 type UpdateAlertDefinitionPausedCommand struct { OrgID int64 `json:"-"` UIDs []string `json:"uids"` @@ -131,20 +132,10 @@ type UpdateAlertDefinitionPausedCommand struct { ResultCount int64 } -// Condition contains backend expressions and queries and the RefID -// of the query or expression that will be evaluated. -type Condition struct { - // Condition is the RefID of the query or expression from - // the Data property to get the results for. - Condition string `json:"condition"` - OrgID int64 `json:"-"` - - // Data is an array of data source queries and/or server side expressions. - Data []AlertQuery `json:"data"` -} - -// IsValid checks the condition's validity. -func (c Condition) IsValid() bool { - // TODO search for refIDs in QueriesAndExpressions - return len(c.Data) != 0 +// EvalAlertConditionCommand is the command for evaluating a condition +// Legacy model; It will be removed in v8 +type EvalAlertConditionCommand struct { + Condition string `json:"condition"` + Data []AlertQuery `json:"data"` + Now time.Time `json:"now"` } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 325cc3dd9fd..276b4734c03 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -81,6 +81,7 @@ func (ng *AlertNG) Init() error { Schedule: ng.schedule, DataProxy: ng.DataProxy, Store: store, + RuleStore: store, AlertingStore: store, Alertmanager: ng.Alertmanager, } @@ -114,6 +115,10 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) { // Create alert_instance table store.AlertInstanceMigration(mg) + // Create alert_rule + store.AddAlertRuleMigrations(mg, defaultIntervalSeconds) + store.AddAlertRuleVersionMigrations(mg) + // Create silence table store.SilenceMigration(mg) } diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go new file mode 100644 index 00000000000..e891e31fdd4 --- /dev/null +++ b/pkg/services/ngalert/store/alert_rule.go @@ -0,0 +1,450 @@ +package store + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/models" + + "github.com/grafana/grafana/pkg/services/dashboards" + + apimodels "github.com/grafana/alerting-api/pkg/api" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" +) + +// AlertRuleMaxTitleLength is the maximum length of the alert rule title +const AlertRuleMaxTitleLength = 190 + +// AlertRuleMaxRuleGroupNameLength is the maximum length of the alert rule group name +const AlertRuleMaxRuleGroupNameLength = 190 + +type UpdateRuleGroupCmd struct { + OrgID int64 + NamespaceUID string + RuleGroup string + RuleGroupConfig apimodels.PostableRuleGroupConfig +} + +type UpsertRule struct { + Existing *ngmodels.AlertRule + New ngmodels.AlertRule +} + +// Store is the interface for persisting alert rules and instances +type RuleStore interface { + DeleteAlertRuleByUID(orgID int64, ruleUID string) error + DeleteNamespaceAlertRules(orgID int64, namespaceUID string) error + DeleteRuleGroupAlertRules(orgID int64, namespaceUID string, ruleGroup string) error + GetAlertRuleByUID(*ngmodels.GetAlertRuleByUIDQuery) error + GetAlertRules(query *ngmodels.ListAlertRulesQuery) error + GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error + GetNamespaceAlertRules(query *ngmodels.ListNamespaceAlertRulesQuery) error + GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRulesQuery) error + GetNamespaceUIDBySlug(string, int64, *models.SignedInUser) (string, error) + GetNamespaceByUID(string, int64, *models.SignedInUser) (string, error) + UpsertAlertRules([]UpsertRule) error + UpdateRuleGroup(UpdateRuleGroupCmd) error + GetAlertInstance(*ngmodels.GetAlertInstanceQuery) error + ListAlertInstances(cmd *ngmodels.ListAlertInstancesQuery) error + SaveAlertInstance(cmd *ngmodels.SaveAlertInstanceCommand) error + ValidateAlertRule(ngmodels.AlertRule, bool) error +} + +func getAlertRuleByUID(sess *sqlstore.DBSession, alertRuleUID string, orgID int64) (*ngmodels.AlertRule, error) { + // we consider optionally enabling some caching + alertRule := ngmodels.AlertRule{OrgID: orgID, UID: alertRuleUID} + has, err := sess.Get(&alertRule) + if err != nil { + return nil, err + } + if !has { + return nil, ngmodels.ErrAlertRuleNotFound + } + return &alertRule, nil +} + +// DeleteAlertRuleByUID is a handler for deleting an alert rule. +// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID. +func (st DBstore) DeleteAlertRuleByUID(orgID int64, ruleUID string) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? AND uid = ?", orgID, ruleUID) + if err != nil { + return err + } + + _, err = sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_uid = ?", orgID, ruleUID) + + if err != nil { + return err + } + + _, err = sess.Exec("DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid = ?", orgID, ruleUID) + if err != nil { + return err + } + return nil + }) +} + +// DeleteNamespaceAlertRules is a handler for deleting namespace alert rules. +func (st DBstore) DeleteNamespaceAlertRules(orgID int64, namespaceUID string) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ?", orgID, namespaceUID); err != nil { + return err + } + + if _, err := sess.Exec(`DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid NOT IN ( + SELECT uid FROM alert_rule where org_id = ? + )`, orgID, orgID); err != nil { + return err + } + + return nil + }) +} + +// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules. +func (st DBstore) DeleteRuleGroupAlertRules(orgID int64, namespaceUID string, ruleGroup string) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil { + return err + } + + if _, err := sess.Exec(`DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid NOT IN ( + SELECT uid FROM alert_rule where org_id = ? + )`, orgID, orgID); err != nil { + return err + } + + return nil + }) +} + +// GetAlertRuleByUID is a handler for retrieving an alert rule from that database by its UID and organisation ID. +// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID. +func (st DBstore) GetAlertRuleByUID(query *ngmodels.GetAlertRuleByUIDQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + alertRule, err := getAlertRuleByUID(sess, query.UID, query.OrgID) + if err != nil { + return err + } + query.Result = alertRule + return nil + }) +} + +// UpsertAlertRules is a handler for creating/updating alert rules. +func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + newRules := make([]ngmodels.AlertRule, 0, len(rules)) + ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) + for _, r := range rules { + if r.Existing == nil && r.New.UID != "" { + // check by UID + existingAlertRule, err := getAlertRuleByUID(sess, r.New.UID, r.New.OrgID) + if err != nil { + if errors.Is(err, ngmodels.ErrAlertRuleNotFound) { + return nil + } + return err + } + r.Existing = existingAlertRule + } + + var parentVersion int64 + switch r.Existing { + case nil: // new rule + uid, err := generateNewAlertRuleUID(sess, r.New.OrgID) + if err != nil { + return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.New.Title, err) + } + r.New.UID = uid + + if r.New.IntervalSeconds == 0 { + r.New.IntervalSeconds = st.DefaultIntervalSeconds + } + + r.New.Version = 1 + + if err := st.ValidateAlertRule(r.New, true); err != nil { + return err + } + + if err := (&r.New).PreSave(TimeNow); err != nil { + return err + } + + newRules = append(newRules, r.New) + default: + // explicitly set the existing properties if missing + // do not rely on xorm + if r.New.Title == "" { + r.New.Title = r.Existing.Title + } + + if r.New.Condition == "" { + r.New.Condition = r.Existing.Condition + } + + if len(r.New.Data) == 0 { + r.New.Data = r.Existing.Data + } + + if r.New.IntervalSeconds == 0 { + r.New.IntervalSeconds = r.Existing.IntervalSeconds + } + + r.New.ID = r.Existing.ID + r.New.OrgID = r.Existing.OrgID + r.New.NamespaceUID = r.Existing.NamespaceUID + r.New.RuleGroup = r.Existing.RuleGroup + r.New.Version = r.Existing.Version + 1 + + if err := st.ValidateAlertRule(r.New, true); err != nil { + return err + } + + if err := (&r.New).PreSave(TimeNow); err != nil { + return err + } + + // no way to update multiple rules at once + if _, err := sess.ID(r.Existing.ID).Update(r.New); err != nil { + return fmt.Errorf("failed to update rule %s: %w", r.New.Title, err) + } + + parentVersion = r.Existing.Version + } + + ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{ + RuleOrgID: r.New.OrgID, + RuleUID: r.New.UID, + RuleNamespaceUID: r.New.NamespaceUID, + RuleGroup: r.New.RuleGroup, + ParentVersion: parentVersion, + Version: r.New.Version, + Created: r.New.Updated, + Condition: r.New.Condition, + Title: r.New.Title, + Data: r.New.Data, + IntervalSeconds: r.New.IntervalSeconds, + NoDataState: r.New.NoDataState, + ExecErrState: r.New.ExecErrState, + }) + } + + if len(newRules) > 0 { + if _, err := sess.Insert(&newRules); err != nil { + return fmt.Errorf("failed to create new rules: %w", err) + } + } + + if len(ruleVersions) > 0 { + if _, err := sess.Insert(&ruleVersions); err != nil { + return fmt.Errorf("failed to create new rule versions: %w", err) + } + } + + return nil + }) +} + +// GetOrgAlertRules is a handler for retrieving alert rules of specific organisation. +func (st DBstore) GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + alertRules := make([]*ngmodels.AlertRule, 0) + q := "SELECT * FROM alert_rule WHERE org_id = ?" + if err := sess.SQL(q, query.OrgID).Find(&alertRules); err != nil { + return err + } + + query.Result = alertRules + return nil + }) +} + +// GetNamespaceAlertRules is a handler for retrieving namespace alert rules of specific organisation. +func (st DBstore) GetNamespaceAlertRules(query *ngmodels.ListNamespaceAlertRulesQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + alertRules := make([]*ngmodels.AlertRule, 0) + // TODO rewrite using group by namespace_uid, rule_group + q := "SELECT * FROM alert_rule WHERE org_id = ? and namespace_uid = ?" + if err := sess.SQL(q, query.OrgID, query.NamespaceUID).Find(&alertRules); err != nil { + return err + } + + query.Result = alertRules + return nil + }) +} + +// GetRuleGroupAlertRules is a handler for retrieving rule group alert rules of specific organisation. +func (st DBstore) GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRulesQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + alertRules := make([]*ngmodels.AlertRule, 0) + q := "SELECT * FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?" + if err := sess.SQL(q, query.OrgID, query.NamespaceUID, query.RuleGroup).Find(&alertRules); err != nil { + return err + } + + query.Result = alertRules + return nil + }) +} + +// GetNamespaceUIDBySlug is a handler for retrieving namespace UID by its name. +func (st DBstore) GetNamespaceUIDBySlug(namespace string, orgID int64, user *models.SignedInUser) (string, error) { + s := dashboards.NewFolderService(orgID, user, st.SQLStore) + folder, err := s.GetFolderBySlug(namespace) + if err != nil { + return "", err + } + return folder.Uid, nil +} + +// GetNamespaceByUID is a handler for retrieving namespace by its UID. +func (st DBstore) GetNamespaceByUID(UID string, orgID int64, user *models.SignedInUser) (string, error) { + s := dashboards.NewFolderService(orgID, user, st.SQLStore) + folder, err := s.GetFolderByUID(UID) + if err != nil { + return "", err + } + return folder.Title, nil +} + +// GetAlertRules returns alert rule identifier, interval, version and pause state +// that are useful for it's scheduling. +func (st DBstore) GetAlertRules(query *ngmodels.ListAlertRulesQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + alerts := make([]*ngmodels.AlertRule, 0) + q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_rule" + if err := sess.SQL(q).Find(&alerts); err != nil { + return err + } + + query.Result = alerts + return nil + }) +} + +func generateNewAlertRuleUID(sess *sqlstore.DBSession, orgID int64) (string, error) { + for i := 0; i < 3; i++ { + uid := util.GenerateShortUID() + + exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&ngmodels.AlertRule{}) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", ngmodels.ErrAlertRuleFailedGenerateUniqueUID +} + +// ValidateAlertRule validates the alert rule interval and organisation. +// If requireData is true checks that it contains at least one alert query +func (st DBstore) ValidateAlertRule(alertRule ngmodels.AlertRule, requireData bool) error { + if !requireData && len(alertRule.Data) == 0 { + return fmt.Errorf("no queries or expressions are found") + } + + if alertRule.Title == "" { + return ErrEmptyTitleError + } + + if alertRule.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 { + return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertRule.IntervalSeconds)*time.Second, st.BaseInterval) + } + + // enfore max name length in SQLite + if len(alertRule.Title) > AlertRuleMaxTitleLength { + return fmt.Errorf("name length should not be greater than %d", AlertRuleMaxTitleLength) + } + + // enfore max name length in SQLite + if len(alertRule.RuleGroup) > AlertRuleMaxRuleGroupNameLength { + return fmt.Errorf("name length should not be greater than %d", AlertRuleMaxRuleGroupNameLength) + } + + if alertRule.OrgID == 0 { + return fmt.Errorf("no organisation is found") + } + + return nil +} + +// UpdateRuleGroup creates new rules and updates and/or deletes existing rules +func (st DBstore) UpdateRuleGroup(cmd UpdateRuleGroupCmd) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + q := &ngmodels.ListRuleGroupAlertRulesQuery{ + OrgID: cmd.OrgID, + NamespaceUID: cmd.NamespaceUID, + RuleGroup: cmd.RuleGroup, + } + if err := st.GetRuleGroupAlertRules(q); err != nil { + return err + } + existingGroupRules := q.Result + + existingGroupRulesUIDs := make(map[string]ngmodels.AlertRule, len(existingGroupRules)) + for _, r := range existingGroupRules { + existingGroupRulesUIDs[r.UID] = *r + } + + upsertRules := make([]UpsertRule, 0) + for _, r := range cmd.RuleGroupConfig.Rules { + if r.GrafanaManagedAlert == nil { + continue + } + + upsertRule := UpsertRule{ + New: ngmodels.AlertRule{ + OrgID: cmd.OrgID, + Title: r.GrafanaManagedAlert.Title, + Condition: r.GrafanaManagedAlert.Condition, + Data: r.GrafanaManagedAlert.Data, + UID: r.GrafanaManagedAlert.UID, + IntervalSeconds: int64(time.Duration(cmd.RuleGroupConfig.Interval).Seconds()), + NamespaceUID: cmd.NamespaceUID, + RuleGroup: cmd.RuleGroup, + NoDataState: ngmodels.NoDataState(r.GrafanaManagedAlert.NoDataState), + ExecErrState: ngmodels.ExecutionErrorState(r.GrafanaManagedAlert.ExecErrState), + }, + } + + if existingGroupRule, ok := existingGroupRulesUIDs[r.GrafanaManagedAlert.UID]; ok { + upsertRule.Existing = &existingGroupRule + // remove the rule from existingGroupRulesUIDs + delete(existingGroupRulesUIDs, r.GrafanaManagedAlert.UID) + } + upsertRules = append(upsertRules, upsertRule) + } + + if err := st.UpsertAlertRules(upsertRules); err != nil { + return err + } + + // delete the remaining rules + for ruleUID := range existingGroupRulesUIDs { + if err := st.DeleteAlertRuleByUID(cmd.OrgID, ruleUID); err != nil { + return err + } + } + return nil + }) +} diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 4d3146fd301..474225718f1 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -16,7 +16,7 @@ import ( // TimeNow makes it possible to test usage of time var TimeNow = time.Now -// AlertDefinitionMaxTitleLength is the maximum length of the alert definition titles +// AlertDefinitionMaxTitleLength is the maximum length of the alert definition title const AlertDefinitionMaxTitleLength = 190 // ErrEmptyTitleError is an error returned if the alert definition title is empty diff --git a/pkg/services/ngalert/store/database_mig.go b/pkg/services/ngalert/store/database_mig.go index a44afba3a77..23aa3887e5e 100644 --- a/pkg/services/ngalert/store/database_mig.go +++ b/pkg/services/ngalert/store/database_mig.go @@ -3,6 +3,7 @@ package store import ( "fmt" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -108,6 +109,77 @@ func AlertInstanceMigration(mg *migrator.Migrator) { mg.AddMigration("add index in alert_instance table on def_org_id, current_state columns", migrator.NewAddIndexMigration(alertInstance, alertInstance.Indices[1])) } +func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { + alertRule := migrator.Table{ + Name: "alert_rule", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "title", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "condition", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "data", Type: migrator.DB_Text, Nullable: false}, + {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "interval_seconds", Type: migrator.DB_BigInt, Nullable: false, Default: fmt.Sprintf("%d", defaultIntervalSeconds)}, + {Name: "version", Type: migrator.DB_Int, Nullable: false, Default: "0"}, + {Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false, Default: "0"}, + // the following fields will correspond to a dashboard (or folder) UIID + {Name: "namespace_uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, + {Name: "rule_group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "no_data_state", Type: migrator.DB_NVarchar, Length: 15, Nullable: false, Default: fmt.Sprintf("'%s'", models.NoData.String())}, + {Name: "exec_err_state", Type: migrator.DB_NVarchar, Length: 15, Nullable: false, Default: fmt.Sprintf("'%s'", models.AlertingErrState.String())}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id", "title"}, Type: migrator.UniqueIndex}, + {Cols: []string{"org_id", "uid"}, Type: migrator.UniqueIndex}, + {Cols: []string{"org_id", "namespace_uid", "rule_group"}, Type: migrator.IndexType}, + }, + } + // create table + mg.AddMigration("create alert_rule table", migrator.NewAddTableMigration(alertRule)) + + // create indices + mg.AddMigration("add index in alert_rule on org_id and title columns", migrator.NewAddIndexMigration(alertRule, alertRule.Indices[0])) + mg.AddMigration("add index in alert_rule on org_id and uid columns", migrator.NewAddIndexMigration(alertRule, alertRule.Indices[1])) + mg.AddMigration("add index in alert_rule on org_id, namespace_uid, group_uid columns", migrator.NewAddIndexMigration(alertRule, alertRule.Indices[2])) + + mg.AddMigration("alter alert_rule table data column to mediumtext in mysql", migrator.NewRawSQLMigration(""). + Mysql("ALTER TABLE alert_rule MODIFY data MEDIUMTEXT;")) +} + +func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { + alertRuleVersion := migrator.Table{ + Name: "alert_rule_version", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "rule_org_id", Type: migrator.DB_BigInt}, + {Name: "rule_uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false, Default: "0"}, + // the following fields will correspond to a dashboard (or folder) UID + {Name: "rule_namespace_uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, + {Name: "rule_group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "parent_version", Type: migrator.DB_Int, Nullable: false}, + {Name: "restored_from", Type: migrator.DB_Int, Nullable: false}, + {Name: "version", Type: migrator.DB_Int, Nullable: false}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "title", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "condition", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "data", Type: migrator.DB_Text, Nullable: false}, + {Name: "interval_seconds", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "no_data_state", Type: migrator.DB_NVarchar, Length: 15, Nullable: false, Default: fmt.Sprintf("'%s'", models.NoData.String())}, + {Name: "exec_err_state", Type: migrator.DB_NVarchar, Length: 15, Nullable: false, Default: fmt.Sprintf("'%s'", models.AlertingErrState.String())}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"rule_org_id", "rule_uid", "version"}, Type: migrator.UniqueIndex}, + {Cols: []string{"rule_org_id", "rule_namespace_uid", "rule_group"}, Type: migrator.IndexType}, + }, + } + mg.AddMigration("create alert_rule_version table", migrator.NewAddTableMigration(alertRuleVersion)) + mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_uid and version columns", migrator.NewAddIndexMigration(alertRuleVersion, alertRuleVersion.Indices[0])) + mg.AddMigration("add index in alert_rule_version table on rule_org_id, rule_namespace_uid and rule_group columns", migrator.NewAddIndexMigration(alertRuleVersion, alertRuleVersion.Indices[1])) + + mg.AddMigration("alter alert_rule_version table data column to mediumtext in mysql", migrator.NewRawSQLMigration(""). + Mysql("ALTER TABLE alert_rule_version MODIFY data MEDIUMTEXT;")) +} + func SilenceMigration(mg *migrator.Migrator) { silence := migrator.Table{ Name: "silence",