[Alerting]: Grafana managed ruler API implementation (#32537)

* [Alerting]: Grafana managed ruler API impl

* Apply suggestions from code review

* fix lint

* Add validation for ruleGroup name length

* Fix MySQL migration

Co-authored-by: kyle <kyle@grafana.com>
pull/32672/head
Sofia Papagiannaki 4 years ago committed by GitHub
parent e499585271
commit ee06970d72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      go.mod
  2. 4
      go.sum
  3. 19
      pkg/services/dashboards/folder_service.go
  4. 3
      pkg/services/ngalert/api/api.go
  5. 219
      pkg/services/ngalert/api/api_ruler.go
  6. 6
      pkg/services/ngalert/api/api_ruler_base.go
  7. 295
      pkg/services/ngalert/api/api_ruler_mock.go
  8. 2
      pkg/services/ngalert/api/fork_ruler.go
  9. 2
      pkg/services/ngalert/api/lotex.go
  10. 120
      pkg/services/ngalert/api/test-data/post-rulegroup-101.json
  11. 120
      pkg/services/ngalert/api/test-data/post-rulegroup-42.json
  12. 30
      pkg/services/ngalert/api/test-data/post-rulegroup-data.json
  13. 246
      pkg/services/ngalert/api/test-data/ruler.http
  14. 155
      pkg/services/ngalert/models/alert_rule.go
  15. 37
      pkg/services/ngalert/models/legacy.go
  16. 5
      pkg/services/ngalert/ngalert.go
  17. 450
      pkg/services/ngalert/store/alert_rule.go
  18. 2
      pkg/services/ngalert/store/database.go
  19. 72
      pkg/services/ngalert/store/database_mig.go

@ -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

@ -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=

@ -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

@ -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})

@ -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),
},
}
}

@ -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")

@ -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"})
}

@ -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)

@ -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)

@ -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"
}
}
]
}

@ -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"
}
}
]
}

@ -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"
}
}
]
}

@ -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}}

@ -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
}

@ -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"`
}

@ -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)
}

@ -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
})
}

@ -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

@ -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",

Loading…
Cancel
Save