mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: backend "ng" code cleanup (#33578)
This commit is contained in:
parent
599a9b9a6d
commit
b8f01fe034
@ -6,15 +6,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
||||||
|
|
||||||
"github.com/go-macaron/binding"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -86,41 +82,4 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) {
|
|||||||
DatasourceCache: api.DatasourceCache,
|
DatasourceCache: api.DatasourceCache,
|
||||||
log: logger,
|
log: logger,
|
||||||
}, m)
|
}, m)
|
||||||
|
|
||||||
// Legacy routes; they will be removed in v8
|
|
||||||
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
|
||||||
alertDefinitions.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertDefinitions))
|
|
||||||
alertDefinitions.Get("/eval/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.alertDefinitionEvalEndpoint))
|
|
||||||
alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(ngmodels.EvalAlertConditionCommand{}), routing.Wrap(api.conditionEvalEndpoint))
|
|
||||||
alertDefinitions.Get("/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.getAlertDefinitionEndpoint))
|
|
||||||
alertDefinitions.Delete("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, routing.Wrap(api.deleteAlertDefinitionEndpoint))
|
|
||||||
alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(ngmodels.SaveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint))
|
|
||||||
alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(ngmodels.UpdateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint))
|
|
||||||
alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint))
|
|
||||||
alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint))
|
|
||||||
})
|
|
||||||
|
|
||||||
if api.Cfg.Env == setting.Dev {
|
|
||||||
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
|
||||||
alertDefinitions.Post("/evalOld", middleware.ReqSignedIn, routing.Wrap(api.conditionEvalOldEndpoint))
|
|
||||||
})
|
|
||||||
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
|
||||||
alertDefinitions.Get("/evalOldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionEvalOldEndpointByID))
|
|
||||||
})
|
|
||||||
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
|
||||||
alertDefinitions.Get("/oldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionOldEndpointByID))
|
|
||||||
})
|
|
||||||
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
|
|
||||||
alertDefinitions.Get("/ruleGroupByOldID/:id", middleware.ReqSignedIn, routing.Wrap(api.ruleGroupByOldID))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) {
|
|
||||||
schedulerRouter.Post("/pause", routing.Wrap(api.pauseScheduler))
|
|
||||||
schedulerRouter.Post("/unpause", routing.Wrap(api.unpauseScheduler))
|
|
||||||
}, middleware.ReqOrgAdmin)
|
|
||||||
|
|
||||||
api.RouteRegister.Group("/api/alert-instances", func(alertInstances routing.RouteRegister) {
|
|
||||||
alertInstances.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertInstancesEndpoint))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -258,25 +258,6 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimode
|
|||||||
return gettableExtendedRuleNode
|
return gettableExtendedRuleNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode {
|
|
||||||
postableExtendedRuleNode := apimodels.PostableExtendedRuleNode{
|
|
||||||
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
||||||
Title: r.Title,
|
|
||||||
Condition: r.Condition,
|
|
||||||
Data: r.Data,
|
|
||||||
UID: r.UID,
|
|
||||||
NoDataState: apimodels.NoDataState(r.NoDataState),
|
|
||||||
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
postableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{
|
|
||||||
For: model.Duration(r.For),
|
|
||||||
Annotations: r.Annotations,
|
|
||||||
Labels: r.Labels,
|
|
||||||
}
|
|
||||||
return postableExtendedRuleNode
|
|
||||||
}
|
|
||||||
|
|
||||||
func toNamespaceErrorResponse(err error) response.Response {
|
func toNamespaceErrorResponse(err error) response.Response {
|
||||||
if errors.Is(err, ngmodels.ErrCannotEditNamespace) {
|
if errors.Is(err, ngmodels.ErrCannotEditNamespace) {
|
||||||
return response.Error(http.StatusForbidden, err.Error(), err)
|
return response.Error(http.StatusForbidden, err.Error(), err)
|
||||||
|
@ -1,215 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// listAlertInstancesEndpoint handles GET /api/alert-instances.
|
|
||||||
func (api *API) listAlertInstancesEndpoint(c *models.ReqContext) response.Response {
|
|
||||||
cmd := ngmodels.ListAlertInstancesQuery{DefinitionOrgID: c.SignedInUser.OrgId}
|
|
||||||
|
|
||||||
if err := api.Store.ListAlertInstances(&cmd); err != nil {
|
|
||||||
return response.Error(500, "Failed to list alert instances", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(200, cmd.Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// conditionEvalEndpoint handles POST /api/alert-definitions/eval.
|
|
||||||
func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response {
|
|
||||||
return conditionEval(c, cmd, api.DatasourceCache, api.DataService, api.Cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
|
|
||||||
func (api *API) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response {
|
|
||||||
alertDefinitionUID := c.Params(":alertDefinitionUID")
|
|
||||||
|
|
||||||
condition, err := api.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to load alert definition conditions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateCondition(*condition, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
|
||||||
return response.Error(400, "invalid condition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluator := eval.Evaluator{Cfg: api.Cfg}
|
|
||||||
evalResults, err := evaluator.ConditionEval(condition, timeNow(), api.DataService)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to evaluate alert", err)
|
|
||||||
}
|
|
||||||
frame := evalResults.AsDataFrame()
|
|
||||||
|
|
||||||
return response.JSONStreaming(200, util.DynMap{
|
|
||||||
"instances": []*data.Frame{&frame},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionUID.
|
|
||||||
func (api *API) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
|
|
||||||
alertDefinitionUID := c.Params(":alertDefinitionUID")
|
|
||||||
|
|
||||||
query := ngmodels.GetAlertDefinitionByUIDQuery{
|
|
||||||
UID: alertDefinitionUID,
|
|
||||||
OrgID: c.SignedInUser.OrgId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
|
|
||||||
return response.Error(500, "Failed to get alert definition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(200, &query.Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionUID.
|
|
||||||
func (api *API) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
|
|
||||||
alertDefinitionUID := c.Params(":alertDefinitionUID")
|
|
||||||
|
|
||||||
cmd := ngmodels.DeleteAlertDefinitionByUIDCommand{
|
|
||||||
UID: alertDefinitionUID,
|
|
||||||
OrgID: c.SignedInUser.OrgId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := api.Store.DeleteAlertDefinitionByUID(&cmd); err != nil {
|
|
||||||
return response.Error(500, "Failed to delete alert definition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Success("Alert definition deleted")
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionUID.
|
|
||||||
func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionCommand) response.Response {
|
|
||||||
cmd.UID = c.Params(":alertDefinitionUID")
|
|
||||||
cmd.OrgID = c.SignedInUser.OrgId
|
|
||||||
|
|
||||||
evalCond := ngmodels.Condition{
|
|
||||||
Condition: cmd.Condition,
|
|
||||||
OrgID: c.SignedInUser.OrgId,
|
|
||||||
Data: cmd.Data,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
|
||||||
return response.Error(400, "invalid condition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := api.Store.UpdateAlertDefinition(&cmd); err != nil {
|
|
||||||
return response.Error(500, "Failed to update alert definition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(200, cmd.Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createAlertDefinitionEndpoint handles POST /api/alert-definitions.
|
|
||||||
func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.SaveAlertDefinitionCommand) response.Response {
|
|
||||||
cmd.OrgID = c.SignedInUser.OrgId
|
|
||||||
|
|
||||||
evalCond := ngmodels.Condition{
|
|
||||||
Condition: cmd.Condition,
|
|
||||||
OrgID: c.SignedInUser.OrgId,
|
|
||||||
Data: cmd.Data,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
|
||||||
return response.Error(400, "invalid condition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := api.Store.SaveAlertDefinition(&cmd); err != nil {
|
|
||||||
return response.Error(500, "Failed to create alert definition", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(200, cmd.Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// listAlertDefinitions handles GET /api/alert-definitions.
|
|
||||||
func (api *API) listAlertDefinitions(c *models.ReqContext) response.Response {
|
|
||||||
query := ngmodels.ListAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId}
|
|
||||||
|
|
||||||
if err := api.Store.GetOrgAlertDefinitions(&query); err != nil {
|
|
||||||
return response.Error(500, "Failed to list alert definitions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(200, util.DynMap{"results": query.Result})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) pauseScheduler() response.Response {
|
|
||||||
err := api.Schedule.Pause()
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(500, "Failed to pause scheduler", err)
|
|
||||||
}
|
|
||||||
return response.JSON(200, util.DynMap{"message": "alert definition scheduler paused"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) unpauseScheduler() response.Response {
|
|
||||||
err := api.Schedule.Unpause()
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(500, "Failed to unpause scheduler", err)
|
|
||||||
}
|
|
||||||
return response.JSON(200, util.DynMap{"message": "alert definition scheduler unpaused"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// alertDefinitionPauseEndpoint handles POST /api/alert-definitions/pause.
|
|
||||||
func (api *API) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response {
|
|
||||||
cmd.OrgID = c.SignedInUser.OrgId
|
|
||||||
cmd.Paused = true
|
|
||||||
|
|
||||||
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(500, "Failed to pause alert definition", err)
|
|
||||||
}
|
|
||||||
return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions paused", cmd.ResultCount)})
|
|
||||||
}
|
|
||||||
|
|
||||||
// alertDefinitionUnpauseEndpoint handles POST /api/alert-definitions/unpause.
|
|
||||||
func (api *API) alertDefinitionUnpauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response {
|
|
||||||
cmd.OrgID = c.SignedInUser.OrgId
|
|
||||||
cmd.Paused = false
|
|
||||||
|
|
||||||
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(500, "Failed to unpause alert definition", err)
|
|
||||||
}
|
|
||||||
return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions unpaused", cmd.ResultCount)})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadAlertCondition returns a Condition object for the given alertDefinitionID.
|
|
||||||
func (api *API) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*ngmodels.Condition, error) {
|
|
||||||
q := ngmodels.GetAlertDefinitionByUIDQuery{UID: alertDefinitionUID, OrgID: orgID}
|
|
||||||
if err := api.Store.GetAlertDefinitionByUID(&q); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
alertDefinition := q.Result
|
|
||||||
|
|
||||||
err := api.Store.ValidateAlertDefinition(alertDefinition, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ngmodels.Condition{
|
|
||||||
Condition: alertDefinition.Condition,
|
|
||||||
OrgID: alertDefinition.OrgID,
|
|
||||||
Data: alertDefinition.Data,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) validateOrgAlertDefinition(c *models.ReqContext) {
|
|
||||||
uid := c.ParamsEscape(":alertDefinitionUID")
|
|
||||||
|
|
||||||
if uid == "" {
|
|
||||||
c.JsonApiErr(403, "Permission denied", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := ngmodels.GetAlertDefinitionByUIDQuery{UID: uid, OrgID: c.SignedInUser.OrgId}
|
|
||||||
|
|
||||||
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
|
|
||||||
c.JsonApiErr(404, "Alert definition not found", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,294 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
"github.com/grafana/grafana/pkg/expr/translate"
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld.
|
|
||||||
func (api *API) conditionEvalOldEndpoint(c *models.ReqContext) response.Response {
|
|
||||||
b, err := c.Req.Body().Bytes()
|
|
||||||
if err != nil {
|
|
||||||
response.Error(400, "failed to read body", err)
|
|
||||||
}
|
|
||||||
evalCond, err := translate.DashboardAlertConditions(b, c.OrgId)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to translate alert conditions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
|
||||||
return response.Error(400, "invalid condition", err)
|
|
||||||
}
|
|
||||||
//now := cmd.Now
|
|
||||||
//if now.IsZero() {
|
|
||||||
//now := timeNow()
|
|
||||||
//}
|
|
||||||
evaluator := eval.Evaluator{Cfg: api.Cfg}
|
|
||||||
evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to evaluate conditions", err)
|
|
||||||
}
|
|
||||||
frame := evalResults.AsDataFrame()
|
|
||||||
return response.JSONStreaming(200, util.DynMap{
|
|
||||||
"instances": []*data.Frame{&frame},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld.
|
|
||||||
func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Response {
|
|
||||||
id := c.ParamsInt64("id")
|
|
||||||
if id == 0 {
|
|
||||||
return response.Error(400, "missing id", nil)
|
|
||||||
}
|
|
||||||
getAlert := &models.GetAlertByIdQuery{
|
|
||||||
Id: id,
|
|
||||||
}
|
|
||||||
if err := bus.Dispatch(getAlert); err != nil {
|
|
||||||
return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err)
|
|
||||||
}
|
|
||||||
if getAlert.Result.OrgId != c.SignedInUser.OrgId {
|
|
||||||
return response.Error(403, "alert does not match organization of user", nil)
|
|
||||||
}
|
|
||||||
settings := getAlert.Result.Settings
|
|
||||||
sb, err := settings.ToDB()
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "failed to marshal alert settings", err)
|
|
||||||
}
|
|
||||||
evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to translate alert conditions", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
|
|
||||||
return response.Error(400, "invalid condition", err)
|
|
||||||
}
|
|
||||||
//now := cmd.Now
|
|
||||||
//if now.IsZero() {
|
|
||||||
//now := timeNow()
|
|
||||||
//}
|
|
||||||
evaluator := eval.Evaluator{Cfg: api.Cfg}
|
|
||||||
evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to evaluate conditions", err)
|
|
||||||
}
|
|
||||||
frame := evalResults.AsDataFrame()
|
|
||||||
return response.JSONStreaming(200, util.DynMap{
|
|
||||||
"instances": []*data.Frame{&frame},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// conditionEvalEndpoint handles POST /api/alert-definitions/oldByID.
|
|
||||||
func (api *API) conditionOldEndpointByID(c *models.ReqContext) response.Response {
|
|
||||||
id := c.ParamsInt64("id")
|
|
||||||
if id == 0 {
|
|
||||||
return response.Error(400, "missing id", nil)
|
|
||||||
}
|
|
||||||
getAlert := &models.GetAlertByIdQuery{
|
|
||||||
Id: id,
|
|
||||||
}
|
|
||||||
if err := bus.Dispatch(getAlert); err != nil {
|
|
||||||
return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err)
|
|
||||||
}
|
|
||||||
if getAlert.Result.OrgId != c.SignedInUser.OrgId {
|
|
||||||
return response.Error(403, "alert does not match organization of user", nil)
|
|
||||||
}
|
|
||||||
settings := getAlert.Result.Settings
|
|
||||||
sb, err := settings.ToDB()
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "failed to marshal alert settings", err)
|
|
||||||
}
|
|
||||||
evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "Failed to translate alert conditions", err)
|
|
||||||
}
|
|
||||||
return response.JSON(200, evalCond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ruleGroupByOldID handles POST /api/alert-definitions/ruleGroupByOldID.
|
|
||||||
func (api *API) ruleGroupByOldID(c *models.ReqContext) response.Response {
|
|
||||||
id := c.ParamsInt64("id")
|
|
||||||
if id == 0 {
|
|
||||||
return response.Error(400, "missing id", nil)
|
|
||||||
}
|
|
||||||
save := c.Query("save") == "true"
|
|
||||||
// Get dashboard alert definition from database.
|
|
||||||
oldAlert, status, err := transGetAlertById(id, *c.SignedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(status, "failed to get alert", fmt.Errorf("failed to get alert for alert id %v: %w", id, err))
|
|
||||||
}
|
|
||||||
// Translate the dashboard's alerts conditions into SSE queries and conditions.
|
|
||||||
sseCond, err := transToSSECondition(oldAlert, *c.SignedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "failed to translate alert conditions",
|
|
||||||
fmt.Errorf("failed to translate alert conditions for alert id %v: %w", id, err))
|
|
||||||
}
|
|
||||||
// Get the dashboard that contains the dashboard Alert.
|
|
||||||
oldAlertsDash, status, err := transGetAlertsDashById(oldAlert.DashboardId, *c.SignedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(status, "failed to get alert's dashboard", fmt.Errorf("failed to get dashboard for alert id %v, %w", id, err))
|
|
||||||
}
|
|
||||||
isGeneralFolder := oldAlertsDash.FolderId == 0 && !oldAlertsDash.IsFolder
|
|
||||||
var namespaceUID string
|
|
||||||
if isGeneralFolder {
|
|
||||||
namespaceUID = "General"
|
|
||||||
} else {
|
|
||||||
// Get the folder that contains the dashboard that contains the dashboard alert.
|
|
||||||
getFolder := &models.GetDashboardQuery{
|
|
||||||
Id: oldAlertsDash.FolderId,
|
|
||||||
OrgId: oldAlertsDash.OrgId,
|
|
||||||
}
|
|
||||||
if err := bus.Dispatch(getFolder); err != nil {
|
|
||||||
return response.Error(400, fmt.Sprintf("could find folder %v for alert with id %v", getFolder.Id, id), err)
|
|
||||||
}
|
|
||||||
namespaceUID = getFolder.Result.Uid
|
|
||||||
}
|
|
||||||
noDataSetting, execErrSetting, err := transNoDataExecSettings(oldAlert, *c.SignedInUser)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(400, "unable to translate nodata/exec error settings",
|
|
||||||
fmt.Errorf("unable to translate nodata/exec error settings for alert id %v: %w", id, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleTags := map[string]string{}
|
|
||||||
|
|
||||||
for k, v := range oldAlert.Settings.Get("alertRuleTags").MustMap() {
|
|
||||||
sV, ok := v.(string)
|
|
||||||
if !ok {
|
|
||||||
return response.Error(400, "unable to unmarshal rule tags",
|
|
||||||
fmt.Errorf("unexpected type %T for tag %v", v, k))
|
|
||||||
}
|
|
||||||
ruleTags[k] = sV
|
|
||||||
}
|
|
||||||
|
|
||||||
rule := ngmodels.AlertRule{
|
|
||||||
Title: oldAlert.Name,
|
|
||||||
Data: sseCond.Data,
|
|
||||||
Condition: sseCond.Condition,
|
|
||||||
NoDataState: *noDataSetting,
|
|
||||||
ExecErrState: *execErrSetting,
|
|
||||||
For: oldAlert.For,
|
|
||||||
Annotations: ruleTags,
|
|
||||||
}
|
|
||||||
rgc := apimodels.PostableRuleGroupConfig{
|
|
||||||
// TODO? Generate new name on conflict?
|
|
||||||
Name: oldAlert.Name,
|
|
||||||
Interval: transAdjustInterval(oldAlert.Frequency),
|
|
||||||
Rules: []apimodels.PostableExtendedRuleNode{
|
|
||||||
toPostableExtendedRuleNode(rule),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmd := store.UpdateRuleGroupCmd{
|
|
||||||
OrgID: oldAlert.OrgId,
|
|
||||||
NamespaceUID: namespaceUID,
|
|
||||||
RuleGroupConfig: rgc,
|
|
||||||
}
|
|
||||||
if !save {
|
|
||||||
return response.JSON(200, cmd)
|
|
||||||
}
|
|
||||||
// note: Update rule group will set the Interval within the grafana_alert from
|
|
||||||
// the interval of the group.
|
|
||||||
err = api.RuleStore.UpdateRuleGroup(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return response.JSON(400, util.DynMap{
|
|
||||||
"message:": "failed to save alert rule",
|
|
||||||
"error": err.Error(),
|
|
||||||
"cmd": cmd,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return response.JSON(200, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transAdjustInterval(freq int64) model.Duration {
|
|
||||||
// 10 corresponds to the SchedulerCfg, but TODO not worrying about fetching for now.
|
|
||||||
var baseFreq int64 = 10
|
|
||||||
if freq <= baseFreq {
|
|
||||||
return model.Duration(time.Second * 10)
|
|
||||||
}
|
|
||||||
return model.Duration(time.Duration((freq - (freq % baseFreq))) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transGetAlertById(id int64, user models.SignedInUser) (*models.Alert, int, error) {
|
|
||||||
getAlert := &models.GetAlertByIdQuery{
|
|
||||||
Id: id,
|
|
||||||
}
|
|
||||||
if err := bus.Dispatch(getAlert); err != nil {
|
|
||||||
return nil, 400, fmt.Errorf("could find alert with id %v: %w", id, err)
|
|
||||||
}
|
|
||||||
if getAlert.Result.OrgId != user.OrgId {
|
|
||||||
return nil, 403, fmt.Errorf("alert does not match organization of user")
|
|
||||||
}
|
|
||||||
return getAlert.Result, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transGetAlertsDashById(dashboardId int64, user models.SignedInUser) (*models.Dashboard, int, error) {
|
|
||||||
getDash := &models.GetDashboardQuery{
|
|
||||||
Id: dashboardId,
|
|
||||||
OrgId: user.OrgId,
|
|
||||||
}
|
|
||||||
if err := bus.Dispatch(getDash); err != nil {
|
|
||||||
return nil, 400, fmt.Errorf("could find dashboard with id %v: %w", dashboardId, err)
|
|
||||||
}
|
|
||||||
return getDash.Result, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transToSSECondition(m *models.Alert, user models.SignedInUser) (*ngmodels.Condition, error) {
|
|
||||||
sb, err := m.Settings.ToDB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal alert settings: %w", err)
|
|
||||||
}
|
|
||||||
evalCond, err := translate.DashboardAlertConditions(sb, user.OrgId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to translate dashboard alert to SSE conditions: %w", err)
|
|
||||||
}
|
|
||||||
return evalCond, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transNoDataExecSettings(m *models.Alert, user models.SignedInUser) (*ngmodels.NoDataState, *ngmodels.ExecutionErrorState, error) {
|
|
||||||
oldNoData := m.Settings.Get("noDataState").MustString()
|
|
||||||
noDataSetting, err := transNoData(oldNoData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
oldExecErr := m.Settings.Get("executionErrorState").MustString()
|
|
||||||
execErrSetting, err := transExecErr(oldExecErr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return &noDataSetting, &execErrSetting, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transNoData(s string) (ngmodels.NoDataState, error) {
|
|
||||||
switch s {
|
|
||||||
case "ok":
|
|
||||||
return ngmodels.OK, nil
|
|
||||||
case "no_data":
|
|
||||||
return ngmodels.NoData, nil
|
|
||||||
case "alerting":
|
|
||||||
return ngmodels.Alerting, nil
|
|
||||||
case "keep_state":
|
|
||||||
return ngmodels.KeepLastState, nil
|
|
||||||
}
|
|
||||||
return ngmodels.NoData, fmt.Errorf("unrecognized No Data setting %v", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transExecErr(s string) (ngmodels.ExecutionErrorState, error) {
|
|
||||||
switch s {
|
|
||||||
case "alerting":
|
|
||||||
return ngmodels.AlertingErrState, nil
|
|
||||||
case "KeepLastState":
|
|
||||||
return ngmodels.KeepLastStateErrState, nil
|
|
||||||
}
|
|
||||||
return ngmodels.AlertingErrState, fmt.Errorf("unrecognized Execution Error setting %v", s)
|
|
||||||
}
|
|
@ -80,7 +80,6 @@ type FetchUniqueOrgIdsQuery struct {
|
|||||||
type ListAlertInstancesQueryResult struct {
|
type ListAlertInstancesQueryResult struct {
|
||||||
DefinitionOrgID int64 `xorm:"def_org_id" json:"definitionOrgId"`
|
DefinitionOrgID int64 `xorm:"def_org_id" json:"definitionOrgId"`
|
||||||
DefinitionUID string `xorm:"def_uid" json:"definitionUid"`
|
DefinitionUID string `xorm:"def_uid" json:"definitionUid"`
|
||||||
DefinitionTitle string `xorm:"def_title" json:"definitionTitle"`
|
|
||||||
Labels InstanceLabels `json:"labels"`
|
Labels InstanceLabels `json:"labels"`
|
||||||
LabelsHash string `json:"labeHash"`
|
LabelsHash string `json:"labeHash"`
|
||||||
CurrentState InstanceStateType `json:"currentState"`
|
CurrentState InstanceStateType `json:"currentState"`
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrAlertDefinitionNotFound is an error for an unknown alert definition.
|
|
||||||
ErrAlertDefinitionNotFound = fmt.Errorf("could not find alert definition")
|
|
||||||
// ErrAlertDefinitionFailedGenerateUniqueUID is an error for failure to generate alert definition UID
|
|
||||||
ErrAlertDefinitionFailedGenerateUniqueUID = errors.New("failed to generate alert definition UID")
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Data []AlertQuery `json:"data"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
IntervalSeconds int64 `json:"intervalSeconds"`
|
|
||||||
Version int64 `json:"version"`
|
|
||||||
UID string `xorm:"uid" json:"uid"`
|
|
||||||
Paused bool `json:"paused"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlertDefinitionKey is the alert definition identifier
|
|
||||||
type AlertDefinitionKey struct {
|
|
||||||
OrgID int64
|
|
||||||
DefinitionUID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k AlertDefinitionKey) String() string {
|
|
||||||
return fmt.Sprintf("{orgID: %d, definitionUID: %s}", k.OrgID, k.DefinitionUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetKey returns the alert definitions identifier
|
|
||||||
func (alertDefinition *AlertDefinition) GetKey() AlertDefinitionKey {
|
|
||||||
return AlertDefinitionKey{OrgID: alertDefinition.OrgID, DefinitionUID: alertDefinition.UID}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreSave sets datasource and loads the updated model for each alert query.
|
|
||||||
func (alertDefinition *AlertDefinition) PreSave(timeNow func() time.Time) error {
|
|
||||||
for i, q := range alertDefinition.Data {
|
|
||||||
err := q.PreSave()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid alert query %s: %w", q.RefID, err)
|
|
||||||
}
|
|
||||||
alertDefinition.Data[i] = q
|
|
||||||
}
|
|
||||||
alertDefinition.Updated = timeNow()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
AlertDefinitionUID string `xorm:"alert_definition_uid"`
|
|
||||||
ParentVersion int64
|
|
||||||
RestoredFrom int64
|
|
||||||
Version int64
|
|
||||||
|
|
||||||
Created time.Time
|
|
||||||
Title string
|
|
||||||
Condition string
|
|
||||||
Data []AlertQuery
|
|
||||||
IntervalSeconds int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
Result *AlertDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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:"-"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Data []AlertQuery `json:"data"`
|
|
||||||
IntervalSeconds *int64 `json:"intervalSeconds"`
|
|
||||||
|
|
||||||
Result *AlertDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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:"-"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Data []AlertQuery `json:"data"`
|
|
||||||
IntervalSeconds *int64 `json:"intervalSeconds"`
|
|
||||||
UID string `json:"-"`
|
|
||||||
|
|
||||||
Result *AlertDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAlertDefinitionsQuery is the query for listing alert definitions
|
|
||||||
// Legacy model; It will be removed in v8
|
|
||||||
type ListAlertDefinitionsQuery struct {
|
|
||||||
OrgID int64 `json:"-"`
|
|
||||||
|
|
||||||
Result []*AlertDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
Paused bool `json:"-"`
|
|
||||||
|
|
||||||
ResultCount int64
|
|
||||||
}
|
|
@ -1,16 +1,11 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimeNow makes it possible to test usage of time
|
// TimeNow makes it possible to test usage of time
|
||||||
@ -21,17 +16,9 @@ const AlertDefinitionMaxTitleLength = 190
|
|||||||
|
|
||||||
// Store is the interface for persisting alert definitions and instances
|
// Store is the interface for persisting alert definitions and instances
|
||||||
type Store interface {
|
type Store interface {
|
||||||
DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error
|
|
||||||
GetAlertDefinitionByUID(*models.GetAlertDefinitionByUIDQuery) error
|
|
||||||
GetAlertDefinitions(*models.ListAlertDefinitionsQuery) error
|
|
||||||
GetOrgAlertDefinitions(*models.ListAlertDefinitionsQuery) error
|
|
||||||
SaveAlertDefinition(*models.SaveAlertDefinitionCommand) error
|
|
||||||
UpdateAlertDefinition(*models.UpdateAlertDefinitionCommand) error
|
|
||||||
GetAlertInstance(*models.GetAlertInstanceQuery) error
|
GetAlertInstance(*models.GetAlertInstanceQuery) error
|
||||||
ListAlertInstances(*models.ListAlertInstancesQuery) error
|
ListAlertInstances(*models.ListAlertInstancesQuery) error
|
||||||
SaveAlertInstance(*models.SaveAlertInstanceCommand) error
|
SaveAlertInstance(*models.SaveAlertInstanceCommand) error
|
||||||
ValidateAlertDefinition(*models.AlertDefinition, bool) error
|
|
||||||
UpdateAlertDefinitionPaused(*models.UpdateAlertDefinitionPausedCommand) error
|
|
||||||
FetchOrgIds(cmd *models.FetchUniqueOrgIdsQuery) error
|
FetchOrgIds(cmd *models.FetchUniqueOrgIdsQuery) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,297 +37,3 @@ type DBstore struct {
|
|||||||
DefaultIntervalSeconds int64
|
DefaultIntervalSeconds int64
|
||||||
SQLStore *sqlstore.SQLStore `inject:""`
|
SQLStore *sqlstore.SQLStore `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*models.AlertDefinition, error) {
|
|
||||||
// we consider optionally enabling some caching
|
|
||||||
alertDefinition := models.AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
|
|
||||||
has, err := sess.Get(&alertDefinition)
|
|
||||||
if !has {
|
|
||||||
return nil, models.ErrAlertDefinitionNotFound
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &alertDefinition, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAlertDefinitionByUID is a handler for deleting an alert definition.
|
|
||||||
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
|
|
||||||
func (st DBstore) DeleteAlertDefinitionByUID(cmd *models.DeleteAlertDefinitionByUIDCommand) error {
|
|
||||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
_, err := sess.Exec("DELETE FROM alert_definition WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.Exec("DELETE FROM alert_definition_version WHERE alert_definition_uid = ?", cmd.UID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.Exec("DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid = ?", cmd.OrgID, cmd.UID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID.
|
|
||||||
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
|
|
||||||
func (st DBstore) GetAlertDefinitionByUID(query *models.GetAlertDefinitionByUIDQuery) error {
|
|
||||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
query.Result = alertDefinition
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAlertDefinition is a handler for saving a new alert definition.
|
|
||||||
func (st DBstore) SaveAlertDefinition(cmd *models.SaveAlertDefinitionCommand) error {
|
|
||||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
intervalSeconds := st.DefaultIntervalSeconds
|
|
||||||
if cmd.IntervalSeconds != nil {
|
|
||||||
intervalSeconds = *cmd.IntervalSeconds
|
|
||||||
}
|
|
||||||
|
|
||||||
var initialVersion int64 = 1
|
|
||||||
|
|
||||||
uid, err := generateNewAlertDefinitionUID(sess, cmd.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to generate UID for alert definition %q: %w", cmd.Title, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
alertDefinition := &models.AlertDefinition{
|
|
||||||
OrgID: cmd.OrgID,
|
|
||||||
Title: cmd.Title,
|
|
||||||
Condition: cmd.Condition,
|
|
||||||
Data: cmd.Data,
|
|
||||||
IntervalSeconds: intervalSeconds,
|
|
||||||
Version: initialVersion,
|
|
||||||
UID: uid,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := st.ValidateAlertDefinition(alertDefinition, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := alertDefinition.PreSave(TimeNow); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := sess.Insert(alertDefinition); err != nil {
|
|
||||||
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "title") {
|
|
||||||
return fmt.Errorf("an alert definition with the title '%s' already exists: %w", cmd.Title, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
alertDefVersion := models.AlertDefinitionVersion{
|
|
||||||
AlertDefinitionID: alertDefinition.ID,
|
|
||||||
AlertDefinitionUID: alertDefinition.UID,
|
|
||||||
Version: alertDefinition.Version,
|
|
||||||
Created: alertDefinition.Updated,
|
|
||||||
Condition: alertDefinition.Condition,
|
|
||||||
Title: alertDefinition.Title,
|
|
||||||
Data: alertDefinition.Data,
|
|
||||||
IntervalSeconds: alertDefinition.IntervalSeconds,
|
|
||||||
}
|
|
||||||
if _, err := sess.Insert(alertDefVersion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Result = alertDefinition
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAlertDefinition is a handler for updating an existing alert definition.
|
|
||||||
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
|
|
||||||
func (st DBstore) UpdateAlertDefinition(cmd *models.UpdateAlertDefinitionCommand) error {
|
|
||||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, models.ErrAlertDefinitionNotFound) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
title := cmd.Title
|
|
||||||
if title == "" {
|
|
||||||
title = existingAlertDefinition.Title
|
|
||||||
}
|
|
||||||
condition := cmd.Condition
|
|
||||||
if condition == "" {
|
|
||||||
condition = existingAlertDefinition.Condition
|
|
||||||
}
|
|
||||||
data := cmd.Data
|
|
||||||
if data == nil {
|
|
||||||
data = existingAlertDefinition.Data
|
|
||||||
}
|
|
||||||
intervalSeconds := cmd.IntervalSeconds
|
|
||||||
if intervalSeconds == nil {
|
|
||||||
intervalSeconds = &existingAlertDefinition.IntervalSeconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// explicitly set all fields regardless of being provided or not
|
|
||||||
alertDefinition := &models.AlertDefinition{
|
|
||||||
ID: existingAlertDefinition.ID,
|
|
||||||
Title: title,
|
|
||||||
Condition: condition,
|
|
||||||
Data: data,
|
|
||||||
OrgID: existingAlertDefinition.OrgID,
|
|
||||||
IntervalSeconds: *intervalSeconds,
|
|
||||||
UID: existingAlertDefinition.UID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := st.ValidateAlertDefinition(alertDefinition, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := alertDefinition.PreSave(TimeNow); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
alertDefinition.Version = existingAlertDefinition.Version + 1
|
|
||||||
|
|
||||||
_, err = sess.ID(existingAlertDefinition.ID).Update(alertDefinition)
|
|
||||||
if err != nil {
|
|
||||||
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "title") {
|
|
||||||
return fmt.Errorf("an alert definition with the title '%s' already exists: %w", cmd.Title, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
alertDefVersion := models.AlertDefinitionVersion{
|
|
||||||
AlertDefinitionID: alertDefinition.ID,
|
|
||||||
AlertDefinitionUID: alertDefinition.UID,
|
|
||||||
ParentVersion: alertDefinition.Version,
|
|
||||||
Version: alertDefinition.Version,
|
|
||||||
Condition: alertDefinition.Condition,
|
|
||||||
Created: alertDefinition.Updated,
|
|
||||||
Title: alertDefinition.Title,
|
|
||||||
Data: alertDefinition.Data,
|
|
||||||
IntervalSeconds: alertDefinition.IntervalSeconds,
|
|
||||||
}
|
|
||||||
if _, err := sess.Insert(alertDefVersion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Result = alertDefinition
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
|
|
||||||
func (st DBstore) GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
|
|
||||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
alertDefinitions := make([]*models.AlertDefinition, 0)
|
|
||||||
q := "SELECT * FROM alert_definition WHERE org_id = ?"
|
|
||||||
if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Result = alertDefinitions
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAlertDefinitions returns alert definition identifier, interval, version and pause state
|
|
||||||
// that are useful for it's scheduling.
|
|
||||||
func (st DBstore) GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
|
|
||||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
alerts := make([]*models.AlertDefinition, 0)
|
|
||||||
q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_definition"
|
|
||||||
if err := sess.SQL(q).Find(&alerts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Result = alerts
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAlertDefinitionPaused update the pause state of an alert definition.
|
|
||||||
func (st DBstore) UpdateAlertDefinitionPaused(cmd *models.UpdateAlertDefinitionPausedCommand) error {
|
|
||||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
|
||||||
if len(cmd.UIDs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
placeHolders := strings.Builder{}
|
|
||||||
const separator = ", "
|
|
||||||
separatorVar := separator
|
|
||||||
params := []interface{}{cmd.Paused, cmd.OrgID}
|
|
||||||
for i, UID := range cmd.UIDs {
|
|
||||||
if i == len(cmd.UIDs)-1 {
|
|
||||||
separatorVar = ""
|
|
||||||
}
|
|
||||||
placeHolders.WriteString(fmt.Sprintf("?%s", separatorVar))
|
|
||||||
params = append(params, UID)
|
|
||||||
}
|
|
||||||
sql := fmt.Sprintf("UPDATE alert_definition SET paused = ? WHERE org_id = ? AND uid IN (%s)", placeHolders.String())
|
|
||||||
|
|
||||||
// prepend sql statement to params
|
|
||||||
var i interface{}
|
|
||||||
params = append(params, i)
|
|
||||||
copy(params[1:], params[0:])
|
|
||||||
params[0] = sql
|
|
||||||
|
|
||||||
res, err := sess.Exec(params...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resultCount, err := res.RowsAffected(); err == nil {
|
|
||||||
cmd.ResultCount = resultCount
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateNewAlertDefinitionUID(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(&models.AlertDefinition{})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return uid, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", models.ErrAlertDefinitionFailedGenerateUniqueUID
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAlertDefinition validates the alert definition interval and organisation.
|
|
||||||
// If requireData is true checks that it contains at least one alert query
|
|
||||||
func (st DBstore) ValidateAlertDefinition(alertDefinition *models.AlertDefinition, requireData bool) error {
|
|
||||||
if !requireData && len(alertDefinition.Data) == 0 {
|
|
||||||
return fmt.Errorf("no queries or expressions are found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if alertDefinition.Title == "" {
|
|
||||||
return fmt.Errorf("title is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if alertDefinition.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 {
|
|
||||||
return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertDefinition.IntervalSeconds)*time.Second, st.BaseInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
// enfore max name length in SQLite
|
|
||||||
if len(alertDefinition.Title) > AlertDefinitionMaxTitleLength {
|
|
||||||
return fmt.Errorf("name length should not be greater than %d", AlertDefinitionMaxTitleLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if alertDefinition.OrgID == 0 {
|
|
||||||
return fmt.Errorf("no organisation is found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -1,450 +0,0 @@
|
|||||||
// +build integration
|
|
||||||
|
|
||||||
package tests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const baseIntervalSeconds = 10
|
|
||||||
|
|
||||||
func mockTimeNow() {
|
|
||||||
var timeSeed int64
|
|
||||||
store.TimeNow = func() time.Time {
|
|
||||||
fakeNow := time.Unix(timeSeed, 0).UTC()
|
|
||||||
timeSeed++
|
|
||||||
return fakeNow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetTimeNow() {
|
|
||||||
store.TimeNow = time.Now
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreatingAlertDefinition(t *testing.T) {
|
|
||||||
mockTimeNow()
|
|
||||||
defer resetTimeNow()
|
|
||||||
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
var customIntervalSeconds int64 = 120
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
inputIntervalSeconds *int64
|
|
||||||
inputTitle string
|
|
||||||
expectedError error
|
|
||||||
expectedInterval int64
|
|
||||||
|
|
||||||
expectedUpdated time.Time
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "should create successfully an alert definition with default interval",
|
|
||||||
inputIntervalSeconds: nil,
|
|
||||||
inputTitle: "a name",
|
|
||||||
expectedInterval: dbstore.DefaultIntervalSeconds,
|
|
||||||
expectedUpdated: time.Unix(0, 0).UTC(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should create successfully an alert definition with custom interval",
|
|
||||||
inputIntervalSeconds: &customIntervalSeconds,
|
|
||||||
inputTitle: "another name",
|
|
||||||
expectedInterval: customIntervalSeconds,
|
|
||||||
expectedUpdated: time.Unix(1, 0).UTC(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should fail to create an alert definition with too big name",
|
|
||||||
inputIntervalSeconds: &customIntervalSeconds,
|
|
||||||
inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
|
|
||||||
expectedError: errors.New(""),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should fail to create an alert definition with empty title",
|
|
||||||
inputIntervalSeconds: &customIntervalSeconds,
|
|
||||||
inputTitle: "",
|
|
||||||
expectedError: fmt.Errorf("title is empty"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
|
|
||||||
q := models.SaveAlertDefinitionCommand{
|
|
||||||
OrgID: 1,
|
|
||||||
Title: tc.inputTitle,
|
|
||||||
Condition: "B",
|
|
||||||
Data: []models.AlertQuery{
|
|
||||||
{
|
|
||||||
Model: json.RawMessage(`{
|
|
||||||
"datasourceUid": "-100",
|
|
||||||
"type":"math",
|
|
||||||
"expression":"2 + 3 > 1"
|
|
||||||
}`),
|
|
||||||
RefID: "B",
|
|
||||||
RelativeTimeRange: models.RelativeTimeRange{
|
|
||||||
From: models.Duration(time.Duration(5) * time.Hour),
|
|
||||||
To: models.Duration(time.Duration(3) * time.Hour),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if tc.inputIntervalSeconds != nil {
|
|
||||||
q.IntervalSeconds = tc.inputIntervalSeconds
|
|
||||||
}
|
|
||||||
err := dbstore.SaveAlertDefinition(&q)
|
|
||||||
switch {
|
|
||||||
case tc.expectedError != nil:
|
|
||||||
require.Error(t, err)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
|
|
||||||
assert.Equal(t, tc.expectedInterval, q.Result.IntervalSeconds)
|
|
||||||
assert.Equal(t, int64(1), q.Result.Version)
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestCreatingConflictionAlertDefinition(t *testing.T) {
|
|
||||||
t.Run("Should fail to create alert definition with conflicting org_id, title", func(t *testing.T) {
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
q := models.SaveAlertDefinitionCommand{
|
|
||||||
OrgID: 1,
|
|
||||||
Title: "title",
|
|
||||||
Condition: "B",
|
|
||||||
Data: []models.AlertQuery{
|
|
||||||
{
|
|
||||||
Model: json.RawMessage(`{
|
|
||||||
"datasourceUid": "-100",
|
|
||||||
"type":"math",
|
|
||||||
"expression":"2 + 3 > 1"
|
|
||||||
}`),
|
|
||||||
RefID: "B",
|
|
||||||
RelativeTimeRange: models.RelativeTimeRange{
|
|
||||||
From: models.Duration(time.Duration(5) * time.Hour),
|
|
||||||
To: models.Duration(time.Duration(3) * time.Hour),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := dbstore.SaveAlertDefinition(&q)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = dbstore.SaveAlertDefinition(&q)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdatingAlertDefinition(t *testing.T) {
|
|
||||||
t.Run("zero rows affected when updating unknown alert", func(t *testing.T) {
|
|
||||||
mockTimeNow()
|
|
||||||
defer resetTimeNow()
|
|
||||||
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
q := models.UpdateAlertDefinitionCommand{
|
|
||||||
UID: "unknown",
|
|
||||||
OrgID: 1,
|
|
||||||
Title: "something completely different",
|
|
||||||
Condition: "A",
|
|
||||||
Data: []models.AlertQuery{
|
|
||||||
{
|
|
||||||
Model: json.RawMessage(`{
|
|
||||||
"datasourceUid": "-100",
|
|
||||||
"type":"math",
|
|
||||||
"expression":"2 + 2 > 1"
|
|
||||||
}`),
|
|
||||||
RefID: "A",
|
|
||||||
RelativeTimeRange: models.RelativeTimeRange{
|
|
||||||
From: models.Duration(time.Duration(5) * time.Hour),
|
|
||||||
To: models.Duration(time.Duration(3) * time.Hour),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := dbstore.UpdateAlertDefinition(&q)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("updating existing alert", func(t *testing.T) {
|
|
||||||
mockTimeNow()
|
|
||||||
defer resetTimeNow()
|
|
||||||
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
var initialInterval int64 = 120
|
|
||||||
alertDefinition := createTestAlertDefinition(t, dbstore, initialInterval)
|
|
||||||
created := alertDefinition.Updated
|
|
||||||
|
|
||||||
var customInterval int64 = 30
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
inputOrgID int64
|
|
||||||
inputTitle string
|
|
||||||
inputInterval *int64
|
|
||||||
expectedError error
|
|
||||||
expectedIntervalSeconds int64
|
|
||||||
expectedUpdated time.Time
|
|
||||||
expectedTitle string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "should not update previous interval if it's not provided",
|
|
||||||
inputInterval: nil,
|
|
||||||
inputOrgID: alertDefinition.OrgID,
|
|
||||||
inputTitle: "something completely different",
|
|
||||||
expectedIntervalSeconds: initialInterval,
|
|
||||||
expectedUpdated: time.Unix(1, 0).UTC(),
|
|
||||||
expectedTitle: "something completely different",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should update interval if it's provided",
|
|
||||||
inputInterval: &customInterval,
|
|
||||||
inputOrgID: alertDefinition.OrgID,
|
|
||||||
inputTitle: "something completely different",
|
|
||||||
expectedIntervalSeconds: customInterval,
|
|
||||||
expectedUpdated: time.Unix(2, 0).UTC(),
|
|
||||||
expectedTitle: "something completely different",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should not update organisation if it's provided",
|
|
||||||
inputInterval: &customInterval,
|
|
||||||
inputOrgID: 0,
|
|
||||||
inputTitle: "something completely different",
|
|
||||||
expectedIntervalSeconds: customInterval,
|
|
||||||
expectedUpdated: time.Unix(3, 0).UTC(),
|
|
||||||
expectedTitle: "something completely different",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should not update alert definition if the title it's too big",
|
|
||||||
inputInterval: &customInterval,
|
|
||||||
inputOrgID: 0,
|
|
||||||
inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
|
|
||||||
expectedError: errors.New(""),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should not update alert definition title if the title is empty",
|
|
||||||
inputInterval: &customInterval,
|
|
||||||
inputOrgID: 0,
|
|
||||||
inputTitle: "",
|
|
||||||
expectedIntervalSeconds: customInterval,
|
|
||||||
expectedUpdated: time.Unix(4, 0).UTC(),
|
|
||||||
expectedTitle: "something completely different",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
q := models.UpdateAlertDefinitionCommand{
|
|
||||||
UID: (*alertDefinition).UID,
|
|
||||||
Condition: "B",
|
|
||||||
Data: []models.AlertQuery{
|
|
||||||
{
|
|
||||||
Model: json.RawMessage(`{
|
|
||||||
"datasourceUid": "-100",
|
|
||||||
"type":"math",
|
|
||||||
"expression":"2 + 3 > 1"
|
|
||||||
}`),
|
|
||||||
RefID: "B",
|
|
||||||
RelativeTimeRange: models.RelativeTimeRange{
|
|
||||||
From: models.Duration(5 * time.Hour),
|
|
||||||
To: models.Duration(3 * time.Hour),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
lastUpdated := created
|
|
||||||
previousAlertDefinition := alertDefinition
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
if tc.inputInterval != nil {
|
|
||||||
q.IntervalSeconds = tc.inputInterval
|
|
||||||
}
|
|
||||||
if tc.inputOrgID != 0 {
|
|
||||||
q.OrgID = tc.inputOrgID
|
|
||||||
}
|
|
||||||
q.Title = tc.inputTitle
|
|
||||||
err := dbstore.UpdateAlertDefinition(&q)
|
|
||||||
switch {
|
|
||||||
case tc.expectedError != nil:
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, previousAlertDefinition.Title, q.Result.Title)
|
|
||||||
assert.Equal(t, previousAlertDefinition.Condition, q.Result.Condition)
|
|
||||||
assert.Equal(t, len(previousAlertDefinition.Data), len(q.Result.Data))
|
|
||||||
assert.Equal(t, previousAlertDefinition.IntervalSeconds, q.Result.IntervalSeconds)
|
|
||||||
assert.Equal(t, previousAlertDefinition.Updated, q.Result.Updated)
|
|
||||||
assert.Equal(t, previousAlertDefinition.Version, q.Result.Version)
|
|
||||||
assert.Equal(t, previousAlertDefinition.OrgID, q.Result.OrgID)
|
|
||||||
assert.Equal(t, previousAlertDefinition.UID, q.Result.UID)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, previousAlertDefinition.ID, q.Result.ID)
|
|
||||||
assert.Equal(t, previousAlertDefinition.UID, q.Result.UID)
|
|
||||||
assert.True(t, q.Result.Updated.After(lastUpdated))
|
|
||||||
assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
|
|
||||||
assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version)
|
|
||||||
|
|
||||||
assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID)
|
|
||||||
|
|
||||||
assert.Equal(t, "something completely different", q.Result.Title)
|
|
||||||
assert.Equal(t, "B", q.Result.Condition)
|
|
||||||
assert.Equal(t, 1, len(q.Result.Data))
|
|
||||||
assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
|
|
||||||
assert.Equal(t, tc.expectedIntervalSeconds, q.Result.IntervalSeconds)
|
|
||||||
assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version)
|
|
||||||
assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID)
|
|
||||||
assert.Equal(t, alertDefinition.UID, q.Result.UID)
|
|
||||||
|
|
||||||
previousAlertDefinition = q.Result
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdatingConflictingAlertDefinition(t *testing.T) {
|
|
||||||
t.Run("should fail to update alert definition with reserved title", func(t *testing.T) {
|
|
||||||
mockTimeNow()
|
|
||||||
defer resetTimeNow()
|
|
||||||
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
var initialInterval int64 = 120
|
|
||||||
alertDef1 := createTestAlertDefinition(t, dbstore, initialInterval)
|
|
||||||
alertDef2 := createTestAlertDefinition(t, dbstore, initialInterval)
|
|
||||||
|
|
||||||
q := models.UpdateAlertDefinitionCommand{
|
|
||||||
UID: (*alertDef2).UID,
|
|
||||||
Title: alertDef1.Title,
|
|
||||||
Condition: "B",
|
|
||||||
Data: []models.AlertQuery{
|
|
||||||
{
|
|
||||||
Model: json.RawMessage(`{
|
|
||||||
"datasourceUid": "-100",
|
|
||||||
"type":"math",
|
|
||||||
"expression":"2 + 3 > 1"
|
|
||||||
}`),
|
|
||||||
RefID: "B",
|
|
||||||
RelativeTimeRange: models.RelativeTimeRange{
|
|
||||||
From: models.Duration(5 * time.Hour),
|
|
||||||
To: models.Duration(3 * time.Hour),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := dbstore.UpdateAlertDefinition(&q)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeletingAlertDefinition(t *testing.T) {
|
|
||||||
t.Run("zero rows affected when deleting unknown alert", func(t *testing.T) {
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
q := models.DeleteAlertDefinitionByUIDCommand{
|
|
||||||
UID: "unknown",
|
|
||||||
OrgID: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := dbstore.DeleteAlertDefinitionByUID(&q)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("deleting successfully existing alert", func(t *testing.T) {
|
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
|
||||||
t.Cleanup(registry.ClearOverrides)
|
|
||||||
|
|
||||||
alertDefinition := createTestAlertDefinition(t, dbstore, 60)
|
|
||||||
|
|
||||||
q := models.DeleteAlertDefinitionByUIDCommand{
|
|
||||||
UID: (*alertDefinition).UID,
|
|
||||||
OrgID: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
// save an instance for the definition
|
|
||||||
saveCmd := &models.SaveAlertInstanceCommand{
|
|
||||||
DefinitionOrgID: alertDefinition.OrgID,
|
|
||||||
DefinitionUID: alertDefinition.UID,
|
|
||||||
State: models.InstanceStateFiring,
|
|
||||||
Labels: models.InstanceLabels{"test": "testValue"},
|
|
||||||
}
|
|
||||||
err := dbstore.SaveAlertInstance(saveCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
listQuery := &models.ListAlertInstancesQuery{
|
|
||||||
DefinitionOrgID: alertDefinition.OrgID,
|
|
||||||
DefinitionUID: alertDefinition.UID,
|
|
||||||
}
|
|
||||||
err = dbstore.ListAlertInstances(listQuery)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, listQuery.Result, 1)
|
|
||||||
|
|
||||||
err = dbstore.DeleteAlertDefinitionByUID(&q)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// assert that alert instance is deleted
|
|
||||||
err = dbstore.ListAlertInstances(listQuery)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, listQuery.Result, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLongString(n int) string {
|
|
||||||
b := make([]rune, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = 'a'
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createTestAlertDefinition creates a dummy alert definition to be used by the tests.
|
|
||||||
func createTestAlertDefinition(t *testing.T, dbstore *store.DBstore, intervalSeconds int64) *models.AlertDefinition {
|
|
||||||
cmd := models.SaveAlertDefinitionCommand{
|
|
||||||
OrgID: 1,
|
|
||||||
Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)),
|
|
||||||
Condition: "A",
|
|
||||||
Data: []models.AlertQuery{
|
|
||||||
{
|
|
||||||
Model: json.RawMessage(`{
|
|
||||||
"datasourceUid": "-100",
|
|
||||||
"type":"math",
|
|
||||||
"expression":"2 + 2 > 1"
|
|
||||||
}`),
|
|
||||||
RelativeTimeRange: models.RelativeTimeRange{
|
|
||||||
From: models.Duration(5 * time.Hour),
|
|
||||||
To: models.Duration(3 * time.Hour),
|
|
||||||
},
|
|
||||||
RefID: "A",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
IntervalSeconds: &intervalSeconds,
|
|
||||||
}
|
|
||||||
err := dbstore.SaveAlertDefinition(&cmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("alert definition: %v with interval: %d created", cmd.Result.GetKey(), intervalSeconds)
|
|
||||||
return cmd.Result
|
|
||||||
}
|
|
@ -4,31 +4,44 @@ package tests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const baseIntervalSeconds = 10
|
||||||
|
|
||||||
|
func mockTimeNow() {
|
||||||
|
var timeSeed int64
|
||||||
|
store.TimeNow = func() time.Time {
|
||||||
|
fakeNow := time.Unix(timeSeed, 0).UTC()
|
||||||
|
timeSeed++
|
||||||
|
return fakeNow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAlertInstanceOperations(t *testing.T) {
|
func TestAlertInstanceOperations(t *testing.T) {
|
||||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||||
|
|
||||||
alertDefinition1 := createTestAlertDefinition(t, dbstore, 60)
|
alertRule1 := createTestAlertRule(t, dbstore, 60)
|
||||||
orgID := alertDefinition1.OrgID
|
orgID := alertRule1.OrgID
|
||||||
|
|
||||||
alertDefinition2 := createTestAlertDefinition(t, dbstore, 60)
|
alertRule2 := createTestAlertRule(t, dbstore, 60)
|
||||||
require.Equal(t, orgID, alertDefinition2.OrgID)
|
require.Equal(t, orgID, alertRule2.OrgID)
|
||||||
|
|
||||||
alertDefinition3 := createTestAlertDefinition(t, dbstore, 60)
|
alertRule3 := createTestAlertRule(t, dbstore, 60)
|
||||||
require.Equal(t, orgID, alertDefinition3.OrgID)
|
require.Equal(t, orgID, alertRule3.OrgID)
|
||||||
|
|
||||||
alertDefinition4 := createTestAlertDefinition(t, dbstore, 60)
|
alertRule4 := createTestAlertRule(t, dbstore, 60)
|
||||||
require.Equal(t, orgID, alertDefinition4.OrgID)
|
require.Equal(t, orgID, alertRule4.OrgID)
|
||||||
|
|
||||||
t.Run("can save and read new alert instance", func(t *testing.T) {
|
t.Run("can save and read new alert instance", func(t *testing.T) {
|
||||||
saveCmd := &models.SaveAlertInstanceCommand{
|
saveCmd := &models.SaveAlertInstanceCommand{
|
||||||
DefinitionOrgID: alertDefinition1.OrgID,
|
DefinitionOrgID: alertRule1.OrgID,
|
||||||
DefinitionUID: alertDefinition1.UID,
|
DefinitionUID: alertRule1.UID,
|
||||||
State: models.InstanceStateFiring,
|
State: models.InstanceStateFiring,
|
||||||
Labels: models.InstanceLabels{"test": "testValue"},
|
Labels: models.InstanceLabels{"test": "testValue"},
|
||||||
}
|
}
|
||||||
@ -45,14 +58,14 @@ func TestAlertInstanceOperations(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
|
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
|
||||||
require.Equal(t, alertDefinition1.OrgID, getCmd.Result.DefinitionOrgID)
|
require.Equal(t, alertRule1.OrgID, getCmd.Result.DefinitionOrgID)
|
||||||
require.Equal(t, alertDefinition1.UID, getCmd.Result.DefinitionUID)
|
require.Equal(t, alertRule1.UID, getCmd.Result.DefinitionUID)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("can save and read new alert instance with no labels", func(t *testing.T) {
|
t.Run("can save and read new alert instance with no labels", func(t *testing.T) {
|
||||||
saveCmd := &models.SaveAlertInstanceCommand{
|
saveCmd := &models.SaveAlertInstanceCommand{
|
||||||
DefinitionOrgID: alertDefinition2.OrgID,
|
DefinitionOrgID: alertRule2.OrgID,
|
||||||
DefinitionUID: alertDefinition2.UID,
|
DefinitionUID: alertRule2.UID,
|
||||||
State: models.InstanceStateNormal,
|
State: models.InstanceStateNormal,
|
||||||
Labels: models.InstanceLabels{},
|
Labels: models.InstanceLabels{},
|
||||||
}
|
}
|
||||||
@ -67,15 +80,15 @@ func TestAlertInstanceOperations(t *testing.T) {
|
|||||||
err = dbstore.GetAlertInstance(getCmd)
|
err = dbstore.GetAlertInstance(getCmd)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, alertDefinition2.OrgID, getCmd.Result.DefinitionOrgID)
|
require.Equal(t, alertRule2.OrgID, getCmd.Result.DefinitionOrgID)
|
||||||
require.Equal(t, alertDefinition2.UID, getCmd.Result.DefinitionUID)
|
require.Equal(t, alertRule2.UID, getCmd.Result.DefinitionUID)
|
||||||
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
|
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("can save two instances with same org_id, uid and different labels", func(t *testing.T) {
|
t.Run("can save two instances with same org_id, uid and different labels", func(t *testing.T) {
|
||||||
saveCmdOne := &models.SaveAlertInstanceCommand{
|
saveCmdOne := &models.SaveAlertInstanceCommand{
|
||||||
DefinitionOrgID: alertDefinition3.OrgID,
|
DefinitionOrgID: alertRule3.OrgID,
|
||||||
DefinitionUID: alertDefinition3.UID,
|
DefinitionUID: alertRule3.UID,
|
||||||
State: models.InstanceStateFiring,
|
State: models.InstanceStateFiring,
|
||||||
Labels: models.InstanceLabels{"test": "testValue"},
|
Labels: models.InstanceLabels{"test": "testValue"},
|
||||||
}
|
}
|
||||||
@ -128,8 +141,8 @@ func TestAlertInstanceOperations(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
|
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
|
||||||
saveCmdOne := &models.SaveAlertInstanceCommand{
|
saveCmdOne := &models.SaveAlertInstanceCommand{
|
||||||
DefinitionOrgID: alertDefinition4.OrgID,
|
DefinitionOrgID: alertRule4.OrgID,
|
||||||
DefinitionUID: alertDefinition4.UID,
|
DefinitionUID: alertRule4.UID,
|
||||||
State: models.InstanceStateFiring,
|
State: models.InstanceStateFiring,
|
||||||
Labels: models.InstanceLabels{"test": "testValue"},
|
Labels: models.InstanceLabels{"test": "testValue"},
|
||||||
}
|
}
|
||||||
@ -147,8 +160,8 @@ func TestAlertInstanceOperations(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
listQuery := &models.ListAlertInstancesQuery{
|
listQuery := &models.ListAlertInstancesQuery{
|
||||||
DefinitionOrgID: alertDefinition4.OrgID,
|
DefinitionOrgID: alertRule4.OrgID,
|
||||||
DefinitionUID: alertDefinition4.UID,
|
DefinitionUID: alertRule4.UID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dbstore.ListAlertInstances(listQuery)
|
err = dbstore.ListAlertInstances(listQuery)
|
||||||
@ -160,7 +173,5 @@ func TestAlertInstanceOperations(t *testing.T) {
|
|||||||
require.Equal(t, saveCmdTwo.DefinitionUID, listQuery.Result[0].DefinitionUID)
|
require.Equal(t, saveCmdTwo.DefinitionUID, listQuery.Result[0].DefinitionUID)
|
||||||
require.Equal(t, saveCmdTwo.Labels, listQuery.Result[0].Labels)
|
require.Equal(t, saveCmdTwo.Labels, listQuery.Result[0].Labels)
|
||||||
require.Equal(t, saveCmdTwo.State, listQuery.Result[0].CurrentState)
|
require.Equal(t, saveCmdTwo.State, listQuery.Result[0].CurrentState)
|
||||||
require.NotEmpty(t, listQuery.Result[0].DefinitionTitle)
|
|
||||||
require.Equal(t, alertDefinition4.Title, listQuery.Result[0].DefinitionTitle)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user