mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[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>
This commit is contained in:
parent
e499585271
commit
ee06970d72
2
go.mod
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
@ -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})
|
||||
|
||||
|
219
pkg/services/ngalert/api/api_ruler.go
Normal file
219
pkg/services/ngalert/api/api_ruler.go
Normal file
@ -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)
|
||||
|
120
pkg/services/ngalert/api/test-data/post-rulegroup-101.json
Normal file
120
pkg/services/ngalert/api/test-data/post-rulegroup-101.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
120
pkg/services/ngalert/api/test-data/post-rulegroup-42.json
Normal file
120
pkg/services/ngalert/api/test-data/post-rulegroup-42.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
246
pkg/services/ngalert/api/test-data/ruler.http
Normal file
246
pkg/services/ngalert/api/test-data/ruler.http
Normal file
@ -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}}
|
155
pkg/services/ngalert/models/alert_rule.go
Normal file
155
pkg/services/ngalert/models/alert_rule.go
Normal file
@ -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.
|
||||
// EvalAlertConditionCommand is the command for evaluating a condition
|
||||
// Legacy model; It will be removed in v8
|
||||
type EvalAlertConditionCommand struct {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
450
pkg/services/ngalert/store/alert_rule.go
Normal file
450
pkg/services/ngalert/store/alert_rule.go
Normal file
@ -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…
Reference in New Issue
Block a user