mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AlertingNG: Split into several packages (#31719)
* AlertingNG: Split into several packages * Move AlertQuery to models
This commit is contained in:
parent
124ef813ab
commit
4ce0a49eac
1
go.mod
1
go.mod
@ -68,6 +68,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.18.0
|
||||
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 // indirect
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/goxmldsig v1.1.0
|
||||
|
4
go.sum
4
go.sum
@ -1303,6 +1303,10 @@ github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4 h1:54z99l8
|
||||
github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4/go.mod h1:XYjkJiog7fyQu3puQNivZPI2pNq1C/775EIoHfDvuvY=
|
||||
github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/quasilyte/go-ruleguard v0.3.1 h1:2KTXnHBCR4BUl8UAL2bCUorOBGC8RsmYncuDA9NEFW4=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.1 h1:CHGOKP2LDz35P49TjW4Bx4BCfFI6ZZU/8zcneECD0q4=
|
||||
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY=
|
||||
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM=
|
||||
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
|
@ -1,22 +0,0 @@
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// timeNow makes it possible to test usage of time
|
||||
var timeNow = time.Now
|
||||
|
||||
// preSave sets datasource and loads the updated model for each alert query.
|
||||
func (alertDefinition *AlertDefinition) preSave() 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
|
||||
}
|
@ -1,7 +1,13 @@
|
||||
package ngalert
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
|
||||
"github.com/go-macaron/binding"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
@ -17,26 +23,31 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type apiImpl struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
// timeNow makes it possible to test usage of time
|
||||
var timeNow = time.Now
|
||||
|
||||
// API handlers.
|
||||
type API struct {
|
||||
Cfg *setting.Cfg
|
||||
DatasourceCache datasources.CacheService
|
||||
RouteRegister routing.RouteRegister
|
||||
DataService *tsdb.Service
|
||||
schedule scheduleService
|
||||
store store
|
||||
Schedule schedule.ScheduleService
|
||||
Store store.Store
|
||||
}
|
||||
|
||||
func (api *apiImpl) registerAPIEndpoints() {
|
||||
// RegisterAPIEndpoints registers API handlers
|
||||
func (api *API) RegisterAPIEndpoints() {
|
||||
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(evalAlertConditionCommand{}), routing.Wrap(api.conditionEvalEndpoint))
|
||||
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(saveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint))
|
||||
alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(updateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint))
|
||||
alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(updateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint))
|
||||
alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(updateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint))
|
||||
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))
|
||||
})
|
||||
|
||||
api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) {
|
||||
@ -50,7 +61,7 @@ func (api *apiImpl) registerAPIEndpoints() {
|
||||
}
|
||||
|
||||
// conditionEvalEndpoint handles POST /api/alert-definitions/eval.
|
||||
func (api *apiImpl) conditionEvalEndpoint(c *models.ReqContext, cmd evalAlertConditionCommand) response.Response {
|
||||
func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response {
|
||||
evalCond := eval.Condition{
|
||||
RefID: cmd.Condition,
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
@ -84,7 +95,7 @@ func (api *apiImpl) conditionEvalEndpoint(c *models.ReqContext, cmd evalAlertCon
|
||||
}
|
||||
|
||||
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
|
||||
func (api *apiImpl) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response {
|
||||
func (api *API) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response {
|
||||
alertDefinitionUID := c.Params(":alertDefinitionUID")
|
||||
|
||||
condition, err := api.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId)
|
||||
@ -118,15 +129,15 @@ func (api *apiImpl) alertDefinitionEvalEndpoint(c *models.ReqContext) response.R
|
||||
}
|
||||
|
||||
// getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionUID.
|
||||
func (api *apiImpl) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
|
||||
func (api *API) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
|
||||
alertDefinitionUID := c.Params(":alertDefinitionUID")
|
||||
|
||||
query := getAlertDefinitionByUIDQuery{
|
||||
query := ngmodels.GetAlertDefinitionByUIDQuery{
|
||||
UID: alertDefinitionUID,
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
}
|
||||
|
||||
if err := api.store.getAlertDefinitionByUID(&query); err != nil {
|
||||
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
|
||||
return response.Error(500, "Failed to get alert definition", err)
|
||||
}
|
||||
|
||||
@ -134,15 +145,15 @@ func (api *apiImpl) getAlertDefinitionEndpoint(c *models.ReqContext) response.Re
|
||||
}
|
||||
|
||||
// deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionUID.
|
||||
func (api *apiImpl) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
|
||||
func (api *API) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
|
||||
alertDefinitionUID := c.Params(":alertDefinitionUID")
|
||||
|
||||
cmd := deleteAlertDefinitionByUIDCommand{
|
||||
cmd := ngmodels.DeleteAlertDefinitionByUIDCommand{
|
||||
UID: alertDefinitionUID,
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
}
|
||||
|
||||
if err := api.store.deleteAlertDefinitionByUID(&cmd); err != nil {
|
||||
if err := api.Store.DeleteAlertDefinitionByUID(&cmd); err != nil {
|
||||
return response.Error(500, "Failed to delete alert definition", err)
|
||||
}
|
||||
|
||||
@ -150,7 +161,7 @@ func (api *apiImpl) deleteAlertDefinitionEndpoint(c *models.ReqContext) response
|
||||
}
|
||||
|
||||
// updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionUID.
|
||||
func (api *apiImpl) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updateAlertDefinitionCommand) response.Response {
|
||||
func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionCommand) response.Response {
|
||||
cmd.UID = c.Params(":alertDefinitionUID")
|
||||
cmd.OrgID = c.SignedInUser.OrgId
|
||||
|
||||
@ -163,7 +174,7 @@ func (api *apiImpl) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd upda
|
||||
return response.Error(400, "invalid condition", err)
|
||||
}
|
||||
|
||||
if err := api.store.updateAlertDefinition(&cmd); err != nil {
|
||||
if err := api.Store.UpdateAlertDefinition(&cmd); err != nil {
|
||||
return response.Error(500, "Failed to update alert definition", err)
|
||||
}
|
||||
|
||||
@ -171,7 +182,7 @@ func (api *apiImpl) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd upda
|
||||
}
|
||||
|
||||
// createAlertDefinitionEndpoint handles POST /api/alert-definitions.
|
||||
func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd saveAlertDefinitionCommand) response.Response {
|
||||
func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.SaveAlertDefinitionCommand) response.Response {
|
||||
cmd.OrgID = c.SignedInUser.OrgId
|
||||
|
||||
evalCond := eval.Condition{
|
||||
@ -183,7 +194,7 @@ func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd save
|
||||
return response.Error(400, "invalid condition", err)
|
||||
}
|
||||
|
||||
if err := api.store.saveAlertDefinition(&cmd); err != nil {
|
||||
if err := api.Store.SaveAlertDefinition(&cmd); err != nil {
|
||||
return response.Error(500, "Failed to create alert definition", err)
|
||||
}
|
||||
|
||||
@ -191,26 +202,26 @@ func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd save
|
||||
}
|
||||
|
||||
// listAlertDefinitions handles GET /api/alert-definitions.
|
||||
func (api *apiImpl) listAlertDefinitions(c *models.ReqContext) response.Response {
|
||||
query := listAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId}
|
||||
func (api *API) listAlertDefinitions(c *models.ReqContext) response.Response {
|
||||
query := ngmodels.ListAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId}
|
||||
|
||||
if err := api.store.getOrgAlertDefinitions(&query); err != nil {
|
||||
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 *apiImpl) pauseScheduler() response.Response {
|
||||
err := api.schedule.Pause()
|
||||
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 *apiImpl) unpauseScheduler() response.Response {
|
||||
err := api.schedule.Unpause()
|
||||
func (api *API) unpauseScheduler() response.Response {
|
||||
err := api.Schedule.Unpause()
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to unpause scheduler", err)
|
||||
}
|
||||
@ -218,11 +229,11 @@ func (api *apiImpl) unpauseScheduler() response.Response {
|
||||
}
|
||||
|
||||
// alertDefinitionPauseEndpoint handles POST /api/alert-definitions/pause.
|
||||
func (api *apiImpl) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd updateAlertDefinitionPausedCommand) response.Response {
|
||||
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)
|
||||
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to pause alert definition", err)
|
||||
}
|
||||
@ -230,13 +241,70 @@ func (api *apiImpl) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd updat
|
||||
}
|
||||
|
||||
// alertDefinitionUnpauseEndpoint handles POST /api/alert-definitions/unpause.
|
||||
func (api *apiImpl) alertDefinitionUnpauseEndpoint(c *models.ReqContext, cmd updateAlertDefinitionPausedCommand) response.Response {
|
||||
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)
|
||||
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) (*eval.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 &eval.Condition{
|
||||
RefID: alertDefinition.Condition,
|
||||
OrgID: alertDefinition.OrgID,
|
||||
QueriesAndExpressions: alertDefinition.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *API) validateCondition(c eval.Condition, user *models.SignedInUser, skipCache bool) error {
|
||||
var refID string
|
||||
|
||||
if len(c.QueriesAndExpressions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, query := range c.QueriesAndExpressions {
|
||||
if c.RefID == query.RefID {
|
||||
refID = c.RefID
|
||||
}
|
||||
|
||||
datasourceUID, err := query.GetDatasource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isExpression, err := query.IsExpression()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isExpression {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = api.DatasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if refID == "" {
|
||||
return fmt.Errorf("condition %s not found in any query or expression", c.RefID)
|
||||
}
|
||||
return nil
|
||||
}
|
19
pkg/services/ngalert/api/instance_api.go
Normal file
19
pkg/services/ngalert/api/instance_api.go
Normal file
@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
23
pkg/services/ngalert/api/middleware.go
Normal file
23
pkg/services/ngalert/api/middleware.go
Normal file
@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
@ -46,7 +48,7 @@ type Condition struct {
|
||||
RefID string `json:"refId"`
|
||||
OrgID int64 `json:"-"`
|
||||
|
||||
QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"`
|
||||
QueriesAndExpressions []models.AlertQuery `json:"queriesAndExpressions"`
|
||||
}
|
||||
|
||||
// ExecutionResults contains the unevaluated results from executing
|
||||
@ -117,16 +119,16 @@ func (c *Condition) execute(ctx AlertExecCtx, now time.Time, dataService *tsdb.S
|
||||
|
||||
for i := range c.QueriesAndExpressions {
|
||||
q := c.QueriesAndExpressions[i]
|
||||
model, err := q.getModel()
|
||||
model, err := q.GetModel()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get query model: %w", err)
|
||||
}
|
||||
interval, err := q.getIntervalDuration()
|
||||
interval, err := q.GetIntervalDuration()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err)
|
||||
}
|
||||
|
||||
maxDatapoints, err := q.getMaxDatapoints()
|
||||
maxDatapoints, err := q.GetMaxDatapoints()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err)
|
||||
}
|
||||
@ -137,7 +139,7 @@ func (c *Condition) execute(ctx AlertExecCtx, now time.Time, dataService *tsdb.S
|
||||
RefID: q.RefID,
|
||||
MaxDataPoints: maxDatapoints,
|
||||
QueryType: q.QueryType,
|
||||
TimeRange: q.RelativeTimeRange.toTimeRange(now),
|
||||
TimeRange: q.RelativeTimeRange.ToTimeRange(now),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (sch *schedule) fetchAllDetails(now time.Time) []*AlertDefinition {
|
||||
q := listAlertDefinitionsQuery{}
|
||||
err := sch.store.getAlertDefinitions(&q)
|
||||
if err != nil {
|
||||
sch.log.Error("failed to fetch alert definitions", "now", now, "err", err)
|
||||
return nil
|
||||
}
|
||||
return q.Result
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// listAlertInstancesEndpoint handles GET /api/alert-instances.
|
||||
func (api *apiImpl) listAlertInstancesEndpoint(c *models.ReqContext) response.Response {
|
||||
cmd := 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)
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
// +build integration
|
||||
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAlertInstanceOperations(t *testing.T) {
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
|
||||
alertDefinition1 := createTestAlertDefinition(t, store, 60)
|
||||
orgID := alertDefinition1.OrgID
|
||||
|
||||
alertDefinition2 := createTestAlertDefinition(t, store, 60)
|
||||
require.Equal(t, orgID, alertDefinition2.OrgID)
|
||||
|
||||
alertDefinition3 := createTestAlertDefinition(t, store, 60)
|
||||
require.Equal(t, orgID, alertDefinition3.OrgID)
|
||||
|
||||
alertDefinition4 := createTestAlertDefinition(t, store, 60)
|
||||
require.Equal(t, orgID, alertDefinition4.OrgID)
|
||||
|
||||
t.Run("can save and read new alert instance", func(t *testing.T) {
|
||||
saveCmd := &saveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition1.OrgID,
|
||||
DefinitionUID: alertDefinition1.UID,
|
||||
State: InstanceStateFiring,
|
||||
Labels: InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
err := store.saveAlertInstance(saveCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
getCmd := &getAlertInstanceQuery{
|
||||
DefinitionOrgID: saveCmd.DefinitionOrgID,
|
||||
DefinitionUID: saveCmd.DefinitionUID,
|
||||
Labels: InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
|
||||
err = store.getAlertInstance(getCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
|
||||
require.Equal(t, alertDefinition1.OrgID, getCmd.Result.DefinitionOrgID)
|
||||
require.Equal(t, alertDefinition1.UID, getCmd.Result.DefinitionUID)
|
||||
})
|
||||
|
||||
t.Run("can save and read new alert instance with no labels", func(t *testing.T) {
|
||||
saveCmd := &saveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition2.OrgID,
|
||||
DefinitionUID: alertDefinition2.UID,
|
||||
State: InstanceStateNormal,
|
||||
}
|
||||
err := store.saveAlertInstance(saveCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
getCmd := &getAlertInstanceQuery{
|
||||
DefinitionOrgID: saveCmd.DefinitionOrgID,
|
||||
DefinitionUID: saveCmd.DefinitionUID,
|
||||
}
|
||||
|
||||
err = store.getAlertInstance(getCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, alertDefinition2.OrgID, getCmd.Result.DefinitionOrgID)
|
||||
require.Equal(t, alertDefinition2.UID, getCmd.Result.DefinitionUID)
|
||||
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) {
|
||||
saveCmdOne := &saveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition3.OrgID,
|
||||
DefinitionUID: alertDefinition3.UID,
|
||||
State: InstanceStateFiring,
|
||||
Labels: InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
|
||||
err := store.saveAlertInstance(saveCmdOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
saveCmdTwo := &saveAlertInstanceCommand{
|
||||
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
|
||||
DefinitionUID: saveCmdOne.DefinitionUID,
|
||||
State: InstanceStateFiring,
|
||||
Labels: InstanceLabels{"test": "meow"},
|
||||
}
|
||||
err = store.saveAlertInstance(saveCmdTwo)
|
||||
require.NoError(t, err)
|
||||
|
||||
listCommand := &listAlertInstancesQuery{
|
||||
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
|
||||
DefinitionUID: saveCmdOne.DefinitionUID,
|
||||
}
|
||||
|
||||
err = store.listAlertInstances(listCommand)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listCommand.Result, 2)
|
||||
})
|
||||
|
||||
t.Run("can list all added instances in org", func(t *testing.T) {
|
||||
listCommand := &listAlertInstancesQuery{
|
||||
DefinitionOrgID: orgID,
|
||||
}
|
||||
|
||||
err := store.listAlertInstances(listCommand)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listCommand.Result, 4)
|
||||
})
|
||||
|
||||
t.Run("can list all added instances in org filtered by current state", func(t *testing.T) {
|
||||
listCommand := &listAlertInstancesQuery{
|
||||
DefinitionOrgID: orgID,
|
||||
State: InstanceStateNormal,
|
||||
}
|
||||
|
||||
err := store.listAlertInstances(listCommand)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listCommand.Result, 1)
|
||||
})
|
||||
|
||||
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
|
||||
saveCmdOne := &saveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition4.OrgID,
|
||||
DefinitionUID: alertDefinition4.UID,
|
||||
State: InstanceStateFiring,
|
||||
Labels: InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
|
||||
err := store.saveAlertInstance(saveCmdOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
saveCmdTwo := &saveAlertInstanceCommand{
|
||||
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
|
||||
DefinitionUID: saveCmdOne.DefinitionUID,
|
||||
State: InstanceStateNormal,
|
||||
Labels: InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
err = store.saveAlertInstance(saveCmdTwo)
|
||||
require.NoError(t, err)
|
||||
|
||||
listCommand := &listAlertInstancesQuery{
|
||||
DefinitionOrgID: alertDefinition4.OrgID,
|
||||
DefinitionUID: alertDefinition4.UID,
|
||||
}
|
||||
|
||||
err = store.listAlertInstances(listCommand)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listCommand.Result, 1)
|
||||
|
||||
require.Equal(t, saveCmdTwo.DefinitionOrgID, listCommand.Result[0].DefinitionOrgID)
|
||||
require.Equal(t, saveCmdTwo.DefinitionUID, listCommand.Result[0].DefinitionUID)
|
||||
require.Equal(t, saveCmdTwo.Labels, listCommand.Result[0].Labels)
|
||||
require.Equal(t, saveCmdTwo.State, listCommand.Result[0].CurrentState)
|
||||
require.NotEmpty(t, listCommand.Result[0].DefinitionTitle)
|
||||
require.Equal(t, alertDefinition4.Title, listCommand.Result[0].DefinitionTitle)
|
||||
})
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func (api *apiImpl) validateOrgAlertDefinition(c *models.ReqContext) {
|
||||
uid := c.ParamsEscape(":alertDefinitionUID")
|
||||
|
||||
if uid == "" {
|
||||
c.JsonApiErr(403, "Permission denied", nil)
|
||||
return
|
||||
}
|
||||
|
||||
query := 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,115 +0,0 @@
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
)
|
||||
|
||||
var errAlertDefinitionFailedGenerateUniqueUID = errors.New("failed to generate alert definition UID")
|
||||
|
||||
// AlertDefinition is the model for alert definitions in Alerting NG.
|
||||
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 []eval.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"`
|
||||
}
|
||||
|
||||
type alertDefinitionKey struct {
|
||||
orgID int64
|
||||
definitionUID string
|
||||
}
|
||||
|
||||
func (k alertDefinitionKey) String() string {
|
||||
return fmt.Sprintf("{orgID: %d, definitionUID: %s}", k.orgID, k.definitionUID)
|
||||
}
|
||||
|
||||
func (alertDefinition *AlertDefinition) getKey() alertDefinitionKey {
|
||||
return alertDefinitionKey{orgID: alertDefinition.OrgID, definitionUID: alertDefinition.UID}
|
||||
}
|
||||
|
||||
// AlertDefinitionVersion is the model for alert definition versions in Alerting NG.
|
||||
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 []eval.AlertQuery
|
||||
IntervalSeconds int64
|
||||
}
|
||||
|
||||
var (
|
||||
// errAlertDefinitionNotFound is an error for an unknown alert definition.
|
||||
errAlertDefinitionNotFound = fmt.Errorf("could not find alert definition")
|
||||
)
|
||||
|
||||
// getAlertDefinitionByUIDQuery is the query for retrieving/deleting an alert definition by UID and organisation ID.
|
||||
type getAlertDefinitionByUIDQuery struct {
|
||||
UID string
|
||||
OrgID int64
|
||||
|
||||
Result *AlertDefinition
|
||||
}
|
||||
|
||||
type deleteAlertDefinitionByUIDCommand struct {
|
||||
UID string
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
// saveAlertDefinitionCommand is the query for saving a new alert definition.
|
||||
type saveAlertDefinitionCommand struct {
|
||||
Title string `json:"title"`
|
||||
OrgID int64 `json:"-"`
|
||||
Condition string `json:"condition"`
|
||||
Data []eval.AlertQuery `json:"data"`
|
||||
IntervalSeconds *int64 `json:"intervalSeconds"`
|
||||
|
||||
Result *AlertDefinition
|
||||
}
|
||||
|
||||
// updateAlertDefinitionCommand is the query for updating an existing alert definition.
|
||||
type updateAlertDefinitionCommand struct {
|
||||
Title string `json:"title"`
|
||||
OrgID int64 `json:"-"`
|
||||
Condition string `json:"condition"`
|
||||
Data []eval.AlertQuery `json:"data"`
|
||||
IntervalSeconds *int64 `json:"intervalSeconds"`
|
||||
UID string `json:"-"`
|
||||
|
||||
Result *AlertDefinition
|
||||
}
|
||||
|
||||
type evalAlertConditionCommand struct {
|
||||
Condition string `json:"condition"`
|
||||
Data []eval.AlertQuery `json:"data"`
|
||||
Now time.Time `json:"now"`
|
||||
}
|
||||
|
||||
type listAlertDefinitionsQuery struct {
|
||||
OrgID int64 `json:"-"`
|
||||
|
||||
Result []*AlertDefinition
|
||||
}
|
||||
|
||||
type updateAlertDefinitionPausedCommand struct {
|
||||
OrgID int64 `json:"-"`
|
||||
UIDs []string `json:"uids"`
|
||||
Paused bool `json:"-"`
|
||||
|
||||
ResultCount int64
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eval
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -49,7 +49,7 @@ func (rtr *RelativeTimeRange) isValid() bool {
|
||||
return rtr.From > rtr.To
|
||||
}
|
||||
|
||||
func (rtr *RelativeTimeRange) toTimeRange(now time.Time) backend.TimeRange {
|
||||
func (rtr *RelativeTimeRange) ToTimeRange(now time.Time) backend.TimeRange {
|
||||
return backend.TimeRange{
|
||||
From: now.Add(-time.Duration(rtr.From)),
|
||||
To: now.Add(-time.Duration(rtr.To)),
|
||||
@ -147,7 +147,7 @@ func (aq *AlertQuery) setMaxDatapoints() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) getMaxDatapoints() (int64, error) {
|
||||
func (aq *AlertQuery) GetMaxDatapoints() (int64, error) {
|
||||
err := aq.setMaxDatapoints()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -192,7 +192,7 @@ func (aq *AlertQuery) getIntervalMS() (int64, error) {
|
||||
return int64(intervalMs), nil
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) getIntervalDuration() (time.Duration, error) {
|
||||
func (aq *AlertQuery) GetIntervalDuration() (time.Duration, error) {
|
||||
err := aq.setIntervalMS()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -214,7 +214,7 @@ func (aq *AlertQuery) GetDatasource() (string, error) {
|
||||
return aq.DatasourceUID, nil
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) getModel() ([]byte, error) {
|
||||
func (aq *AlertQuery) GetModel() ([]byte, error) {
|
||||
err := aq.setDatasource()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -270,7 +270,7 @@ func (aq *AlertQuery) PreSave() error {
|
||||
}
|
||||
|
||||
// override model
|
||||
model, err := aq.getModel()
|
||||
model, err := aq.GetModel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eval
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -186,7 +186,7 @@ func TestAlertQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("can update model maxDataPoints (if missing)", func(t *testing.T) {
|
||||
maxDataPoints, err := tc.alertQuery.getMaxDatapoints()
|
||||
maxDataPoints, err := tc.alertQuery.GetMaxDatapoints()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedMaxPoints, maxDataPoints)
|
||||
})
|
||||
@ -198,7 +198,7 @@ func TestAlertQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("can get the updated model with the default properties (if missing)", func(t *testing.T) {
|
||||
blob, err := tc.alertQuery.getModel()
|
||||
blob, err := tc.alertQuery.GetModel()
|
||||
require.NoError(t, err)
|
||||
model := make(map[string]interface{})
|
||||
err = json.Unmarshal(blob, &model)
|
@ -1,4 +1,4 @@
|
||||
package ngalert
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -33,8 +33,8 @@ func (i InstanceStateType) IsValid() bool {
|
||||
i == InstanceStateNormal
|
||||
}
|
||||
|
||||
// saveAlertInstanceCommand is the query for saving a new alert instance.
|
||||
type saveAlertInstanceCommand struct {
|
||||
// SaveAlertInstanceCommand is the query for saving a new alert instance.
|
||||
type SaveAlertInstanceCommand struct {
|
||||
DefinitionOrgID int64
|
||||
DefinitionUID string
|
||||
Labels InstanceLabels
|
||||
@ -42,9 +42,9 @@ type saveAlertInstanceCommand struct {
|
||||
LastEvalTime time.Time
|
||||
}
|
||||
|
||||
// getAlertDefinitionByIDQuery is the query for retrieving/deleting an alert definition by ID.
|
||||
// GetAlertInstanceQuery is the query for retrieving/deleting an alert definition by ID.
|
||||
// nolint:unused
|
||||
type getAlertInstanceQuery struct {
|
||||
type GetAlertInstanceQuery struct {
|
||||
DefinitionOrgID int64
|
||||
DefinitionUID string
|
||||
Labels InstanceLabels
|
||||
@ -52,17 +52,17 @@ type getAlertInstanceQuery struct {
|
||||
Result *AlertInstance
|
||||
}
|
||||
|
||||
// listAlertInstancesCommand is the query list alert Instances.
|
||||
type listAlertInstancesQuery struct {
|
||||
// ListAlertInstancesQuery is the query list alert Instances.
|
||||
type ListAlertInstancesQuery struct {
|
||||
DefinitionOrgID int64 `json:"-"`
|
||||
DefinitionUID string
|
||||
State InstanceStateType
|
||||
|
||||
Result []*listAlertInstancesQueryResult
|
||||
Result []*ListAlertInstancesQueryResult
|
||||
}
|
||||
|
||||
// listAlertInstancesQueryResult represents the result of listAlertInstancesQuery.
|
||||
type listAlertInstancesQueryResult struct {
|
||||
// ListAlertInstancesQueryResult represents the result of listAlertInstancesQuery.
|
||||
type ListAlertInstancesQueryResult struct {
|
||||
DefinitionOrgID int64 `xorm:"def_org_id" json:"definitionOrgId"`
|
||||
DefinitionUID string `xorm:"def_uid" json:"definitionUid"`
|
||||
DefinitionTitle string `xorm:"def_title" json:"definitionTitle"`
|
||||
@ -73,9 +73,9 @@ type listAlertInstancesQueryResult struct {
|
||||
LastEvalTime time.Time `json:"lastEvalTime"`
|
||||
}
|
||||
|
||||
// validateAlertInstance validates that the alert instance contains an alert definition id,
|
||||
// ValidateAlertInstance validates that the alert instance contains an alert definition id,
|
||||
// and state.
|
||||
func validateAlertInstance(alertInstance *AlertInstance) error {
|
||||
func ValidateAlertInstance(alertInstance *AlertInstance) error {
|
||||
if alertInstance == nil {
|
||||
return fmt.Errorf("alert instance is invalid because it is nil")
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ngalert
|
||||
package models
|
||||
|
||||
import (
|
||||
// nolint:gosec
|
147
pkg/services/ngalert/models/models.go
Normal file
147
pkg/services/ngalert/models/models.go
Normal file
@ -0,0 +1,147 @@
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
type GetAlertDefinitionByUIDQuery struct {
|
||||
UID string
|
||||
OrgID int64
|
||||
|
||||
Result *AlertDefinition
|
||||
}
|
||||
|
||||
// DeleteAlertDefinitionByUIDCommand is the command for deleting an alert definition
|
||||
type DeleteAlertDefinitionByUIDCommand struct {
|
||||
UID string
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
// SaveAlertDefinitionCommand is the query for saving a new alert definition.
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
type ListAlertDefinitionsQuery struct {
|
||||
OrgID int64 `json:"-"`
|
||||
|
||||
Result []*AlertDefinition
|
||||
}
|
||||
|
||||
// UpdateAlertDefinitionPausedCommand is the command for updating an alert definitions
|
||||
type UpdateAlertDefinitionPausedCommand struct {
|
||||
OrgID int64 `json:"-"`
|
||||
UIDs []string `json:"uids"`
|
||||
Paused bool `json:"-"`
|
||||
|
||||
ResultCount int64
|
||||
}
|
||||
|
||||
// Condition contains backend expressions and queries and the RefID
|
||||
// of the query or expression that will be evaluated.
|
||||
type Condition struct {
|
||||
RefID string `json:"refId"`
|
||||
OrgID int64 `json:"-"`
|
||||
|
||||
QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"`
|
||||
}
|
||||
|
||||
// IsValid checks the condition's validity.
|
||||
func (c Condition) IsValid() bool {
|
||||
// TODO search for refIDs in QueriesAndExpressions
|
||||
return len(c.QueriesAndExpressions) != 0
|
||||
}
|
@ -4,6 +4,11 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@ -36,8 +41,8 @@ type AlertNG struct {
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
SQLStore *sqlstore.SQLStore `inject:""`
|
||||
DataService *tsdb.Service `inject:""`
|
||||
log log.Logger
|
||||
schedule scheduleService
|
||||
Log log.Logger
|
||||
schedule schedule.ScheduleService
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -46,37 +51,37 @@ func init() {
|
||||
|
||||
// Init initializes the AlertingService.
|
||||
func (ng *AlertNG) Init() error {
|
||||
ng.log = log.New("ngalert")
|
||||
ng.Log = log.New("ngalert")
|
||||
|
||||
baseInterval := baseIntervalSeconds * time.Second
|
||||
|
||||
store := storeImpl{baseInterval: baseInterval, SQLStore: ng.SQLStore}
|
||||
store := store.DBstore{BaseInterval: baseInterval, DefaultIntervalSeconds: defaultIntervalSeconds, SQLStore: ng.SQLStore}
|
||||
|
||||
schedCfg := schedulerCfg{
|
||||
c: clock.New(),
|
||||
baseInterval: baseInterval,
|
||||
logger: ng.log,
|
||||
evaluator: eval.Evaluator{Cfg: ng.Cfg},
|
||||
store: store,
|
||||
schedCfg := schedule.SchedulerCfg{
|
||||
C: clock.New(),
|
||||
BaseInterval: baseInterval,
|
||||
Logger: ng.Log,
|
||||
MaxAttempts: maxAttempts,
|
||||
Evaluator: eval.Evaluator{Cfg: ng.Cfg},
|
||||
Store: store,
|
||||
}
|
||||
ng.schedule = newScheduler(schedCfg, ng.DataService)
|
||||
ng.schedule = schedule.NewScheduler(schedCfg, ng.DataService)
|
||||
|
||||
api := apiImpl{
|
||||
api := api.API{
|
||||
Cfg: ng.Cfg,
|
||||
DatasourceCache: ng.DatasourceCache,
|
||||
RouteRegister: ng.RouteRegister,
|
||||
DataService: ng.DataService,
|
||||
schedule: ng.schedule,
|
||||
store: store,
|
||||
}
|
||||
api.registerAPIEndpoints()
|
||||
Schedule: ng.schedule,
|
||||
Store: store}
|
||||
api.RegisterAPIEndpoints()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts the scheduler
|
||||
func (ng *AlertNG) Run(ctx context.Context) error {
|
||||
ng.log.Debug("ngalert starting")
|
||||
ng.Log.Debug("ngalert starting")
|
||||
return ng.schedule.Ticker(ctx)
|
||||
}
|
||||
|
||||
@ -100,23 +105,3 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) {
|
||||
// Create alert_instance table
|
||||
alertInstanceMigration(mg)
|
||||
}
|
||||
|
||||
// LoadAlertCondition returns a Condition object for the given alertDefinitionID.
|
||||
func (api *apiImpl) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*eval.Condition, error) {
|
||||
q := 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 &eval.Condition{
|
||||
RefID: alertDefinition.Condition,
|
||||
OrgID: alertDefinition.OrgID,
|
||||
QueriesAndExpressions: alertDefinition.Data,
|
||||
}, nil
|
||||
}
|
||||
|
17
pkg/services/ngalert/schedule/fetcher.go
Normal file
17
pkg/services/ngalert/schedule/fetcher.go
Normal file
@ -0,0 +1,17 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
func (sch *schedule) fetchAllDetails(now time.Time) []*models.AlertDefinition {
|
||||
q := models.ListAlertDefinitionsQuery{}
|
||||
err := sch.store.GetAlertDefinitions(&q)
|
||||
if err != nil {
|
||||
sch.log.Error("failed to fetch alert definitions", "now", now, "err", err)
|
||||
return nil
|
||||
}
|
||||
return q.Result
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ngalert
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -6,6 +6,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
@ -14,25 +18,29 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type scheduleService interface {
|
||||
// timeNow makes it possible to test usage of time
|
||||
var timeNow = time.Now
|
||||
|
||||
// ScheduleService handles scheduling
|
||||
type ScheduleService interface {
|
||||
Ticker(context.Context) error
|
||||
Pause() error
|
||||
Unpause() error
|
||||
|
||||
// the following are used by tests only used for tests
|
||||
evalApplied(alertDefinitionKey, time.Time)
|
||||
stopApplied(alertDefinitionKey)
|
||||
overrideCfg(cfg schedulerCfg)
|
||||
evalApplied(models.AlertDefinitionKey, time.Time)
|
||||
stopApplied(models.AlertDefinitionKey)
|
||||
overrideCfg(cfg SchedulerCfg)
|
||||
}
|
||||
|
||||
func (sch *schedule) definitionRoutine(grafanaCtx context.Context, key alertDefinitionKey,
|
||||
func (sch *schedule) definitionRoutine(grafanaCtx context.Context, key models.AlertDefinitionKey,
|
||||
evalCh <-chan *evalContext, stopCh <-chan struct{}) error {
|
||||
sch.log.Debug("alert definition routine started", "key", key)
|
||||
|
||||
evalRunning := false
|
||||
var start, end time.Time
|
||||
var attempt int64
|
||||
var alertDefinition *AlertDefinition
|
||||
var alertDefinition *models.AlertDefinition
|
||||
for {
|
||||
select {
|
||||
case ctx := <-evalCh:
|
||||
@ -45,8 +53,8 @@ func (sch *schedule) definitionRoutine(grafanaCtx context.Context, key alertDefi
|
||||
|
||||
// fetch latest alert definition version
|
||||
if alertDefinition == nil || alertDefinition.Version < ctx.version {
|
||||
q := getAlertDefinitionByUIDQuery{OrgID: key.orgID, UID: key.definitionUID}
|
||||
err := sch.store.getAlertDefinitionByUID(&q)
|
||||
q := models.GetAlertDefinitionByUIDQuery{OrgID: key.OrgID, UID: key.DefinitionUID}
|
||||
err := sch.store.GetAlertDefinitionByUID(&q)
|
||||
if err != nil {
|
||||
sch.log.Error("failed to fetch alert definition", "key", key)
|
||||
return err
|
||||
@ -70,8 +78,8 @@ func (sch *schedule) definitionRoutine(grafanaCtx context.Context, key alertDefi
|
||||
}
|
||||
for _, r := range results {
|
||||
sch.log.Debug("alert definition result", "title", alertDefinition.Title, "key", key, "attempt", attempt, "now", ctx.now, "duration", end.Sub(start), "instance", r.Instance, "state", r.State.String())
|
||||
cmd := saveAlertInstanceCommand{DefinitionOrgID: key.orgID, DefinitionUID: key.definitionUID, State: InstanceStateType(r.State.String()), Labels: InstanceLabels(r.Instance), LastEvalTime: ctx.now}
|
||||
err := sch.store.saveAlertInstance(&cmd)
|
||||
cmd := models.SaveAlertInstanceCommand{DefinitionOrgID: key.OrgID, DefinitionUID: key.DefinitionUID, State: models.InstanceStateType(r.State.String()), Labels: models.InstanceLabels(r.Instance), LastEvalTime: ctx.now}
|
||||
err := sch.store.SaveAlertInstance(&cmd)
|
||||
if err != nil {
|
||||
sch.log.Error("failed saving alert instance", "title", alertDefinition.Title, "key", key, "attempt", attempt, "now", ctx.now, "instance", r.Instance, "state", r.State.String(), "error", err)
|
||||
}
|
||||
@ -120,60 +128,62 @@ type schedule struct {
|
||||
// evalApplied is only used for tests: test code can set it to non-nil
|
||||
// function, and then it'll be called from the event loop whenever the
|
||||
// message from evalApplied is handled.
|
||||
evalAppliedFunc func(alertDefinitionKey, time.Time)
|
||||
evalAppliedFunc func(models.AlertDefinitionKey, time.Time)
|
||||
|
||||
// stopApplied is only used for tests: test code can set it to non-nil
|
||||
// function, and then it'll be called from the event loop whenever the
|
||||
// message from stopApplied is handled.
|
||||
stopAppliedFunc func(alertDefinitionKey)
|
||||
stopAppliedFunc func(models.AlertDefinitionKey)
|
||||
|
||||
log log.Logger
|
||||
|
||||
evaluator eval.Evaluator
|
||||
|
||||
store store
|
||||
store store.Store
|
||||
|
||||
dataService *tsdb.Service
|
||||
}
|
||||
|
||||
type schedulerCfg struct {
|
||||
c clock.Clock
|
||||
baseInterval time.Duration
|
||||
logger log.Logger
|
||||
evalAppliedFunc func(alertDefinitionKey, time.Time)
|
||||
stopAppliedFunc func(alertDefinitionKey)
|
||||
evaluator eval.Evaluator
|
||||
store store
|
||||
// SchedulerCfg is the scheduler configuration.
|
||||
type SchedulerCfg struct {
|
||||
C clock.Clock
|
||||
BaseInterval time.Duration
|
||||
Logger log.Logger
|
||||
EvalAppliedFunc func(models.AlertDefinitionKey, time.Time)
|
||||
MaxAttempts int64
|
||||
StopAppliedFunc func(models.AlertDefinitionKey)
|
||||
Evaluator eval.Evaluator
|
||||
Store store.Store
|
||||
}
|
||||
|
||||
// newScheduler returns a new schedule.
|
||||
func newScheduler(cfg schedulerCfg, dataService *tsdb.Service) *schedule {
|
||||
ticker := alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds()))
|
||||
// NewScheduler returns a new schedule.
|
||||
func NewScheduler(cfg SchedulerCfg, dataService *tsdb.Service) *schedule {
|
||||
ticker := alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
|
||||
sch := schedule{
|
||||
registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)},
|
||||
maxAttempts: maxAttempts,
|
||||
clock: cfg.c,
|
||||
baseInterval: cfg.baseInterval,
|
||||
log: cfg.logger,
|
||||
registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[models.AlertDefinitionKey]alertDefinitionInfo)},
|
||||
maxAttempts: cfg.MaxAttempts,
|
||||
clock: cfg.C,
|
||||
baseInterval: cfg.BaseInterval,
|
||||
log: cfg.Logger,
|
||||
heartbeat: ticker,
|
||||
evalAppliedFunc: cfg.evalAppliedFunc,
|
||||
stopAppliedFunc: cfg.stopAppliedFunc,
|
||||
evaluator: cfg.evaluator,
|
||||
store: cfg.store,
|
||||
evalAppliedFunc: cfg.EvalAppliedFunc,
|
||||
stopAppliedFunc: cfg.StopAppliedFunc,
|
||||
evaluator: cfg.Evaluator,
|
||||
store: cfg.Store,
|
||||
dataService: dataService,
|
||||
}
|
||||
return &sch
|
||||
}
|
||||
|
||||
func (sch *schedule) overrideCfg(cfg schedulerCfg) {
|
||||
sch.clock = cfg.c
|
||||
sch.baseInterval = cfg.baseInterval
|
||||
sch.heartbeat = alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds()))
|
||||
sch.evalAppliedFunc = cfg.evalAppliedFunc
|
||||
sch.stopAppliedFunc = cfg.stopAppliedFunc
|
||||
func (sch *schedule) overrideCfg(cfg SchedulerCfg) {
|
||||
sch.clock = cfg.C
|
||||
sch.baseInterval = cfg.BaseInterval
|
||||
sch.heartbeat = alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
|
||||
sch.evalAppliedFunc = cfg.EvalAppliedFunc
|
||||
sch.stopAppliedFunc = cfg.StopAppliedFunc
|
||||
}
|
||||
|
||||
func (sch *schedule) evalApplied(alertDefKey alertDefinitionKey, now time.Time) {
|
||||
func (sch *schedule) evalApplied(alertDefKey models.AlertDefinitionKey, now time.Time) {
|
||||
if sch.evalAppliedFunc == nil {
|
||||
return
|
||||
}
|
||||
@ -181,7 +191,7 @@ func (sch *schedule) evalApplied(alertDefKey alertDefinitionKey, now time.Time)
|
||||
sch.evalAppliedFunc(alertDefKey, now)
|
||||
}
|
||||
|
||||
func (sch *schedule) stopApplied(alertDefKey alertDefinitionKey) {
|
||||
func (sch *schedule) stopApplied(alertDefKey models.AlertDefinitionKey) {
|
||||
if sch.stopAppliedFunc == nil {
|
||||
return
|
||||
}
|
||||
@ -223,7 +233,7 @@ func (sch *schedule) Ticker(grafanaCtx context.Context) error {
|
||||
registeredDefinitions := sch.registry.keyMap()
|
||||
|
||||
type readyToRunItem struct {
|
||||
key alertDefinitionKey
|
||||
key models.AlertDefinitionKey
|
||||
definitionInfo alertDefinitionInfo
|
||||
}
|
||||
readyToRun := make([]readyToRunItem, 0)
|
||||
@ -232,7 +242,7 @@ func (sch *schedule) Ticker(grafanaCtx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
key := item.getKey()
|
||||
key := item.GetKey()
|
||||
itemVersion := item.Version
|
||||
newRoutine := !sch.registry.exists(key)
|
||||
definitionInfo := sch.registry.getOrCreateInfo(key, itemVersion)
|
||||
@ -292,12 +302,12 @@ func (sch *schedule) Ticker(grafanaCtx context.Context) error {
|
||||
|
||||
type alertDefinitionRegistry struct {
|
||||
mu sync.Mutex
|
||||
alertDefinitionInfo map[alertDefinitionKey]alertDefinitionInfo
|
||||
alertDefinitionInfo map[models.AlertDefinitionKey]alertDefinitionInfo
|
||||
}
|
||||
|
||||
// getOrCreateInfo returns the channel for the specific alert definition
|
||||
// if it does not exists creates one and returns it
|
||||
func (r *alertDefinitionRegistry) getOrCreateInfo(key alertDefinitionKey, definitionVersion int64) alertDefinitionInfo {
|
||||
func (r *alertDefinitionRegistry) getOrCreateInfo(key models.AlertDefinitionKey, definitionVersion int64) alertDefinitionInfo {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@ -313,7 +323,7 @@ func (r *alertDefinitionRegistry) getOrCreateInfo(key alertDefinitionKey, defini
|
||||
|
||||
// get returns the channel for the specific alert definition
|
||||
// if the key does not exist returns an error
|
||||
func (r *alertDefinitionRegistry) get(key alertDefinitionKey) (*alertDefinitionInfo, error) {
|
||||
func (r *alertDefinitionRegistry) get(key models.AlertDefinitionKey) (*alertDefinitionInfo, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@ -324,7 +334,7 @@ func (r *alertDefinitionRegistry) get(key alertDefinitionKey) (*alertDefinitionI
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (r *alertDefinitionRegistry) exists(key alertDefinitionKey) bool {
|
||||
func (r *alertDefinitionRegistry) exists(key models.AlertDefinitionKey) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@ -332,15 +342,15 @@ func (r *alertDefinitionRegistry) exists(key alertDefinitionKey) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *alertDefinitionRegistry) del(key alertDefinitionKey) {
|
||||
func (r *alertDefinitionRegistry) del(key models.AlertDefinitionKey) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
delete(r.alertDefinitionInfo, key)
|
||||
}
|
||||
|
||||
func (r *alertDefinitionRegistry) iter() <-chan alertDefinitionKey {
|
||||
c := make(chan alertDefinitionKey)
|
||||
func (r *alertDefinitionRegistry) iter() <-chan models.AlertDefinitionKey {
|
||||
c := make(chan models.AlertDefinitionKey)
|
||||
|
||||
f := func() {
|
||||
r.mu.Lock()
|
||||
@ -356,8 +366,8 @@ func (r *alertDefinitionRegistry) iter() <-chan alertDefinitionKey {
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *alertDefinitionRegistry) keyMap() map[alertDefinitionKey]struct{} {
|
||||
definitionsIDs := make(map[alertDefinitionKey]struct{})
|
||||
func (r *alertDefinitionRegistry) keyMap() map[models.AlertDefinitionKey]struct{} {
|
||||
definitionsIDs := make(map[models.AlertDefinitionKey]struct{})
|
||||
for k := range r.iter() {
|
||||
definitionsIDs[k] = struct{}{}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ngalert
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -7,36 +7,51 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type store interface {
|
||||
deleteAlertDefinitionByUID(*deleteAlertDefinitionByUIDCommand) error
|
||||
getAlertDefinitionByUID(*getAlertDefinitionByUIDQuery) error
|
||||
getAlertDefinitions(query *listAlertDefinitionsQuery) error
|
||||
getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error
|
||||
saveAlertDefinition(*saveAlertDefinitionCommand) error
|
||||
updateAlertDefinition(*updateAlertDefinitionCommand) error
|
||||
getAlertInstance(*getAlertInstanceQuery) error
|
||||
listAlertInstances(cmd *listAlertInstancesQuery) error
|
||||
saveAlertInstance(cmd *saveAlertInstanceCommand) error
|
||||
validateAlertDefinition(*AlertDefinition, bool) error
|
||||
updateAlertDefinitionPaused(*updateAlertDefinitionPausedCommand) error
|
||||
// TimeNow makes it possible to test usage of time
|
||||
var TimeNow = time.Now
|
||||
|
||||
// AlertDefinitionMaxTitleLength is the maximum length of the alert definition titles
|
||||
const AlertDefinitionMaxTitleLength = 190
|
||||
|
||||
// ErrEmptyTitleError is an error returned if the alert definition title is empty
|
||||
var ErrEmptyTitleError = errors.New("title is empty")
|
||||
|
||||
// Store is the interface for persisting alert definitions and instances
|
||||
type Store interface {
|
||||
DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error
|
||||
GetAlertDefinitionByUID(*models.GetAlertDefinitionByUIDQuery) error
|
||||
GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error
|
||||
GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error
|
||||
SaveAlertDefinition(*models.SaveAlertDefinitionCommand) error
|
||||
UpdateAlertDefinition(*models.UpdateAlertDefinitionCommand) error
|
||||
GetAlertInstance(*models.GetAlertInstanceQuery) error
|
||||
ListAlertInstances(cmd *models.ListAlertInstancesQuery) error
|
||||
SaveAlertInstance(cmd *models.SaveAlertInstanceCommand) error
|
||||
ValidateAlertDefinition(*models.AlertDefinition, bool) error
|
||||
UpdateAlertDefinitionPaused(*models.UpdateAlertDefinitionPausedCommand) error
|
||||
}
|
||||
|
||||
type storeImpl struct {
|
||||
// DBstore stores the alert definitions and instances in the database.
|
||||
type DBstore struct {
|
||||
// the base scheduler tick rate; it's used for validating definition interval
|
||||
baseInterval time.Duration
|
||||
SQLStore *sqlstore.SQLStore `inject:""`
|
||||
BaseInterval time.Duration
|
||||
// default alert definiiton interval
|
||||
DefaultIntervalSeconds int64
|
||||
SQLStore *sqlstore.SQLStore `inject:""`
|
||||
}
|
||||
|
||||
func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*AlertDefinition, error) {
|
||||
func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*models.AlertDefinition, error) {
|
||||
// we consider optionally enabling some caching
|
||||
alertDefinition := AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
|
||||
alertDefinition := models.AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
|
||||
has, err := sess.Get(&alertDefinition)
|
||||
if !has {
|
||||
return nil, errAlertDefinitionNotFound
|
||||
return nil, models.ErrAlertDefinitionNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -44,9 +59,9 @@ func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string
|
||||
return &alertDefinition, nil
|
||||
}
|
||||
|
||||
// deleteAlertDefinitionByID is a handler for deleting an alert definition.
|
||||
// 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 storeImpl) deleteAlertDefinitionByUID(cmd *deleteAlertDefinitionByUIDCommand) error {
|
||||
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 {
|
||||
@ -66,9 +81,9 @@ func (st storeImpl) deleteAlertDefinitionByUID(cmd *deleteAlertDefinitionByUIDCo
|
||||
})
|
||||
}
|
||||
|
||||
// getAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID.
|
||||
// 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 storeImpl) getAlertDefinitionByUID(query *getAlertDefinitionByUIDQuery) error {
|
||||
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 {
|
||||
@ -79,10 +94,10 @@ func (st storeImpl) getAlertDefinitionByUID(query *getAlertDefinitionByUIDQuery)
|
||||
})
|
||||
}
|
||||
|
||||
// saveAlertDefinition is a handler for saving a new alert definition.
|
||||
func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
|
||||
// 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 := defaultIntervalSeconds
|
||||
intervalSeconds := st.DefaultIntervalSeconds
|
||||
if cmd.IntervalSeconds != nil {
|
||||
intervalSeconds = *cmd.IntervalSeconds
|
||||
}
|
||||
@ -94,7 +109,7 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
|
||||
return fmt.Errorf("failed to generate UID for alert definition %q: %w", cmd.Title, err)
|
||||
}
|
||||
|
||||
alertDefinition := &AlertDefinition{
|
||||
alertDefinition := &models.AlertDefinition{
|
||||
OrgID: cmd.OrgID,
|
||||
Title: cmd.Title,
|
||||
Condition: cmd.Condition,
|
||||
@ -104,11 +119,11 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
|
||||
UID: uid,
|
||||
}
|
||||
|
||||
if err := st.validateAlertDefinition(alertDefinition, false); err != nil {
|
||||
if err := st.ValidateAlertDefinition(alertDefinition, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := alertDefinition.preSave(); err != nil {
|
||||
if err := alertDefinition.PreSave(TimeNow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -119,7 +134,7 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
alertDefVersion := AlertDefinitionVersion{
|
||||
alertDefVersion := models.AlertDefinitionVersion{
|
||||
AlertDefinitionID: alertDefinition.ID,
|
||||
AlertDefinitionUID: alertDefinition.UID,
|
||||
Version: alertDefinition.Version,
|
||||
@ -138,13 +153,13 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
// updateAlertDefinition is a handler for updating an existing alert definition.
|
||||
// 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 storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) error {
|
||||
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, errAlertDefinitionNotFound) {
|
||||
if errors.Is(err, models.ErrAlertDefinitionNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@ -168,7 +183,7 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
|
||||
}
|
||||
|
||||
// explicitly set all fields regardless of being provided or not
|
||||
alertDefinition := &AlertDefinition{
|
||||
alertDefinition := &models.AlertDefinition{
|
||||
ID: existingAlertDefinition.ID,
|
||||
Title: title,
|
||||
Condition: condition,
|
||||
@ -178,11 +193,11 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
|
||||
UID: existingAlertDefinition.UID,
|
||||
}
|
||||
|
||||
if err := st.validateAlertDefinition(alertDefinition, true); err != nil {
|
||||
if err := st.ValidateAlertDefinition(alertDefinition, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := alertDefinition.preSave(); err != nil {
|
||||
if err := alertDefinition.PreSave(TimeNow); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -196,7 +211,7 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
|
||||
return err
|
||||
}
|
||||
|
||||
alertDefVersion := AlertDefinitionVersion{
|
||||
alertDefVersion := models.AlertDefinitionVersion{
|
||||
AlertDefinitionID: alertDefinition.ID,
|
||||
AlertDefinitionUID: alertDefinition.UID,
|
||||
ParentVersion: alertDefinition.Version,
|
||||
@ -216,10 +231,10 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
|
||||
})
|
||||
}
|
||||
|
||||
// getOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
|
||||
func (st storeImpl) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error {
|
||||
// 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([]*AlertDefinition, 0)
|
||||
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
|
||||
@ -230,9 +245,11 @@ func (st storeImpl) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) err
|
||||
})
|
||||
}
|
||||
|
||||
func (st storeImpl) getAlertDefinitions(query *listAlertDefinitionsQuery) error {
|
||||
// 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([]*AlertDefinition, 0)
|
||||
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
|
||||
@ -243,7 +260,8 @@ func (st storeImpl) getAlertDefinitions(query *listAlertDefinitionsQuery) error
|
||||
})
|
||||
}
|
||||
|
||||
func (st storeImpl) updateAlertDefinitionPaused(cmd *updateAlertDefinitionPausedCommand) error {
|
||||
// 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
|
||||
@ -282,7 +300,7 @@ func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (strin
|
||||
for i := 0; i < 3; i++ {
|
||||
uid := util.GenerateShortUID()
|
||||
|
||||
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&AlertDefinition{})
|
||||
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&models.AlertDefinition{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -292,5 +310,32 @@ func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (strin
|
||||
}
|
||||
}
|
||||
|
||||
return "", errAlertDefinitionFailedGenerateUniqueUID
|
||||
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 ErrEmptyTitleError
|
||||
}
|
||||
|
||||
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,20 +1,20 @@
|
||||
package ngalert
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
// getAlertInstance is a handler for retrieving an alert instance based on OrgId, AlertDefintionID, and
|
||||
// GetAlertInstance is a handler for retrieving an alert instance based on OrgId, AlertDefintionID, and
|
||||
// the hash of the labels.
|
||||
// nolint:unused
|
||||
func (st storeImpl) getAlertInstance(cmd *getAlertInstanceQuery) error {
|
||||
func (st DBstore) GetAlertInstance(cmd *models.GetAlertInstanceQuery) error {
|
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
instance := AlertInstance{}
|
||||
instance := models.AlertInstance{}
|
||||
s := strings.Builder{}
|
||||
s.WriteString(`SELECT * FROM alert_instance
|
||||
WHERE
|
||||
@ -43,11 +43,11 @@ func (st storeImpl) getAlertInstance(cmd *getAlertInstanceQuery) error {
|
||||
})
|
||||
}
|
||||
|
||||
// listAlertInstances is a handler for retrieving alert instances within specific organisation
|
||||
// ListAlertInstances is a handler for retrieving alert instances within specific organisation
|
||||
// based on various filters.
|
||||
func (st storeImpl) listAlertInstances(cmd *listAlertInstancesQuery) error {
|
||||
func (st DBstore) ListAlertInstances(cmd *models.ListAlertInstancesQuery) error {
|
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
alertInstances := make([]*listAlertInstancesQueryResult, 0)
|
||||
alertInstances := make([]*models.ListAlertInstancesQueryResult, 0)
|
||||
|
||||
s := strings.Builder{}
|
||||
params := make([]interface{}, 0)
|
||||
@ -76,26 +76,26 @@ func (st storeImpl) listAlertInstances(cmd *listAlertInstancesQuery) error {
|
||||
})
|
||||
}
|
||||
|
||||
// saveAlertDefinition is a handler for saving a new alert definition.
|
||||
// SaveAlertInstance is a handler for saving a new alert instance.
|
||||
// nolint:unused
|
||||
func (st storeImpl) saveAlertInstance(cmd *saveAlertInstanceCommand) error {
|
||||
func (st DBstore) SaveAlertInstance(cmd *models.SaveAlertInstanceCommand) error {
|
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
labelTupleJSON, labelsHash, err := cmd.Labels.StringAndHash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alertInstance := &AlertInstance{
|
||||
alertInstance := &models.AlertInstance{
|
||||
DefinitionOrgID: cmd.DefinitionOrgID,
|
||||
DefinitionUID: cmd.DefinitionUID,
|
||||
Labels: cmd.Labels,
|
||||
LabelsHash: labelsHash,
|
||||
CurrentState: cmd.State,
|
||||
CurrentStateSince: time.Now(),
|
||||
CurrentStateSince: TimeNow(),
|
||||
LastEvalTime: cmd.LastEvalTime,
|
||||
}
|
||||
|
||||
if err := validateAlertInstance(alertInstance); err != nil {
|
||||
if err := models.ValidateAlertInstance(alertInstance); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// +build integration
|
||||
|
||||
package ngalert
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -8,15 +8,19 @@ import (
|
||||
"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/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const baseIntervalSeconds = 10
|
||||
|
||||
func mockTimeNow() {
|
||||
var timeSeed int64
|
||||
timeNow = func() time.Time {
|
||||
store.TimeNow = func() time.Time {
|
||||
fakeNow := time.Unix(timeSeed, 0).UTC()
|
||||
timeSeed++
|
||||
return fakeNow
|
||||
@ -24,13 +28,16 @@ func mockTimeNow() {
|
||||
}
|
||||
|
||||
func resetTimeNow() {
|
||||
timeNow = time.Now
|
||||
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
|
||||
@ -45,7 +52,7 @@ func TestCreatingAlertDefinition(t *testing.T) {
|
||||
desc: "should create successfully an alert definition with default interval",
|
||||
inputIntervalSeconds: nil,
|
||||
inputTitle: "a name",
|
||||
expectedInterval: defaultIntervalSeconds,
|
||||
expectedInterval: dbstore.DefaultIntervalSeconds,
|
||||
expectedUpdated: time.Unix(0, 0).UTC(),
|
||||
},
|
||||
{
|
||||
@ -58,28 +65,25 @@ func TestCreatingAlertDefinition(t *testing.T) {
|
||||
{
|
||||
desc: "should fail to create an alert definition with too big name",
|
||||
inputIntervalSeconds: &customIntervalSeconds,
|
||||
inputTitle: getLongString(alertDefinitionMaxTitleLength + 1),
|
||||
inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
|
||||
expectedError: errors.New(""),
|
||||
},
|
||||
{
|
||||
desc: "should fail to create an alert definition with empty title",
|
||||
inputIntervalSeconds: &customIntervalSeconds,
|
||||
inputTitle: "",
|
||||
expectedError: errEmptyTitleError,
|
||||
expectedError: store.ErrEmptyTitleError,
|
||||
},
|
||||
}
|
||||
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
|
||||
q := saveAlertDefinitionCommand{
|
||||
q := models.SaveAlertDefinitionCommand{
|
||||
OrgID: 1,
|
||||
Title: tc.inputTitle,
|
||||
Condition: "B",
|
||||
Data: []eval.AlertQuery{
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
@ -87,9 +91,9 @@ func TestCreatingAlertDefinition(t *testing.T) {
|
||||
"expression":"2 + 3 > 1"
|
||||
}`),
|
||||
RefID: "B",
|
||||
RelativeTimeRange: eval.RelativeTimeRange{
|
||||
From: eval.Duration(time.Duration(5) * time.Hour),
|
||||
To: eval.Duration(time.Duration(3) * time.Hour),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(time.Duration(5) * time.Hour),
|
||||
To: models.Duration(time.Duration(3) * time.Hour),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -97,7 +101,7 @@ func TestCreatingAlertDefinition(t *testing.T) {
|
||||
if tc.inputIntervalSeconds != nil {
|
||||
q.IntervalSeconds = tc.inputIntervalSeconds
|
||||
}
|
||||
err := store.saveAlertDefinition(&q)
|
||||
err := dbstore.SaveAlertDefinition(&q)
|
||||
switch {
|
||||
case tc.expectedError != nil:
|
||||
require.Error(t, err)
|
||||
@ -113,14 +117,14 @@ func TestCreatingAlertDefinition(t *testing.T) {
|
||||
}
|
||||
func TestCreatingConflictionAlertDefinition(t *testing.T) {
|
||||
t.Run("Should fail to create alert definition with conflicting org_id, title", func(t *testing.T) {
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
q := saveAlertDefinitionCommand{
|
||||
q := models.SaveAlertDefinitionCommand{
|
||||
OrgID: 1,
|
||||
Title: "title",
|
||||
Condition: "B",
|
||||
Data: []eval.AlertQuery{
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
@ -128,20 +132,20 @@ func TestCreatingConflictionAlertDefinition(t *testing.T) {
|
||||
"expression":"2 + 3 > 1"
|
||||
}`),
|
||||
RefID: "B",
|
||||
RelativeTimeRange: eval.RelativeTimeRange{
|
||||
From: eval.Duration(time.Duration(5) * time.Hour),
|
||||
To: eval.Duration(time.Duration(3) * time.Hour),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(time.Duration(5) * time.Hour),
|
||||
To: models.Duration(time.Duration(3) * time.Hour),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := store.saveAlertDefinition(&q)
|
||||
err := dbstore.SaveAlertDefinition(&q)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.saveAlertDefinition(&q)
|
||||
err = dbstore.SaveAlertDefinition(&q)
|
||||
require.Error(t, err)
|
||||
assert.True(t, store.SQLStore.Dialect.IsUniqueConstraintViolation(err))
|
||||
assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err))
|
||||
})
|
||||
}
|
||||
|
||||
@ -150,15 +154,15 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
mockTimeNow()
|
||||
defer resetTimeNow()
|
||||
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
q := updateAlertDefinitionCommand{
|
||||
q := models.UpdateAlertDefinitionCommand{
|
||||
UID: "unknown",
|
||||
OrgID: 1,
|
||||
Title: "something completely different",
|
||||
Condition: "A",
|
||||
Data: []eval.AlertQuery{
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
@ -166,15 +170,15 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
"expression":"2 + 2 > 1"
|
||||
}`),
|
||||
RefID: "A",
|
||||
RelativeTimeRange: eval.RelativeTimeRange{
|
||||
From: eval.Duration(time.Duration(5) * time.Hour),
|
||||
To: eval.Duration(time.Duration(3) * time.Hour),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(time.Duration(5) * time.Hour),
|
||||
To: models.Duration(time.Duration(3) * time.Hour),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := store.updateAlertDefinition(&q)
|
||||
err := dbstore.UpdateAlertDefinition(&q)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@ -182,11 +186,11 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
mockTimeNow()
|
||||
defer resetTimeNow()
|
||||
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
var initialInterval int64 = 120
|
||||
alertDefinition := createTestAlertDefinition(t, store, initialInterval)
|
||||
alertDefinition := createTestAlertDefinition(t, dbstore, initialInterval)
|
||||
created := alertDefinition.Updated
|
||||
|
||||
var customInterval int64 = 30
|
||||
@ -231,7 +235,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
desc: "should not update alert definition if the title it's too big",
|
||||
inputInterval: &customInterval,
|
||||
inputOrgID: 0,
|
||||
inputTitle: getLongString(alertDefinitionMaxTitleLength + 1),
|
||||
inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
|
||||
expectedError: errors.New(""),
|
||||
},
|
||||
{
|
||||
@ -245,10 +249,10 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
q := updateAlertDefinitionCommand{
|
||||
q := models.UpdateAlertDefinitionCommand{
|
||||
UID: (*alertDefinition).UID,
|
||||
Condition: "B",
|
||||
Data: []eval.AlertQuery{
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
@ -256,9 +260,9 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
"expression":"2 + 3 > 1"
|
||||
}`),
|
||||
RefID: "B",
|
||||
RelativeTimeRange: eval.RelativeTimeRange{
|
||||
From: eval.Duration(5 * time.Hour),
|
||||
To: eval.Duration(3 * time.Hour),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(5 * time.Hour),
|
||||
To: models.Duration(3 * time.Hour),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -275,7 +279,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
|
||||
q.OrgID = tc.inputOrgID
|
||||
}
|
||||
q.Title = tc.inputTitle
|
||||
err := store.updateAlertDefinition(&q)
|
||||
err := dbstore.UpdateAlertDefinition(&q)
|
||||
switch {
|
||||
case tc.expectedError != nil:
|
||||
require.Error(t, err)
|
||||
@ -321,18 +325,18 @@ func TestUpdatingConflictingAlertDefinition(t *testing.T) {
|
||||
mockTimeNow()
|
||||
defer resetTimeNow()
|
||||
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
var initialInterval int64 = 120
|
||||
alertDef1 := createTestAlertDefinition(t, store, initialInterval)
|
||||
alertDef2 := createTestAlertDefinition(t, store, initialInterval)
|
||||
alertDef1 := createTestAlertDefinition(t, dbstore, initialInterval)
|
||||
alertDef2 := createTestAlertDefinition(t, dbstore, initialInterval)
|
||||
|
||||
q := updateAlertDefinitionCommand{
|
||||
q := models.UpdateAlertDefinitionCommand{
|
||||
UID: (*alertDef2).UID,
|
||||
Title: alertDef1.Title,
|
||||
Condition: "B",
|
||||
Data: []eval.AlertQuery{
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
@ -340,70 +344,70 @@ func TestUpdatingConflictingAlertDefinition(t *testing.T) {
|
||||
"expression":"2 + 3 > 1"
|
||||
}`),
|
||||
RefID: "B",
|
||||
RelativeTimeRange: eval.RelativeTimeRange{
|
||||
From: eval.Duration(5 * time.Hour),
|
||||
To: eval.Duration(3 * time.Hour),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(5 * time.Hour),
|
||||
To: models.Duration(3 * time.Hour),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := store.updateAlertDefinition(&q)
|
||||
err := dbstore.UpdateAlertDefinition(&q)
|
||||
require.Error(t, err)
|
||||
assert.True(t, store.SQLStore.Dialect.IsUniqueConstraintViolation(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) {
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
q := deleteAlertDefinitionByUIDCommand{
|
||||
q := models.DeleteAlertDefinitionByUIDCommand{
|
||||
UID: "unknown",
|
||||
OrgID: 1,
|
||||
}
|
||||
|
||||
err := store.deleteAlertDefinitionByUID(&q)
|
||||
err := dbstore.DeleteAlertDefinitionByUID(&q)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("deleting successfully existing alert", func(t *testing.T) {
|
||||
_, store := setupTestEnv(t, baseIntervalSeconds)
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
alertDefinition := createTestAlertDefinition(t, store, 60)
|
||||
alertDefinition := createTestAlertDefinition(t, dbstore, 60)
|
||||
|
||||
q := deleteAlertDefinitionByUIDCommand{
|
||||
q := models.DeleteAlertDefinitionByUIDCommand{
|
||||
UID: (*alertDefinition).UID,
|
||||
OrgID: 1,
|
||||
}
|
||||
|
||||
// save an instance for the definition
|
||||
saveCmd := &saveAlertInstanceCommand{
|
||||
saveCmd := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition.OrgID,
|
||||
DefinitionUID: alertDefinition.UID,
|
||||
State: InstanceStateFiring,
|
||||
Labels: InstanceLabels{"test": "testValue"},
|
||||
State: models.InstanceStateFiring,
|
||||
Labels: models.InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
err := store.saveAlertInstance(saveCmd)
|
||||
err := dbstore.SaveAlertInstance(saveCmd)
|
||||
require.NoError(t, err)
|
||||
listCommand := &listAlertInstancesQuery{
|
||||
listQuery := &models.ListAlertInstancesQuery{
|
||||
DefinitionOrgID: alertDefinition.OrgID,
|
||||
DefinitionUID: alertDefinition.UID,
|
||||
}
|
||||
err = store.listAlertInstances(listCommand)
|
||||
err = dbstore.ListAlertInstances(listQuery)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listCommand.Result, 1)
|
||||
require.Len(t, listQuery.Result, 1)
|
||||
|
||||
err = store.deleteAlertDefinitionByUID(&q)
|
||||
err = dbstore.DeleteAlertDefinitionByUID(&q)
|
||||
require.NoError(t, err)
|
||||
|
||||
// assert that alert instance is deleted
|
||||
err = store.listAlertInstances(listCommand)
|
||||
err = dbstore.ListAlertInstances(listQuery)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listCommand.Result, 0)
|
||||
require.Len(t, listQuery.Result, 0)
|
||||
})
|
||||
}
|
||||
|
165
pkg/services/ngalert/tests/instance_database_test.go
Normal file
165
pkg/services/ngalert/tests/instance_database_test.go
Normal file
@ -0,0 +1,165 @@
|
||||
// +build integration
|
||||
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAlertInstanceOperations(t *testing.T) {
|
||||
dbstore := setupTestEnv(t, baseIntervalSeconds)
|
||||
|
||||
alertDefinition1 := createTestAlertDefinition(t, dbstore, 60)
|
||||
orgID := alertDefinition1.OrgID
|
||||
|
||||
alertDefinition2 := createTestAlertDefinition(t, dbstore, 60)
|
||||
require.Equal(t, orgID, alertDefinition2.OrgID)
|
||||
|
||||
alertDefinition3 := createTestAlertDefinition(t, dbstore, 60)
|
||||
require.Equal(t, orgID, alertDefinition3.OrgID)
|
||||
|
||||
alertDefinition4 := createTestAlertDefinition(t, dbstore, 60)
|
||||
require.Equal(t, orgID, alertDefinition4.OrgID)
|
||||
|
||||
t.Run("can save and read new alert instance", func(t *testing.T) {
|
||||
saveCmd := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition1.OrgID,
|
||||
DefinitionUID: alertDefinition1.UID,
|
||||
State: models.InstanceStateFiring,
|
||||
Labels: models.InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
err := dbstore.SaveAlertInstance(saveCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
getCmd := &models.GetAlertInstanceQuery{
|
||||
DefinitionOrgID: saveCmd.DefinitionOrgID,
|
||||
DefinitionUID: saveCmd.DefinitionUID,
|
||||
Labels: models.InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
|
||||
err = dbstore.GetAlertInstance(getCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
|
||||
require.Equal(t, alertDefinition1.OrgID, getCmd.Result.DefinitionOrgID)
|
||||
require.Equal(t, alertDefinition1.UID, getCmd.Result.DefinitionUID)
|
||||
})
|
||||
|
||||
t.Run("can save and read new alert instance with no labels", func(t *testing.T) {
|
||||
saveCmd := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition2.OrgID,
|
||||
DefinitionUID: alertDefinition2.UID,
|
||||
State: models.InstanceStateNormal,
|
||||
}
|
||||
err := dbstore.SaveAlertInstance(saveCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
getCmd := &models.GetAlertInstanceQuery{
|
||||
DefinitionOrgID: saveCmd.DefinitionOrgID,
|
||||
DefinitionUID: saveCmd.DefinitionUID,
|
||||
}
|
||||
|
||||
err = dbstore.GetAlertInstance(getCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, alertDefinition2.OrgID, getCmd.Result.DefinitionOrgID)
|
||||
require.Equal(t, alertDefinition2.UID, getCmd.Result.DefinitionUID)
|
||||
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) {
|
||||
saveCmdOne := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition3.OrgID,
|
||||
DefinitionUID: alertDefinition3.UID,
|
||||
State: models.InstanceStateFiring,
|
||||
Labels: models.InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
|
||||
err := dbstore.SaveAlertInstance(saveCmdOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
saveCmdTwo := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
|
||||
DefinitionUID: saveCmdOne.DefinitionUID,
|
||||
State: models.InstanceStateFiring,
|
||||
Labels: models.InstanceLabels{"test": "meow"},
|
||||
}
|
||||
err = dbstore.SaveAlertInstance(saveCmdTwo)
|
||||
require.NoError(t, err)
|
||||
|
||||
listQuery := &models.ListAlertInstancesQuery{
|
||||
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
|
||||
DefinitionUID: saveCmdOne.DefinitionUID,
|
||||
}
|
||||
|
||||
err = dbstore.ListAlertInstances(listQuery)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listQuery.Result, 2)
|
||||
})
|
||||
|
||||
t.Run("can list all added instances in org", func(t *testing.T) {
|
||||
listQuery := &models.ListAlertInstancesQuery{
|
||||
DefinitionOrgID: orgID,
|
||||
}
|
||||
|
||||
err := dbstore.ListAlertInstances(listQuery)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listQuery.Result, 4)
|
||||
})
|
||||
|
||||
t.Run("can list all added instances in org filtered by current state", func(t *testing.T) {
|
||||
listQuery := &models.ListAlertInstancesQuery{
|
||||
DefinitionOrgID: orgID,
|
||||
State: models.InstanceStateNormal,
|
||||
}
|
||||
|
||||
err := dbstore.ListAlertInstances(listQuery)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listQuery.Result, 1)
|
||||
})
|
||||
|
||||
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
|
||||
saveCmdOne := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: alertDefinition4.OrgID,
|
||||
DefinitionUID: alertDefinition4.UID,
|
||||
State: models.InstanceStateFiring,
|
||||
Labels: models.InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
|
||||
err := dbstore.SaveAlertInstance(saveCmdOne)
|
||||
require.NoError(t, err)
|
||||
|
||||
saveCmdTwo := &models.SaveAlertInstanceCommand{
|
||||
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
|
||||
DefinitionUID: saveCmdOne.DefinitionUID,
|
||||
State: models.InstanceStateNormal,
|
||||
Labels: models.InstanceLabels{"test": "testValue"},
|
||||
}
|
||||
err = dbstore.SaveAlertInstance(saveCmdTwo)
|
||||
require.NoError(t, err)
|
||||
|
||||
listQuery := &models.ListAlertInstancesQuery{
|
||||
DefinitionOrgID: alertDefinition4.OrgID,
|
||||
DefinitionUID: alertDefinition4.UID,
|
||||
}
|
||||
|
||||
err = dbstore.ListAlertInstances(listQuery)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, listQuery.Result, 1)
|
||||
|
||||
require.Equal(t, saveCmdTwo.DefinitionOrgID, listQuery.Result[0].DefinitionOrgID)
|
||||
require.Equal(t, saveCmdTwo.DefinitionUID, listQuery.Result[0].DefinitionUID)
|
||||
require.Equal(t, saveCmdTwo.Labels, listQuery.Result[0].Labels)
|
||||
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)
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ngalert
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -8,7 +8,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@ -16,47 +21,49 @@ import (
|
||||
)
|
||||
|
||||
type evalAppliedInfo struct {
|
||||
alertDefKey alertDefinitionKey
|
||||
alertDefKey models.AlertDefinitionKey
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func TestAlertingTicker(t *testing.T) {
|
||||
ng, store := setupTestEnv(t, 1)
|
||||
dbstore := setupTestEnv(t, 1)
|
||||
t.Cleanup(registry.ClearOverrides)
|
||||
|
||||
alerts := make([]*AlertDefinition, 0)
|
||||
alerts := make([]*models.AlertDefinition, 0)
|
||||
// create alert definition with zero interval (should never run)
|
||||
alerts = append(alerts, createTestAlertDefinition(t, store, 0))
|
||||
alerts = append(alerts, createTestAlertDefinition(t, dbstore, 0))
|
||||
|
||||
// create alert definition with one second interval
|
||||
alerts = append(alerts, createTestAlertDefinition(t, store, 1))
|
||||
alerts = append(alerts, createTestAlertDefinition(t, dbstore, 1))
|
||||
|
||||
evalAppliedCh := make(chan evalAppliedInfo, len(alerts))
|
||||
stopAppliedCh := make(chan alertDefinitionKey, len(alerts))
|
||||
stopAppliedCh := make(chan models.AlertDefinitionKey, len(alerts))
|
||||
|
||||
mockedClock := clock.NewMock()
|
||||
baseInterval := time.Second
|
||||
|
||||
schefCfg := schedulerCfg{
|
||||
c: mockedClock,
|
||||
baseInterval: baseInterval,
|
||||
evalAppliedFunc: func(alertDefKey alertDefinitionKey, now time.Time) {
|
||||
schefCfg := schedule.SchedulerCfg{
|
||||
C: mockedClock,
|
||||
BaseInterval: baseInterval,
|
||||
EvalAppliedFunc: func(alertDefKey models.AlertDefinitionKey, now time.Time) {
|
||||
evalAppliedCh <- evalAppliedInfo{alertDefKey: alertDefKey, now: now}
|
||||
},
|
||||
stopAppliedFunc: func(alertDefKey alertDefinitionKey) {
|
||||
StopAppliedFunc: func(alertDefKey models.AlertDefinitionKey) {
|
||||
stopAppliedCh <- alertDefKey
|
||||
},
|
||||
Store: dbstore,
|
||||
Logger: log.New("ngalert schedule test"),
|
||||
}
|
||||
ng.schedule.overrideCfg(schefCfg)
|
||||
sched := schedule.NewScheduler(schefCfg, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
go func() {
|
||||
err := ng.schedule.Ticker(ctx)
|
||||
err := sched.Ticker(ctx)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
runtime.Gosched()
|
||||
|
||||
expectedAlertDefinitionsEvaluated := []alertDefinitionKey{alerts[1].getKey()}
|
||||
expectedAlertDefinitionsEvaluated := []models.AlertDefinitionKey{alerts[1].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 1st tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
@ -64,93 +71,93 @@ func TestAlertingTicker(t *testing.T) {
|
||||
|
||||
// change alert definition interval to three seconds
|
||||
var threeSecInterval int64 = 3
|
||||
err := store.updateAlertDefinition(&updateAlertDefinitionCommand{
|
||||
err := dbstore.UpdateAlertDefinition(&models.UpdateAlertDefinitionCommand{
|
||||
UID: alerts[0].UID,
|
||||
IntervalSeconds: &threeSecInterval,
|
||||
OrgID: alerts[0].OrgID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("alert definition: %v interval reset to: %d", alerts[0].getKey(), threeSecInterval)
|
||||
t.Logf("alert definition: %v interval reset to: %d", alerts[0].GetKey(), threeSecInterval)
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[1].getKey()}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{alerts[1].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 2nd tick alert definition: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[1].getKey(), alerts[0].getKey()}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{alerts[1].GetKey(), alerts[0].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 3rd tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[1].getKey()}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{alerts[1].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 4th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
|
||||
err = store.deleteAlertDefinitionByUID(&deleteAlertDefinitionByUIDCommand{UID: alerts[1].UID, OrgID: alerts[1].OrgID})
|
||||
err = dbstore.DeleteAlertDefinitionByUID(&models.DeleteAlertDefinitionByUIDCommand{UID: alerts[1].UID, OrgID: alerts[1].OrgID})
|
||||
require.NoError(t, err)
|
||||
t.Logf("alert definition: %v deleted", alerts[1].getKey())
|
||||
t.Logf("alert definition: %v deleted", alerts[1].GetKey())
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{}
|
||||
t.Run(fmt.Sprintf("on 5th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
expectedAlertDefinitionsStopped := []alertDefinitionKey{alerts[1].getKey()}
|
||||
expectedAlertDefinitionsStopped := []models.AlertDefinitionKey{alerts[1].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 5th tick alert definitions: %s should be stopped", concatenate(expectedAlertDefinitionsStopped)), func(t *testing.T) {
|
||||
assertStopRun(t, stopAppliedCh, expectedAlertDefinitionsStopped...)
|
||||
})
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[0].getKey()}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{alerts[0].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 6th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
|
||||
// create alert definition with one second interval
|
||||
alerts = append(alerts, createTestAlertDefinition(t, store, 1))
|
||||
alerts = append(alerts, createTestAlertDefinition(t, dbstore, 1))
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[2].getKey()}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{alerts[2].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 7th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
|
||||
// pause alert definition
|
||||
err = store.updateAlertDefinitionPaused(&updateAlertDefinitionPausedCommand{UIDs: []string{alerts[2].UID}, OrgID: alerts[2].OrgID, Paused: true})
|
||||
err = dbstore.UpdateAlertDefinitionPaused(&models.UpdateAlertDefinitionPausedCommand{UIDs: []string{alerts[2].UID}, OrgID: alerts[2].OrgID, Paused: true})
|
||||
require.NoError(t, err)
|
||||
t.Logf("alert definition: %v paused", alerts[2].getKey())
|
||||
t.Logf("alert definition: %v paused", alerts[2].GetKey())
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{}
|
||||
t.Run(fmt.Sprintf("on 8th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
|
||||
expectedAlertDefinitionsStopped = []alertDefinitionKey{alerts[2].getKey()}
|
||||
expectedAlertDefinitionsStopped = []models.AlertDefinitionKey{alerts[2].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 8th tick alert definitions: %s should be stopped", concatenate(expectedAlertDefinitionsStopped)), func(t *testing.T) {
|
||||
assertStopRun(t, stopAppliedCh, expectedAlertDefinitionsStopped...)
|
||||
})
|
||||
|
||||
// unpause alert definition
|
||||
err = store.updateAlertDefinitionPaused(&updateAlertDefinitionPausedCommand{UIDs: []string{alerts[2].UID}, OrgID: alerts[2].OrgID, Paused: false})
|
||||
err = dbstore.UpdateAlertDefinitionPaused(&models.UpdateAlertDefinitionPausedCommand{UIDs: []string{alerts[2].UID}, OrgID: alerts[2].OrgID, Paused: false})
|
||||
require.NoError(t, err)
|
||||
t.Logf("alert definition: %v unpaused", alerts[2].getKey())
|
||||
t.Logf("alert definition: %v unpaused", alerts[2].GetKey())
|
||||
|
||||
expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[0].getKey(), alerts[2].getKey()}
|
||||
expectedAlertDefinitionsEvaluated = []models.AlertDefinitionKey{alerts[0].GetKey(), alerts[2].GetKey()}
|
||||
t.Run(fmt.Sprintf("on 9th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
|
||||
tick := advanceClock(t, mockedClock)
|
||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
|
||||
})
|
||||
}
|
||||
|
||||
func assertEvalRun(t *testing.T, ch <-chan evalAppliedInfo, tick time.Time, keys ...alertDefinitionKey) {
|
||||
func assertEvalRun(t *testing.T, ch <-chan evalAppliedInfo, tick time.Time, keys ...models.AlertDefinitionKey) {
|
||||
timeout := time.After(time.Second)
|
||||
|
||||
expected := make(map[alertDefinitionKey]struct{}, len(keys))
|
||||
expected := make(map[models.AlertDefinitionKey]struct{}, len(keys))
|
||||
for _, k := range keys {
|
||||
expected[k] = struct{}{}
|
||||
}
|
||||
@ -175,10 +182,10 @@ func assertEvalRun(t *testing.T, ch <-chan evalAppliedInfo, tick time.Time, keys
|
||||
}
|
||||
}
|
||||
|
||||
func assertStopRun(t *testing.T, ch <-chan alertDefinitionKey, keys ...alertDefinitionKey) {
|
||||
func assertStopRun(t *testing.T, ch <-chan models.AlertDefinitionKey, keys ...models.AlertDefinitionKey) {
|
||||
timeout := time.After(time.Second)
|
||||
|
||||
expected := make(map[alertDefinitionKey]struct{}, len(keys))
|
||||
expected := make(map[models.AlertDefinitionKey]struct{}, len(keys))
|
||||
for _, k := range keys {
|
||||
expected[k] = struct{}{}
|
||||
}
|
||||
@ -208,7 +215,7 @@ func advanceClock(t *testing.T, mockedClock *clock.Mock) time.Time {
|
||||
// t.Logf("Tick: %v", mockedClock.Now())
|
||||
}
|
||||
|
||||
func concatenate(keys []alertDefinitionKey) string {
|
||||
func concatenate(keys []models.AlertDefinitionKey) string {
|
||||
s := make([]string, len(keys))
|
||||
for _, k := range keys {
|
||||
s = append(s, k.String())
|
@ -1,4 +1,4 @@
|
||||
package ngalert
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -7,18 +7,25 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl) {
|
||||
// setupTestEnv initializes a store to used by the tests.
|
||||
func setupTestEnv(t *testing.T, baseIntervalSeconds int64) *store.DBstore {
|
||||
cfg := setting.NewCfg()
|
||||
// AlertNG is disabled by default and only if it's enabled
|
||||
// its database migrations run and the relative database tables are created
|
||||
cfg.FeatureToggles = map[string]bool{"ngalert": true}
|
||||
|
||||
ng := overrideAlertNGInRegistry(t, cfg)
|
||||
@ -26,18 +33,20 @@ func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl)
|
||||
|
||||
err := ng.Init()
|
||||
require.NoError(t, err)
|
||||
return ng, &storeImpl{SQLStore: ng.SQLStore, baseInterval: time.Duration(baseIntervalSeconds) * time.Second}
|
||||
return &store.DBstore{SQLStore: ng.SQLStore, BaseInterval: time.Duration(baseIntervalSeconds) * time.Second}
|
||||
}
|
||||
|
||||
func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG {
|
||||
ng := AlertNG{
|
||||
func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) ngalert.AlertNG {
|
||||
ng := ngalert.AlertNG{
|
||||
Cfg: cfg,
|
||||
RouteRegister: routing.NewRouteRegister(),
|
||||
log: log.New("ngalert-test"),
|
||||
Log: log.New("ngalert-test"),
|
||||
}
|
||||
|
||||
// hook for initialising the service after the Cfg is populated
|
||||
// so that database migrations will run
|
||||
overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
|
||||
if _, ok := descriptor.Instance.(*AlertNG); ok {
|
||||
if _, ok := descriptor.Instance.(*ngalert.AlertNG); ok {
|
||||
return ®istry.Descriptor{
|
||||
Name: descriptor.Name,
|
||||
Instance: &ng,
|
||||
@ -52,29 +61,30 @@ func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG {
|
||||
return ng
|
||||
}
|
||||
|
||||
func createTestAlertDefinition(t *testing.T, store *storeImpl, intervalSeconds int64) *AlertDefinition {
|
||||
cmd := saveAlertDefinitionCommand{
|
||||
// createTestAlertDefinition creates a dummy alert definition to be used by the tests.
|
||||
func createTestAlertDefinition(t *testing.T, store *store.DBstore, intervalSeconds int64) *models.AlertDefinition {
|
||||
cmd := models.SaveAlertDefinitionCommand{
|
||||
OrgID: 1,
|
||||
Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)),
|
||||
Condition: "A",
|
||||
Data: []eval.AlertQuery{
|
||||
Data: []models.AlertQuery{
|
||||
{
|
||||
Model: json.RawMessage(`{
|
||||
"datasource": "__expr__",
|
||||
"type":"math",
|
||||
"expression":"2 + 2 > 1"
|
||||
}`),
|
||||
RelativeTimeRange: eval.RelativeTimeRange{
|
||||
From: eval.Duration(5 * time.Hour),
|
||||
To: eval.Duration(3 * time.Hour),
|
||||
RelativeTimeRange: models.RelativeTimeRange{
|
||||
From: models.Duration(5 * time.Hour),
|
||||
To: models.Duration(3 * time.Hour),
|
||||
},
|
||||
RefID: "A",
|
||||
},
|
||||
},
|
||||
IntervalSeconds: &intervalSeconds,
|
||||
}
|
||||
err := store.saveAlertDefinition(&cmd)
|
||||
err := store.SaveAlertDefinition(&cmd)
|
||||
require.NoError(t, err)
|
||||
t.Logf("alert definition: %v with interval: %d created", cmd.Result.getKey(), intervalSeconds)
|
||||
t.Logf("alert definition: %v with interval: %d created", cmd.Result.GetKey(), intervalSeconds)
|
||||
return cmd.Result
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package ngalert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
)
|
||||
|
||||
const alertDefinitionMaxTitleLength = 190
|
||||
|
||||
var errEmptyTitleError = errors.New("title is empty")
|
||||
|
||||
// validateAlertDefinition validates the alert definition interval and organisation.
|
||||
// If requireData is true checks that it contains at least one alert query
|
||||
func (st storeImpl) validateAlertDefinition(alertDefinition *AlertDefinition, requireData bool) error {
|
||||
if !requireData && len(alertDefinition.Data) == 0 {
|
||||
return fmt.Errorf("no queries or expressions are found")
|
||||
}
|
||||
|
||||
if alertDefinition.Title == "" {
|
||||
return errEmptyTitleError
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// validateCondition validates that condition queries refer to existing datasources
|
||||
func (api *apiImpl) validateCondition(c eval.Condition, user *models.SignedInUser, skipCache bool) error {
|
||||
var refID string
|
||||
|
||||
if len(c.QueriesAndExpressions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, query := range c.QueriesAndExpressions {
|
||||
if c.RefID == query.RefID {
|
||||
refID = c.RefID
|
||||
}
|
||||
|
||||
datasourceUID, err := query.GetDatasource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isExpression, err := query.IsExpression()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isExpression {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = api.DatasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if refID == "" {
|
||||
return fmt.Errorf("condition %s not found in any query or expression", c.RefID)
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user