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/go-cmp v0.5.5
|
||||||
github.com/google/uuid v1.2.0
|
github.com/google/uuid v1.2.0
|
||||||
github.com/gosimple/slug v1.9.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-aws-sdk v0.4.0
|
||||||
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
|
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.90.0
|
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/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 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
|
||||||
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
|
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 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-20210330162237-0b5408c529a8/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY=
|
||||||
github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee h1:jpZdUOta4PK3CH3+2UCuzqn1SGZ+dQj+dWH45B0c1aI=
|
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)
|
GetFolders(limit int64) ([]*models.Folder, error)
|
||||||
GetFolderByID(id int64) (*models.Folder, error)
|
GetFolderByID(id int64) (*models.Folder, error)
|
||||||
GetFolderByUID(uid string) (*models.Folder, error)
|
GetFolderByUID(uid string) (*models.Folder, error)
|
||||||
|
GetFolderBySlug(slug string) (*models.Folder, error)
|
||||||
CreateFolder(title, uid string) (*models.Folder, error)
|
CreateFolder(title, uid string) (*models.Folder, error)
|
||||||
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
|
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
|
||||||
DeleteFolder(uid string) (*models.Folder, error)
|
DeleteFolder(uid string) (*models.Folder, error)
|
||||||
@ -96,6 +97,24 @@ func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, erro
|
|||||||
return dashToFolder(dashFolder), nil
|
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) {
|
func (dr *dashboardServiceImpl) CreateFolder(title, uid string) (*models.Folder, error) {
|
||||||
dashFolder := models.NewDashboardFolder(title)
|
dashFolder := models.NewDashboardFolder(title)
|
||||||
dashFolder.OrgId = dr.orgId
|
dashFolder.OrgId = dr.orgId
|
||||||
|
@ -44,6 +44,7 @@ type API struct {
|
|||||||
DataService *tsdb.Service
|
DataService *tsdb.Service
|
||||||
Schedule schedule.ScheduleService
|
Schedule schedule.ScheduleService
|
||||||
Store store.Store
|
Store store.Store
|
||||||
|
RuleStore store.RuleStore
|
||||||
AlertingStore store.AlertingStore
|
AlertingStore store.AlertingStore
|
||||||
DataProxy *datasourceproxy.DatasourceProxyService
|
DataProxy *datasourceproxy.DatasourceProxyService
|
||||||
Alertmanager Alertmanager
|
Alertmanager Alertmanager
|
||||||
@ -68,7 +69,7 @@ func (api *API) RegisterAPIEndpoints() {
|
|||||||
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
||||||
api.DatasourceCache,
|
api.DatasourceCache,
|
||||||
NewLotexRuler(proxy, logger),
|
NewLotexRuler(proxy, logger),
|
||||||
RulerApiMock{log: logger},
|
RulerSrv{store: api.RuleStore, log: logger},
|
||||||
))
|
))
|
||||||
api.RegisterTestingApiEndpoints(TestingApiMock{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
|
RouteGetNamespaceRulesConfig(*models.ReqContext) response.Response
|
||||||
RouteGetRulegGroupConfig(*models.ReqContext) response.Response
|
RouteGetRulegGroupConfig(*models.ReqContext) response.Response
|
||||||
RouteGetRulesConfig(*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 {
|
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}"), 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/{Namespace}/{Groupname}"), routing.Wrap(srv.RouteGetRulegGroupConfig))
|
||||||
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules"), routing.Wrap(srv.RouteGetRulesConfig))
|
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)
|
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")
|
recipient := c.Params(":Recipient")
|
||||||
base.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient)
|
base.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient)
|
||||||
namespace := c.Params(":Namespace")
|
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)
|
backendType, err := backendType(ctx, r.DatasourceCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.Error(400, err.Error(), 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)
|
legacyRulerPrefix, err := r.getPrefix(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.Error(500, err.Error(), 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.
|
// AlertDefinition is the model for alert definitions in Alerting NG.
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type AlertDefinition struct {
|
type AlertDefinition struct {
|
||||||
ID int64 `xorm:"pk autoincr 'id'" json:"id"`
|
ID int64 `xorm:"pk autoincr 'id'" json:"id"`
|
||||||
OrgID int64 `xorm:"org_id" json:"orgId"`
|
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.
|
// AlertDefinitionVersion is the model for alert definition versions in Alerting NG.
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type AlertDefinitionVersion struct {
|
type AlertDefinitionVersion struct {
|
||||||
ID int64 `xorm:"pk autoincr 'id'"`
|
ID int64 `xorm:"pk autoincr 'id'"`
|
||||||
AlertDefinitionID int64 `xorm:"alert_definition_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.
|
// 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 {
|
type GetAlertDefinitionByUIDQuery struct {
|
||||||
UID string
|
UID string
|
||||||
OrgID int64
|
OrgID int64
|
||||||
@ -80,12 +83,14 @@ type GetAlertDefinitionByUIDQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAlertDefinitionByUIDCommand is the command for deleting an alert definition
|
// DeleteAlertDefinitionByUIDCommand is the command for deleting an alert definition
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type DeleteAlertDefinitionByUIDCommand struct {
|
type DeleteAlertDefinitionByUIDCommand struct {
|
||||||
UID string
|
UID string
|
||||||
OrgID int64
|
OrgID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAlertDefinitionCommand is the query for saving a new alert definition.
|
// SaveAlertDefinitionCommand is the query for saving a new alert definition.
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type SaveAlertDefinitionCommand struct {
|
type SaveAlertDefinitionCommand struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
OrgID int64 `json:"-"`
|
OrgID int64 `json:"-"`
|
||||||
@ -97,6 +102,7 @@ type SaveAlertDefinitionCommand struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAlertDefinitionCommand is the query for updating an existing alert definition.
|
// UpdateAlertDefinitionCommand is the query for updating an existing alert definition.
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type UpdateAlertDefinitionCommand struct {
|
type UpdateAlertDefinitionCommand struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
OrgID int64 `json:"-"`
|
OrgID int64 `json:"-"`
|
||||||
@ -108,14 +114,8 @@ type UpdateAlertDefinitionCommand struct {
|
|||||||
Result *AlertDefinition
|
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
|
// ListAlertDefinitionsQuery is the query for listing alert definitions
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type ListAlertDefinitionsQuery struct {
|
type ListAlertDefinitionsQuery struct {
|
||||||
OrgID int64 `json:"-"`
|
OrgID int64 `json:"-"`
|
||||||
|
|
||||||
@ -123,6 +123,7 @@ type ListAlertDefinitionsQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAlertDefinitionPausedCommand is the command for updating an alert definitions
|
// UpdateAlertDefinitionPausedCommand is the command for updating an alert definitions
|
||||||
|
// Legacy model; It will be removed in v8
|
||||||
type UpdateAlertDefinitionPausedCommand struct {
|
type UpdateAlertDefinitionPausedCommand struct {
|
||||||
OrgID int64 `json:"-"`
|
OrgID int64 `json:"-"`
|
||||||
UIDs []string `json:"uids"`
|
UIDs []string `json:"uids"`
|
||||||
@ -131,20 +132,10 @@ type UpdateAlertDefinitionPausedCommand struct {
|
|||||||
ResultCount int64
|
ResultCount int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Condition contains backend expressions and queries and the RefID
|
// EvalAlertConditionCommand is the command for evaluating a condition
|
||||||
// of the query or expression that will be evaluated.
|
// Legacy model; It will be removed in v8
|
||||||
type Condition struct {
|
type EvalAlertConditionCommand struct {
|
||||||
// Condition is the RefID of the query or expression from
|
|
||||||
// the Data property to get the results for.
|
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
OrgID int64 `json:"-"`
|
|
||||||
|
|
||||||
// Data is an array of data source queries and/or server side expressions.
|
|
||||||
Data []AlertQuery `json:"data"`
|
Data []AlertQuery `json:"data"`
|
||||||
}
|
Now time.Time `json:"now"`
|
||||||
|
|
||||||
// IsValid checks the condition's validity.
|
|
||||||
func (c Condition) IsValid() bool {
|
|
||||||
// TODO search for refIDs in QueriesAndExpressions
|
|
||||||
return len(c.Data) != 0
|
|
||||||
}
|
}
|
@ -81,6 +81,7 @@ func (ng *AlertNG) Init() error {
|
|||||||
Schedule: ng.schedule,
|
Schedule: ng.schedule,
|
||||||
DataProxy: ng.DataProxy,
|
DataProxy: ng.DataProxy,
|
||||||
Store: store,
|
Store: store,
|
||||||
|
RuleStore: store,
|
||||||
AlertingStore: store,
|
AlertingStore: store,
|
||||||
Alertmanager: ng.Alertmanager,
|
Alertmanager: ng.Alertmanager,
|
||||||
}
|
}
|
||||||
@ -114,6 +115,10 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) {
|
|||||||
// Create alert_instance table
|
// Create alert_instance table
|
||||||
store.AlertInstanceMigration(mg)
|
store.AlertInstanceMigration(mg)
|
||||||
|
|
||||||
|
// Create alert_rule
|
||||||
|
store.AddAlertRuleMigrations(mg, defaultIntervalSeconds)
|
||||||
|
store.AddAlertRuleVersionMigrations(mg)
|
||||||
|
|
||||||
// Create silence table
|
// Create silence table
|
||||||
store.SilenceMigration(mg)
|
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
|
// TimeNow makes it possible to test usage of time
|
||||||
var TimeNow = time.Now
|
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
|
const AlertDefinitionMaxTitleLength = 190
|
||||||
|
|
||||||
// ErrEmptyTitleError is an error returned if the alert definition title is empty
|
// ErrEmptyTitleError is an error returned if the alert definition title is empty
|
||||||
|
@ -3,6 +3,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"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]))
|
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) {
|
func SilenceMigration(mg *migrator.Migrator) {
|
||||||
silence := migrator.Table{
|
silence := migrator.Table{
|
||||||
Name: "silence",
|
Name: "silence",
|
||||||
|
Loading…
Reference in New Issue
Block a user