AlertingNG: Split into several packages (#31719)

* AlertingNG: Split into several packages

* Move AlertQuery to models
This commit is contained in:
Sofia Papagiannaki 2021-03-08 22:19:21 +02:00 committed by GitHub
parent 124ef813ab
commit 4ce0a49eac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 840 additions and 765 deletions

1
go.mod
View File

@ -68,6 +68,7 @@ require (
github.com/prometheus/client_golang v1.9.0 github.com/prometheus/client_golang v1.9.0
github.com/prometheus/client_model v0.2.0 github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.18.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 v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.0 github.com/russellhaering/goxmldsig v1.1.0

4
go.sum
View File

@ -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/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/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/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/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 h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=

View File

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

View File

@ -1,7 +1,13 @@
package ngalert package api
import ( import (
"fmt" "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/go-macaron/binding"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
@ -17,26 +23,31 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
type apiImpl struct { // timeNow makes it possible to test usage of time
Cfg *setting.Cfg `inject:""` var timeNow = time.Now
DatasourceCache datasources.CacheService `inject:""`
RouteRegister routing.RouteRegister `inject:""` // API handlers.
type API struct {
Cfg *setting.Cfg
DatasourceCache datasources.CacheService
RouteRegister routing.RouteRegister
DataService *tsdb.Service DataService *tsdb.Service
schedule scheduleService Schedule schedule.ScheduleService
store store 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) { api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertDefinitions)) alertDefinitions.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertDefinitions))
alertDefinitions.Get("/eval/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.alertDefinitionEvalEndpoint)) 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.Get("/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.getAlertDefinitionEndpoint))
alertDefinitions.Delete("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, routing.Wrap(api.deleteAlertDefinitionEndpoint)) alertDefinitions.Delete("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, routing.Wrap(api.deleteAlertDefinitionEndpoint))
alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(saveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint)) alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(ngmodels.SaveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint))
alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(updateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint)) alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(ngmodels.UpdateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint))
alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(updateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint)) alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint))
alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(updateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint)) alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint))
}) })
api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) { api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) {
@ -50,7 +61,7 @@ func (api *apiImpl) registerAPIEndpoints() {
} }
// conditionEvalEndpoint handles POST /api/alert-definitions/eval. // 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{ evalCond := eval.Condition{
RefID: cmd.Condition, RefID: cmd.Condition,
OrgID: c.SignedInUser.OrgId, 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. // 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") alertDefinitionUID := c.Params(":alertDefinitionUID")
condition, err := api.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId) 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. // 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") alertDefinitionUID := c.Params(":alertDefinitionUID")
query := getAlertDefinitionByUIDQuery{ query := ngmodels.GetAlertDefinitionByUIDQuery{
UID: alertDefinitionUID, UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId, 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) 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. // 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") alertDefinitionUID := c.Params(":alertDefinitionUID")
cmd := deleteAlertDefinitionByUIDCommand{ cmd := ngmodels.DeleteAlertDefinitionByUIDCommand{
UID: alertDefinitionUID, UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId, 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) 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. // 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.UID = c.Params(":alertDefinitionUID")
cmd.OrgID = c.SignedInUser.OrgId 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) 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) 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. // 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 cmd.OrgID = c.SignedInUser.OrgId
evalCond := eval.Condition{ evalCond := eval.Condition{
@ -183,7 +194,7 @@ func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd save
return response.Error(400, "invalid condition", err) 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) 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. // listAlertDefinitions handles GET /api/alert-definitions.
func (api *apiImpl) listAlertDefinitions(c *models.ReqContext) response.Response { func (api *API) listAlertDefinitions(c *models.ReqContext) response.Response {
query := listAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId} 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.Error(500, "Failed to list alert definitions", err)
} }
return response.JSON(200, util.DynMap{"results": query.Result}) return response.JSON(200, util.DynMap{"results": query.Result})
} }
func (api *apiImpl) pauseScheduler() response.Response { func (api *API) pauseScheduler() response.Response {
err := api.schedule.Pause() err := api.Schedule.Pause()
if err != nil { if err != nil {
return response.Error(500, "Failed to pause scheduler", err) return response.Error(500, "Failed to pause scheduler", err)
} }
return response.JSON(200, util.DynMap{"message": "alert definition scheduler paused"}) return response.JSON(200, util.DynMap{"message": "alert definition scheduler paused"})
} }
func (api *apiImpl) unpauseScheduler() response.Response { func (api *API) unpauseScheduler() response.Response {
err := api.schedule.Unpause() err := api.Schedule.Unpause()
if err != nil { if err != nil {
return response.Error(500, "Failed to unpause scheduler", err) 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. // 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.OrgID = c.SignedInUser.OrgId
cmd.Paused = true cmd.Paused = true
err := api.store.updateAlertDefinitionPaused(&cmd) err := api.Store.UpdateAlertDefinitionPaused(&cmd)
if err != nil { if err != nil {
return response.Error(500, "Failed to pause alert definition", err) 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. // 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.OrgID = c.SignedInUser.OrgId
cmd.Paused = false cmd.Paused = false
err := api.store.updateAlertDefinitionPaused(&cmd) err := api.Store.UpdateAlertDefinitionPaused(&cmd)
if err != nil { if err != nil {
return response.Error(500, "Failed to unpause alert definition", err) 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)}) 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
}

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

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

View File

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
@ -46,7 +48,7 @@ type Condition struct {
RefID string `json:"refId"` RefID string `json:"refId"`
OrgID int64 `json:"-"` OrgID int64 `json:"-"`
QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"` QueriesAndExpressions []models.AlertQuery `json:"queriesAndExpressions"`
} }
// ExecutionResults contains the unevaluated results from executing // 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 { for i := range c.QueriesAndExpressions {
q := c.QueriesAndExpressions[i] q := c.QueriesAndExpressions[i]
model, err := q.getModel() model, err := q.GetModel()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get query model: %w", err) return nil, fmt.Errorf("failed to get query model: %w", err)
} }
interval, err := q.getIntervalDuration() interval, err := q.GetIntervalDuration()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err) return nil, fmt.Errorf("failed to retrieve intervalMs from the model: %w", err)
} }
maxDatapoints, err := q.getMaxDatapoints() maxDatapoints, err := q.GetMaxDatapoints()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve maxDatapoints from the model: %w", err) 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, RefID: q.RefID,
MaxDataPoints: maxDatapoints, MaxDataPoints: maxDatapoints,
QueryType: q.QueryType, QueryType: q.QueryType,
TimeRange: q.RelativeTimeRange.toTimeRange(now), TimeRange: q.RelativeTimeRange.ToTimeRange(now),
}) })
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package eval package models
import ( import (
"encoding/json" "encoding/json"
@ -49,7 +49,7 @@ func (rtr *RelativeTimeRange) isValid() bool {
return rtr.From > rtr.To 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{ return backend.TimeRange{
From: now.Add(-time.Duration(rtr.From)), From: now.Add(-time.Duration(rtr.From)),
To: now.Add(-time.Duration(rtr.To)), To: now.Add(-time.Duration(rtr.To)),
@ -147,7 +147,7 @@ func (aq *AlertQuery) setMaxDatapoints() error {
return nil return nil
} }
func (aq *AlertQuery) getMaxDatapoints() (int64, error) { func (aq *AlertQuery) GetMaxDatapoints() (int64, error) {
err := aq.setMaxDatapoints() err := aq.setMaxDatapoints()
if err != nil { if err != nil {
return 0, err return 0, err
@ -192,7 +192,7 @@ func (aq *AlertQuery) getIntervalMS() (int64, error) {
return int64(intervalMs), nil return int64(intervalMs), nil
} }
func (aq *AlertQuery) getIntervalDuration() (time.Duration, error) { func (aq *AlertQuery) GetIntervalDuration() (time.Duration, error) {
err := aq.setIntervalMS() err := aq.setIntervalMS()
if err != nil { if err != nil {
return 0, err return 0, err
@ -214,7 +214,7 @@ func (aq *AlertQuery) GetDatasource() (string, error) {
return aq.DatasourceUID, nil return aq.DatasourceUID, nil
} }
func (aq *AlertQuery) getModel() ([]byte, error) { func (aq *AlertQuery) GetModel() ([]byte, error) {
err := aq.setDatasource() err := aq.setDatasource()
if err != nil { if err != nil {
return nil, err return nil, err
@ -270,7 +270,7 @@ func (aq *AlertQuery) PreSave() error {
} }
// override model // override model
model, err := aq.getModel() model, err := aq.GetModel()
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,4 +1,4 @@
package eval package models
import ( import (
"encoding/json" "encoding/json"
@ -186,7 +186,7 @@ func TestAlertQuery(t *testing.T) {
}) })
t.Run("can update model maxDataPoints (if missing)", func(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.NoError(t, err)
require.Equal(t, tc.expectedMaxPoints, maxDataPoints) 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) { 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) require.NoError(t, err)
model := make(map[string]interface{}) model := make(map[string]interface{})
err = json.Unmarshal(blob, &model) err = json.Unmarshal(blob, &model)

View File

@ -1,4 +1,4 @@
package ngalert package models
import ( import (
"fmt" "fmt"
@ -33,8 +33,8 @@ func (i InstanceStateType) IsValid() bool {
i == InstanceStateNormal i == InstanceStateNormal
} }
// saveAlertInstanceCommand is the query for saving a new alert instance. // SaveAlertInstanceCommand is the query for saving a new alert instance.
type saveAlertInstanceCommand struct { type SaveAlertInstanceCommand struct {
DefinitionOrgID int64 DefinitionOrgID int64
DefinitionUID string DefinitionUID string
Labels InstanceLabels Labels InstanceLabels
@ -42,9 +42,9 @@ type saveAlertInstanceCommand struct {
LastEvalTime time.Time 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 // nolint:unused
type getAlertInstanceQuery struct { type GetAlertInstanceQuery struct {
DefinitionOrgID int64 DefinitionOrgID int64
DefinitionUID string DefinitionUID string
Labels InstanceLabels Labels InstanceLabels
@ -52,17 +52,17 @@ type getAlertInstanceQuery struct {
Result *AlertInstance Result *AlertInstance
} }
// listAlertInstancesCommand is the query list alert Instances. // ListAlertInstancesQuery is the query list alert Instances.
type listAlertInstancesQuery struct { type ListAlertInstancesQuery struct {
DefinitionOrgID int64 `json:"-"` DefinitionOrgID int64 `json:"-"`
DefinitionUID string DefinitionUID string
State InstanceStateType State InstanceStateType
Result []*listAlertInstancesQueryResult Result []*ListAlertInstancesQueryResult
} }
// listAlertInstancesQueryResult represents the result of listAlertInstancesQuery. // ListAlertInstancesQueryResult represents the result of listAlertInstancesQuery.
type listAlertInstancesQueryResult struct { type ListAlertInstancesQueryResult struct {
DefinitionOrgID int64 `xorm:"def_org_id" json:"definitionOrgId"` DefinitionOrgID int64 `xorm:"def_org_id" json:"definitionOrgId"`
DefinitionUID string `xorm:"def_uid" json:"definitionUid"` DefinitionUID string `xorm:"def_uid" json:"definitionUid"`
DefinitionTitle string `xorm:"def_title" json:"definitionTitle"` DefinitionTitle string `xorm:"def_title" json:"definitionTitle"`
@ -73,9 +73,9 @@ type listAlertInstancesQueryResult struct {
LastEvalTime time.Time `json:"lastEvalTime"` 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. // and state.
func validateAlertInstance(alertInstance *AlertInstance) error { func ValidateAlertInstance(alertInstance *AlertInstance) error {
if alertInstance == nil { if alertInstance == nil {
return fmt.Errorf("alert instance is invalid because it is nil") return fmt.Errorf("alert instance is invalid because it is nil")
} }

View File

@ -1,4 +1,4 @@
package ngalert package models
import ( import (
// nolint:gosec // nolint:gosec

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

View File

@ -4,6 +4,11 @@ import (
"context" "context"
"time" "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/benbjohnson/clock"
"github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
@ -36,8 +41,8 @@ type AlertNG struct {
RouteRegister routing.RouteRegister `inject:""` RouteRegister routing.RouteRegister `inject:""`
SQLStore *sqlstore.SQLStore `inject:""` SQLStore *sqlstore.SQLStore `inject:""`
DataService *tsdb.Service `inject:""` DataService *tsdb.Service `inject:""`
log log.Logger Log log.Logger
schedule scheduleService schedule schedule.ScheduleService
} }
func init() { func init() {
@ -46,37 +51,37 @@ func init() {
// Init initializes the AlertingService. // Init initializes the AlertingService.
func (ng *AlertNG) Init() error { func (ng *AlertNG) Init() error {
ng.log = log.New("ngalert") ng.Log = log.New("ngalert")
baseInterval := baseIntervalSeconds * time.Second baseInterval := baseIntervalSeconds * time.Second
store := storeImpl{baseInterval: baseInterval, SQLStore: ng.SQLStore} store := store.DBstore{BaseInterval: baseInterval, DefaultIntervalSeconds: defaultIntervalSeconds, SQLStore: ng.SQLStore}
schedCfg := schedulerCfg{ schedCfg := schedule.SchedulerCfg{
c: clock.New(), C: clock.New(),
baseInterval: baseInterval, BaseInterval: baseInterval,
logger: ng.log, Logger: ng.Log,
evaluator: eval.Evaluator{Cfg: ng.Cfg}, MaxAttempts: maxAttempts,
store: store, 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, Cfg: ng.Cfg,
DatasourceCache: ng.DatasourceCache, DatasourceCache: ng.DatasourceCache,
RouteRegister: ng.RouteRegister, RouteRegister: ng.RouteRegister,
DataService: ng.DataService, DataService: ng.DataService,
schedule: ng.schedule, Schedule: ng.schedule,
store: store, Store: store}
} api.RegisterAPIEndpoints()
api.registerAPIEndpoints()
return nil return nil
} }
// Run starts the scheduler // Run starts the scheduler
func (ng *AlertNG) Run(ctx context.Context) error { func (ng *AlertNG) Run(ctx context.Context) error {
ng.log.Debug("ngalert starting") ng.Log.Debug("ngalert starting")
return ng.schedule.Ticker(ctx) return ng.schedule.Ticker(ctx)
} }
@ -100,23 +105,3 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) {
// Create alert_instance table // Create alert_instance table
alertInstanceMigration(mg) 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
}

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

View File

@ -1,4 +1,4 @@
package ngalert package schedule
import ( import (
"context" "context"
@ -6,6 +6,10 @@ import (
"sync" "sync"
"time" "time"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/benbjohnson/clock" "github.com/benbjohnson/clock"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
@ -14,25 +18,29 @@ import (
"golang.org/x/sync/errgroup" "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 Ticker(context.Context) error
Pause() error Pause() error
Unpause() error Unpause() error
// the following are used by tests only used for tests // the following are used by tests only used for tests
evalApplied(alertDefinitionKey, time.Time) evalApplied(models.AlertDefinitionKey, time.Time)
stopApplied(alertDefinitionKey) stopApplied(models.AlertDefinitionKey)
overrideCfg(cfg schedulerCfg) 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 { evalCh <-chan *evalContext, stopCh <-chan struct{}) error {
sch.log.Debug("alert definition routine started", "key", key) sch.log.Debug("alert definition routine started", "key", key)
evalRunning := false evalRunning := false
var start, end time.Time var start, end time.Time
var attempt int64 var attempt int64
var alertDefinition *AlertDefinition var alertDefinition *models.AlertDefinition
for { for {
select { select {
case ctx := <-evalCh: case ctx := <-evalCh:
@ -45,8 +53,8 @@ func (sch *schedule) definitionRoutine(grafanaCtx context.Context, key alertDefi
// fetch latest alert definition version // fetch latest alert definition version
if alertDefinition == nil || alertDefinition.Version < ctx.version { if alertDefinition == nil || alertDefinition.Version < ctx.version {
q := getAlertDefinitionByUIDQuery{OrgID: key.orgID, UID: key.definitionUID} q := models.GetAlertDefinitionByUIDQuery{OrgID: key.OrgID, UID: key.DefinitionUID}
err := sch.store.getAlertDefinitionByUID(&q) err := sch.store.GetAlertDefinitionByUID(&q)
if err != nil { if err != nil {
sch.log.Error("failed to fetch alert definition", "key", key) sch.log.Error("failed to fetch alert definition", "key", key)
return err return err
@ -70,8 +78,8 @@ func (sch *schedule) definitionRoutine(grafanaCtx context.Context, key alertDefi
} }
for _, r := range results { 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()) 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} 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) err := sch.store.SaveAlertInstance(&cmd)
if err != nil { 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) 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 // 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 // function, and then it'll be called from the event loop whenever the
// message from evalApplied is handled. // 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 // 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 // function, and then it'll be called from the event loop whenever the
// message from stopApplied is handled. // message from stopApplied is handled.
stopAppliedFunc func(alertDefinitionKey) stopAppliedFunc func(models.AlertDefinitionKey)
log log.Logger log log.Logger
evaluator eval.Evaluator evaluator eval.Evaluator
store store store store.Store
dataService *tsdb.Service dataService *tsdb.Service
} }
type schedulerCfg struct { // SchedulerCfg is the scheduler configuration.
c clock.Clock type SchedulerCfg struct {
baseInterval time.Duration C clock.Clock
logger log.Logger BaseInterval time.Duration
evalAppliedFunc func(alertDefinitionKey, time.Time) Logger log.Logger
stopAppliedFunc func(alertDefinitionKey) EvalAppliedFunc func(models.AlertDefinitionKey, time.Time)
evaluator eval.Evaluator MaxAttempts int64
store store StopAppliedFunc func(models.AlertDefinitionKey)
Evaluator eval.Evaluator
Store store.Store
} }
// newScheduler returns a new schedule. // NewScheduler returns a new schedule.
func newScheduler(cfg schedulerCfg, dataService *tsdb.Service) *schedule { func NewScheduler(cfg SchedulerCfg, dataService *tsdb.Service) *schedule {
ticker := alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds())) ticker := alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
sch := schedule{ sch := schedule{
registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)}, registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[models.AlertDefinitionKey]alertDefinitionInfo)},
maxAttempts: maxAttempts, maxAttempts: cfg.MaxAttempts,
clock: cfg.c, clock: cfg.C,
baseInterval: cfg.baseInterval, baseInterval: cfg.BaseInterval,
log: cfg.logger, log: cfg.Logger,
heartbeat: ticker, heartbeat: ticker,
evalAppliedFunc: cfg.evalAppliedFunc, evalAppliedFunc: cfg.EvalAppliedFunc,
stopAppliedFunc: cfg.stopAppliedFunc, stopAppliedFunc: cfg.StopAppliedFunc,
evaluator: cfg.evaluator, evaluator: cfg.Evaluator,
store: cfg.store, store: cfg.Store,
dataService: dataService, dataService: dataService,
} }
return &sch return &sch
} }
func (sch *schedule) overrideCfg(cfg schedulerCfg) { func (sch *schedule) overrideCfg(cfg SchedulerCfg) {
sch.clock = cfg.c sch.clock = cfg.C
sch.baseInterval = cfg.baseInterval sch.baseInterval = cfg.BaseInterval
sch.heartbeat = alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds())) sch.heartbeat = alerting.NewTicker(cfg.C.Now(), time.Second*0, cfg.C, int64(cfg.BaseInterval.Seconds()))
sch.evalAppliedFunc = cfg.evalAppliedFunc sch.evalAppliedFunc = cfg.EvalAppliedFunc
sch.stopAppliedFunc = cfg.stopAppliedFunc 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 { if sch.evalAppliedFunc == nil {
return return
} }
@ -181,7 +191,7 @@ func (sch *schedule) evalApplied(alertDefKey alertDefinitionKey, now time.Time)
sch.evalAppliedFunc(alertDefKey, now) sch.evalAppliedFunc(alertDefKey, now)
} }
func (sch *schedule) stopApplied(alertDefKey alertDefinitionKey) { func (sch *schedule) stopApplied(alertDefKey models.AlertDefinitionKey) {
if sch.stopAppliedFunc == nil { if sch.stopAppliedFunc == nil {
return return
} }
@ -223,7 +233,7 @@ func (sch *schedule) Ticker(grafanaCtx context.Context) error {
registeredDefinitions := sch.registry.keyMap() registeredDefinitions := sch.registry.keyMap()
type readyToRunItem struct { type readyToRunItem struct {
key alertDefinitionKey key models.AlertDefinitionKey
definitionInfo alertDefinitionInfo definitionInfo alertDefinitionInfo
} }
readyToRun := make([]readyToRunItem, 0) readyToRun := make([]readyToRunItem, 0)
@ -232,7 +242,7 @@ func (sch *schedule) Ticker(grafanaCtx context.Context) error {
continue continue
} }
key := item.getKey() key := item.GetKey()
itemVersion := item.Version itemVersion := item.Version
newRoutine := !sch.registry.exists(key) newRoutine := !sch.registry.exists(key)
definitionInfo := sch.registry.getOrCreateInfo(key, itemVersion) definitionInfo := sch.registry.getOrCreateInfo(key, itemVersion)
@ -292,12 +302,12 @@ func (sch *schedule) Ticker(grafanaCtx context.Context) error {
type alertDefinitionRegistry struct { type alertDefinitionRegistry struct {
mu sync.Mutex mu sync.Mutex
alertDefinitionInfo map[alertDefinitionKey]alertDefinitionInfo alertDefinitionInfo map[models.AlertDefinitionKey]alertDefinitionInfo
} }
// getOrCreateInfo returns the channel for the specific alert definition // getOrCreateInfo returns the channel for the specific alert definition
// if it does not exists creates one and returns it // 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() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -313,7 +323,7 @@ func (r *alertDefinitionRegistry) getOrCreateInfo(key alertDefinitionKey, defini
// get returns the channel for the specific alert definition // get returns the channel for the specific alert definition
// if the key does not exist returns an error // 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() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -324,7 +334,7 @@ func (r *alertDefinitionRegistry) get(key alertDefinitionKey) (*alertDefinitionI
return &info, nil return &info, nil
} }
func (r *alertDefinitionRegistry) exists(key alertDefinitionKey) bool { func (r *alertDefinitionRegistry) exists(key models.AlertDefinitionKey) bool {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -332,15 +342,15 @@ func (r *alertDefinitionRegistry) exists(key alertDefinitionKey) bool {
return ok return ok
} }
func (r *alertDefinitionRegistry) del(key alertDefinitionKey) { func (r *alertDefinitionRegistry) del(key models.AlertDefinitionKey) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
delete(r.alertDefinitionInfo, key) delete(r.alertDefinitionInfo, key)
} }
func (r *alertDefinitionRegistry) iter() <-chan alertDefinitionKey { func (r *alertDefinitionRegistry) iter() <-chan models.AlertDefinitionKey {
c := make(chan alertDefinitionKey) c := make(chan models.AlertDefinitionKey)
f := func() { f := func() {
r.mu.Lock() r.mu.Lock()
@ -356,8 +366,8 @@ func (r *alertDefinitionRegistry) iter() <-chan alertDefinitionKey {
return c return c
} }
func (r *alertDefinitionRegistry) keyMap() map[alertDefinitionKey]struct{} { func (r *alertDefinitionRegistry) keyMap() map[models.AlertDefinitionKey]struct{} {
definitionsIDs := make(map[alertDefinitionKey]struct{}) definitionsIDs := make(map[models.AlertDefinitionKey]struct{})
for k := range r.iter() { for k := range r.iter() {
definitionsIDs[k] = struct{}{} definitionsIDs[k] = struct{}{}
} }

View File

@ -1,4 +1,4 @@
package ngalert package store
import ( import (
"context" "context"
@ -7,36 +7,51 @@ import (
"strings" "strings"
"time" "time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
type store interface { // TimeNow makes it possible to test usage of time
deleteAlertDefinitionByUID(*deleteAlertDefinitionByUIDCommand) error var TimeNow = time.Now
getAlertDefinitionByUID(*getAlertDefinitionByUIDQuery) error
getAlertDefinitions(query *listAlertDefinitionsQuery) error // AlertDefinitionMaxTitleLength is the maximum length of the alert definition titles
getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error const AlertDefinitionMaxTitleLength = 190
saveAlertDefinition(*saveAlertDefinitionCommand) error
updateAlertDefinition(*updateAlertDefinitionCommand) error // ErrEmptyTitleError is an error returned if the alert definition title is empty
getAlertInstance(*getAlertInstanceQuery) error var ErrEmptyTitleError = errors.New("title is empty")
listAlertInstances(cmd *listAlertInstancesQuery) error
saveAlertInstance(cmd *saveAlertInstanceCommand) error // Store is the interface for persisting alert definitions and instances
validateAlertDefinition(*AlertDefinition, bool) error type Store interface {
updateAlertDefinitionPaused(*updateAlertDefinitionPausedCommand) error 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 // the base scheduler tick rate; it's used for validating definition interval
baseInterval time.Duration BaseInterval time.Duration
SQLStore *sqlstore.SQLStore `inject:""` // 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 // we consider optionally enabling some caching
alertDefinition := AlertDefinition{OrgID: orgID, UID: alertDefinitionUID} alertDefinition := models.AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
has, err := sess.Get(&alertDefinition) has, err := sess.Get(&alertDefinition)
if !has { if !has {
return nil, errAlertDefinitionNotFound return nil, models.ErrAlertDefinitionNotFound
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -44,9 +59,9 @@ func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string
return &alertDefinition, nil 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. // 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 { 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) _, err := sess.Exec("DELETE FROM alert_definition WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID)
if err != nil { 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. // 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 { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID) alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID)
if err != nil { if err != nil {
@ -79,10 +94,10 @@ func (st storeImpl) getAlertDefinitionByUID(query *getAlertDefinitionByUIDQuery)
}) })
} }
// saveAlertDefinition is a handler for saving a new alert definition. // SaveAlertDefinition is a handler for saving a new alert definition.
func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error { func (st DBstore) SaveAlertDefinition(cmd *models.SaveAlertDefinitionCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
intervalSeconds := defaultIntervalSeconds intervalSeconds := st.DefaultIntervalSeconds
if cmd.IntervalSeconds != nil { if cmd.IntervalSeconds != nil {
intervalSeconds = *cmd.IntervalSeconds 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) return fmt.Errorf("failed to generate UID for alert definition %q: %w", cmd.Title, err)
} }
alertDefinition := &AlertDefinition{ alertDefinition := &models.AlertDefinition{
OrgID: cmd.OrgID, OrgID: cmd.OrgID,
Title: cmd.Title, Title: cmd.Title,
Condition: cmd.Condition, Condition: cmd.Condition,
@ -104,11 +119,11 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
UID: uid, UID: uid,
} }
if err := st.validateAlertDefinition(alertDefinition, false); err != nil { if err := st.ValidateAlertDefinition(alertDefinition, false); err != nil {
return err return err
} }
if err := alertDefinition.preSave(); err != nil { if err := alertDefinition.PreSave(TimeNow); err != nil {
return err return err
} }
@ -119,7 +134,7 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
return err return err
} }
alertDefVersion := AlertDefinitionVersion{ alertDefVersion := models.AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID, AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: alertDefinition.UID, AlertDefinitionUID: alertDefinition.UID,
Version: alertDefinition.Version, 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. // 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 { return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID) existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID)
if err != nil { if err != nil {
if errors.Is(err, errAlertDefinitionNotFound) { if errors.Is(err, models.ErrAlertDefinitionNotFound) {
return nil return nil
} }
return err return err
@ -168,7 +183,7 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
} }
// explicitly set all fields regardless of being provided or not // explicitly set all fields regardless of being provided or not
alertDefinition := &AlertDefinition{ alertDefinition := &models.AlertDefinition{
ID: existingAlertDefinition.ID, ID: existingAlertDefinition.ID,
Title: title, Title: title,
Condition: condition, Condition: condition,
@ -178,11 +193,11 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
UID: existingAlertDefinition.UID, UID: existingAlertDefinition.UID,
} }
if err := st.validateAlertDefinition(alertDefinition, true); err != nil { if err := st.ValidateAlertDefinition(alertDefinition, true); err != nil {
return err return err
} }
if err := alertDefinition.preSave(); err != nil { if err := alertDefinition.PreSave(TimeNow); err != nil {
return err return err
} }
@ -196,7 +211,7 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
return err return err
} }
alertDefVersion := AlertDefinitionVersion{ alertDefVersion := models.AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID, AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: alertDefinition.UID, AlertDefinitionUID: alertDefinition.UID,
ParentVersion: alertDefinition.Version, 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. // GetOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
func (st storeImpl) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error { func (st DBstore) GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) 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 = ?" q := "SELECT * FROM alert_definition WHERE org_id = ?"
if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil { if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil {
return err 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 { 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" q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_definition"
if err := sess.SQL(q).Find(&alerts); err != nil { if err := sess.SQL(q).Find(&alerts); err != nil {
return err 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 { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if len(cmd.UIDs) == 0 { if len(cmd.UIDs) == 0 {
return nil return nil
@ -282,7 +300,7 @@ func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (strin
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
uid := util.GenerateShortUID() 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 { if err != nil {
return "", err 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
} }

View File

@ -1,20 +1,20 @@
package ngalert package store
import ( import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore" "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. // the hash of the labels.
// nolint:unused // 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 { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
instance := AlertInstance{} instance := models.AlertInstance{}
s := strings.Builder{} s := strings.Builder{}
s.WriteString(`SELECT * FROM alert_instance s.WriteString(`SELECT * FROM alert_instance
WHERE 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. // 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 { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertInstances := make([]*listAlertInstancesQueryResult, 0) alertInstances := make([]*models.ListAlertInstancesQueryResult, 0)
s := strings.Builder{} s := strings.Builder{}
params := make([]interface{}, 0) 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 // 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 { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
labelTupleJSON, labelsHash, err := cmd.Labels.StringAndHash() labelTupleJSON, labelsHash, err := cmd.Labels.StringAndHash()
if err != nil { if err != nil {
return err return err
} }
alertInstance := &AlertInstance{ alertInstance := &models.AlertInstance{
DefinitionOrgID: cmd.DefinitionOrgID, DefinitionOrgID: cmd.DefinitionOrgID,
DefinitionUID: cmd.DefinitionUID, DefinitionUID: cmd.DefinitionUID,
Labels: cmd.Labels, Labels: cmd.Labels,
LabelsHash: labelsHash, LabelsHash: labelsHash,
CurrentState: cmd.State, CurrentState: cmd.State,
CurrentStateSince: time.Now(), CurrentStateSince: TimeNow(),
LastEvalTime: cmd.LastEvalTime, LastEvalTime: cmd.LastEvalTime,
} }
if err := validateAlertInstance(alertInstance); err != nil { if err := models.ValidateAlertInstance(alertInstance); err != nil {
return err return err
} }

View File

@ -1,6 +1,6 @@
// +build integration // +build integration
package ngalert package tests
import ( import (
"encoding/json" "encoding/json"
@ -8,15 +8,19 @@ import (
"testing" "testing"
"time" "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/registry"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const baseIntervalSeconds = 10
func mockTimeNow() { func mockTimeNow() {
var timeSeed int64 var timeSeed int64
timeNow = func() time.Time { store.TimeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0).UTC() fakeNow := time.Unix(timeSeed, 0).UTC()
timeSeed++ timeSeed++
return fakeNow return fakeNow
@ -24,13 +28,16 @@ func mockTimeNow() {
} }
func resetTimeNow() { func resetTimeNow() {
timeNow = time.Now store.TimeNow = time.Now
} }
func TestCreatingAlertDefinition(t *testing.T) { func TestCreatingAlertDefinition(t *testing.T) {
mockTimeNow() mockTimeNow()
defer resetTimeNow() defer resetTimeNow()
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
var customIntervalSeconds int64 = 120 var customIntervalSeconds int64 = 120
testCases := []struct { testCases := []struct {
desc string desc string
@ -45,7 +52,7 @@ func TestCreatingAlertDefinition(t *testing.T) {
desc: "should create successfully an alert definition with default interval", desc: "should create successfully an alert definition with default interval",
inputIntervalSeconds: nil, inputIntervalSeconds: nil,
inputTitle: "a name", inputTitle: "a name",
expectedInterval: defaultIntervalSeconds, expectedInterval: dbstore.DefaultIntervalSeconds,
expectedUpdated: time.Unix(0, 0).UTC(), 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", desc: "should fail to create an alert definition with too big name",
inputIntervalSeconds: &customIntervalSeconds, inputIntervalSeconds: &customIntervalSeconds,
inputTitle: getLongString(alertDefinitionMaxTitleLength + 1), inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
expectedError: errors.New(""), expectedError: errors.New(""),
}, },
{ {
desc: "should fail to create an alert definition with empty title", desc: "should fail to create an alert definition with empty title",
inputIntervalSeconds: &customIntervalSeconds, inputIntervalSeconds: &customIntervalSeconds,
inputTitle: "", inputTitle: "",
expectedError: errEmptyTitleError, expectedError: store.ErrEmptyTitleError,
}, },
} }
_, store := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
q := saveAlertDefinitionCommand{ q := models.SaveAlertDefinitionCommand{
OrgID: 1, OrgID: 1,
Title: tc.inputTitle, Title: tc.inputTitle,
Condition: "B", Condition: "B",
Data: []eval.AlertQuery{ Data: []models.AlertQuery{
{ {
Model: json.RawMessage(`{ Model: json.RawMessage(`{
"datasource": "__expr__", "datasource": "__expr__",
@ -87,9 +91,9 @@ func TestCreatingAlertDefinition(t *testing.T) {
"expression":"2 + 3 > 1" "expression":"2 + 3 > 1"
}`), }`),
RefID: "B", RefID: "B",
RelativeTimeRange: eval.RelativeTimeRange{ RelativeTimeRange: models.RelativeTimeRange{
From: eval.Duration(time.Duration(5) * time.Hour), From: models.Duration(time.Duration(5) * time.Hour),
To: eval.Duration(time.Duration(3) * time.Hour), To: models.Duration(time.Duration(3) * time.Hour),
}, },
}, },
}, },
@ -97,7 +101,7 @@ func TestCreatingAlertDefinition(t *testing.T) {
if tc.inputIntervalSeconds != nil { if tc.inputIntervalSeconds != nil {
q.IntervalSeconds = tc.inputIntervalSeconds q.IntervalSeconds = tc.inputIntervalSeconds
} }
err := store.saveAlertDefinition(&q) err := dbstore.SaveAlertDefinition(&q)
switch { switch {
case tc.expectedError != nil: case tc.expectedError != nil:
require.Error(t, err) require.Error(t, err)
@ -113,14 +117,14 @@ func TestCreatingAlertDefinition(t *testing.T) {
} }
func TestCreatingConflictionAlertDefinition(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) { 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) t.Cleanup(registry.ClearOverrides)
q := saveAlertDefinitionCommand{ q := models.SaveAlertDefinitionCommand{
OrgID: 1, OrgID: 1,
Title: "title", Title: "title",
Condition: "B", Condition: "B",
Data: []eval.AlertQuery{ Data: []models.AlertQuery{
{ {
Model: json.RawMessage(`{ Model: json.RawMessage(`{
"datasource": "__expr__", "datasource": "__expr__",
@ -128,20 +132,20 @@ func TestCreatingConflictionAlertDefinition(t *testing.T) {
"expression":"2 + 3 > 1" "expression":"2 + 3 > 1"
}`), }`),
RefID: "B", RefID: "B",
RelativeTimeRange: eval.RelativeTimeRange{ RelativeTimeRange: models.RelativeTimeRange{
From: eval.Duration(time.Duration(5) * time.Hour), From: models.Duration(time.Duration(5) * time.Hour),
To: eval.Duration(time.Duration(3) * time.Hour), To: models.Duration(time.Duration(3) * time.Hour),
}, },
}, },
}, },
} }
err := store.saveAlertDefinition(&q) err := dbstore.SaveAlertDefinition(&q)
require.NoError(t, err) require.NoError(t, err)
err = store.saveAlertDefinition(&q) err = dbstore.SaveAlertDefinition(&q)
require.Error(t, err) 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() mockTimeNow()
defer resetTimeNow() defer resetTimeNow()
_, store := setupTestEnv(t, baseIntervalSeconds) dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides) t.Cleanup(registry.ClearOverrides)
q := updateAlertDefinitionCommand{ q := models.UpdateAlertDefinitionCommand{
UID: "unknown", UID: "unknown",
OrgID: 1, OrgID: 1,
Title: "something completely different", Title: "something completely different",
Condition: "A", Condition: "A",
Data: []eval.AlertQuery{ Data: []models.AlertQuery{
{ {
Model: json.RawMessage(`{ Model: json.RawMessage(`{
"datasource": "__expr__", "datasource": "__expr__",
@ -166,15 +170,15 @@ func TestUpdatingAlertDefinition(t *testing.T) {
"expression":"2 + 2 > 1" "expression":"2 + 2 > 1"
}`), }`),
RefID: "A", RefID: "A",
RelativeTimeRange: eval.RelativeTimeRange{ RelativeTimeRange: models.RelativeTimeRange{
From: eval.Duration(time.Duration(5) * time.Hour), From: models.Duration(time.Duration(5) * time.Hour),
To: eval.Duration(time.Duration(3) * time.Hour), To: models.Duration(time.Duration(3) * time.Hour),
}, },
}, },
}, },
} }
err := store.updateAlertDefinition(&q) err := dbstore.UpdateAlertDefinition(&q)
require.NoError(t, err) require.NoError(t, err)
}) })
@ -182,11 +186,11 @@ func TestUpdatingAlertDefinition(t *testing.T) {
mockTimeNow() mockTimeNow()
defer resetTimeNow() defer resetTimeNow()
_, store := setupTestEnv(t, baseIntervalSeconds) dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides) t.Cleanup(registry.ClearOverrides)
var initialInterval int64 = 120 var initialInterval int64 = 120
alertDefinition := createTestAlertDefinition(t, store, initialInterval) alertDefinition := createTestAlertDefinition(t, dbstore, initialInterval)
created := alertDefinition.Updated created := alertDefinition.Updated
var customInterval int64 = 30 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", desc: "should not update alert definition if the title it's too big",
inputInterval: &customInterval, inputInterval: &customInterval,
inputOrgID: 0, inputOrgID: 0,
inputTitle: getLongString(alertDefinitionMaxTitleLength + 1), inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
expectedError: errors.New(""), expectedError: errors.New(""),
}, },
{ {
@ -245,10 +249,10 @@ func TestUpdatingAlertDefinition(t *testing.T) {
}, },
} }
q := updateAlertDefinitionCommand{ q := models.UpdateAlertDefinitionCommand{
UID: (*alertDefinition).UID, UID: (*alertDefinition).UID,
Condition: "B", Condition: "B",
Data: []eval.AlertQuery{ Data: []models.AlertQuery{
{ {
Model: json.RawMessage(`{ Model: json.RawMessage(`{
"datasource": "__expr__", "datasource": "__expr__",
@ -256,9 +260,9 @@ func TestUpdatingAlertDefinition(t *testing.T) {
"expression":"2 + 3 > 1" "expression":"2 + 3 > 1"
}`), }`),
RefID: "B", RefID: "B",
RelativeTimeRange: eval.RelativeTimeRange{ RelativeTimeRange: models.RelativeTimeRange{
From: eval.Duration(5 * time.Hour), From: models.Duration(5 * time.Hour),
To: eval.Duration(3 * time.Hour), To: models.Duration(3 * time.Hour),
}, },
}, },
}, },
@ -275,7 +279,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
q.OrgID = tc.inputOrgID q.OrgID = tc.inputOrgID
} }
q.Title = tc.inputTitle q.Title = tc.inputTitle
err := store.updateAlertDefinition(&q) err := dbstore.UpdateAlertDefinition(&q)
switch { switch {
case tc.expectedError != nil: case tc.expectedError != nil:
require.Error(t, err) require.Error(t, err)
@ -321,18 +325,18 @@ func TestUpdatingConflictingAlertDefinition(t *testing.T) {
mockTimeNow() mockTimeNow()
defer resetTimeNow() defer resetTimeNow()
_, store := setupTestEnv(t, baseIntervalSeconds) dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides) t.Cleanup(registry.ClearOverrides)
var initialInterval int64 = 120 var initialInterval int64 = 120
alertDef1 := createTestAlertDefinition(t, store, initialInterval) alertDef1 := createTestAlertDefinition(t, dbstore, initialInterval)
alertDef2 := createTestAlertDefinition(t, store, initialInterval) alertDef2 := createTestAlertDefinition(t, dbstore, initialInterval)
q := updateAlertDefinitionCommand{ q := models.UpdateAlertDefinitionCommand{
UID: (*alertDef2).UID, UID: (*alertDef2).UID,
Title: alertDef1.Title, Title: alertDef1.Title,
Condition: "B", Condition: "B",
Data: []eval.AlertQuery{ Data: []models.AlertQuery{
{ {
Model: json.RawMessage(`{ Model: json.RawMessage(`{
"datasource": "__expr__", "datasource": "__expr__",
@ -340,70 +344,70 @@ func TestUpdatingConflictingAlertDefinition(t *testing.T) {
"expression":"2 + 3 > 1" "expression":"2 + 3 > 1"
}`), }`),
RefID: "B", RefID: "B",
RelativeTimeRange: eval.RelativeTimeRange{ RelativeTimeRange: models.RelativeTimeRange{
From: eval.Duration(5 * time.Hour), From: models.Duration(5 * time.Hour),
To: eval.Duration(3 * time.Hour), To: models.Duration(3 * time.Hour),
}, },
}, },
}, },
} }
err := store.updateAlertDefinition(&q) err := dbstore.UpdateAlertDefinition(&q)
require.Error(t, err) 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) { func TestDeletingAlertDefinition(t *testing.T) {
t.Run("zero rows affected when deleting unknown alert", func(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) t.Cleanup(registry.ClearOverrides)
q := deleteAlertDefinitionByUIDCommand{ q := models.DeleteAlertDefinitionByUIDCommand{
UID: "unknown", UID: "unknown",
OrgID: 1, OrgID: 1,
} }
err := store.deleteAlertDefinitionByUID(&q) err := dbstore.DeleteAlertDefinitionByUID(&q)
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("deleting successfully existing alert", func(t *testing.T) { t.Run("deleting successfully existing alert", func(t *testing.T) {
_, store := setupTestEnv(t, baseIntervalSeconds) dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides) t.Cleanup(registry.ClearOverrides)
alertDefinition := createTestAlertDefinition(t, store, 60) alertDefinition := createTestAlertDefinition(t, dbstore, 60)
q := deleteAlertDefinitionByUIDCommand{ q := models.DeleteAlertDefinitionByUIDCommand{
UID: (*alertDefinition).UID, UID: (*alertDefinition).UID,
OrgID: 1, OrgID: 1,
} }
// save an instance for the definition // save an instance for the definition
saveCmd := &saveAlertInstanceCommand{ saveCmd := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition.OrgID, DefinitionOrgID: alertDefinition.OrgID,
DefinitionUID: alertDefinition.UID, DefinitionUID: alertDefinition.UID,
State: InstanceStateFiring, State: models.InstanceStateFiring,
Labels: InstanceLabels{"test": "testValue"}, Labels: models.InstanceLabels{"test": "testValue"},
} }
err := store.saveAlertInstance(saveCmd) err := dbstore.SaveAlertInstance(saveCmd)
require.NoError(t, err) require.NoError(t, err)
listCommand := &listAlertInstancesQuery{ listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: alertDefinition.OrgID, DefinitionOrgID: alertDefinition.OrgID,
DefinitionUID: alertDefinition.UID, DefinitionUID: alertDefinition.UID,
} }
err = store.listAlertInstances(listCommand) err = dbstore.ListAlertInstances(listQuery)
require.NoError(t, err) 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) require.NoError(t, err)
// assert that alert instance is deleted // assert that alert instance is deleted
err = store.listAlertInstances(listCommand) err = dbstore.ListAlertInstances(listQuery)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, listCommand.Result, 0) require.Len(t, listQuery.Result, 0)
}) })
} }

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

View File

@ -1,4 +1,4 @@
package ngalert package tests
import ( import (
"context" "context"
@ -8,7 +8,12 @@ import (
"testing" "testing"
"time" "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/registry"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -16,47 +21,49 @@ import (
) )
type evalAppliedInfo struct { type evalAppliedInfo struct {
alertDefKey alertDefinitionKey alertDefKey models.AlertDefinitionKey
now time.Time now time.Time
} }
func TestAlertingTicker(t *testing.T) { func TestAlertingTicker(t *testing.T) {
ng, store := setupTestEnv(t, 1) dbstore := setupTestEnv(t, 1)
t.Cleanup(registry.ClearOverrides) t.Cleanup(registry.ClearOverrides)
alerts := make([]*AlertDefinition, 0) alerts := make([]*models.AlertDefinition, 0)
// create alert definition with zero interval (should never run) // 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 // 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)) evalAppliedCh := make(chan evalAppliedInfo, len(alerts))
stopAppliedCh := make(chan alertDefinitionKey, len(alerts)) stopAppliedCh := make(chan models.AlertDefinitionKey, len(alerts))
mockedClock := clock.NewMock() mockedClock := clock.NewMock()
baseInterval := time.Second baseInterval := time.Second
schefCfg := schedulerCfg{ schefCfg := schedule.SchedulerCfg{
c: mockedClock, C: mockedClock,
baseInterval: baseInterval, BaseInterval: baseInterval,
evalAppliedFunc: func(alertDefKey alertDefinitionKey, now time.Time) { EvalAppliedFunc: func(alertDefKey models.AlertDefinitionKey, now time.Time) {
evalAppliedCh <- evalAppliedInfo{alertDefKey: alertDefKey, now: now} evalAppliedCh <- evalAppliedInfo{alertDefKey: alertDefKey, now: now}
}, },
stopAppliedFunc: func(alertDefKey alertDefinitionKey) { StopAppliedFunc: func(alertDefKey models.AlertDefinitionKey) {
stopAppliedCh <- alertDefKey stopAppliedCh <- alertDefKey
}, },
Store: dbstore,
Logger: log.New("ngalert schedule test"),
} }
ng.schedule.overrideCfg(schefCfg) sched := schedule.NewScheduler(schefCfg, nil)
ctx := context.Background() ctx := context.Background()
go func() { go func() {
err := ng.schedule.Ticker(ctx) err := sched.Ticker(ctx)
require.NoError(t, err) require.NoError(t, err)
}() }()
runtime.Gosched() 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) { t.Run(fmt.Sprintf("on 1st tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
@ -64,93 +71,93 @@ func TestAlertingTicker(t *testing.T) {
// change alert definition interval to three seconds // change alert definition interval to three seconds
var threeSecInterval int64 = 3 var threeSecInterval int64 = 3
err := store.updateAlertDefinition(&updateAlertDefinitionCommand{ err := dbstore.UpdateAlertDefinition(&models.UpdateAlertDefinitionCommand{
UID: alerts[0].UID, UID: alerts[0].UID,
IntervalSeconds: &threeSecInterval, IntervalSeconds: &threeSecInterval,
OrgID: alerts[0].OrgID, OrgID: alerts[0].OrgID,
}) })
require.NoError(t, err) 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) { t.Run(fmt.Sprintf("on 2nd tick alert definition: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) 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) { t.Run(fmt.Sprintf("on 3rd tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) 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) { t.Run(fmt.Sprintf("on 4th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) 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) 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) { t.Run(fmt.Sprintf("on 5th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) 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) { t.Run(fmt.Sprintf("on 5th tick alert definitions: %s should be stopped", concatenate(expectedAlertDefinitionsStopped)), func(t *testing.T) {
assertStopRun(t, stopAppliedCh, expectedAlertDefinitionsStopped...) 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) { t.Run(fmt.Sprintf("on 6th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
// create alert definition with one second interval // 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) { t.Run(fmt.Sprintf("on 7th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
// pause alert definition // 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) 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) { t.Run(fmt.Sprintf("on 8th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) 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) { t.Run(fmt.Sprintf("on 8th tick alert definitions: %s should be stopped", concatenate(expectedAlertDefinitionsStopped)), func(t *testing.T) {
assertStopRun(t, stopAppliedCh, expectedAlertDefinitionsStopped...) assertStopRun(t, stopAppliedCh, expectedAlertDefinitionsStopped...)
}) })
// unpause alert definition // 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) 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) { t.Run(fmt.Sprintf("on 9th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) 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) timeout := time.After(time.Second)
expected := make(map[alertDefinitionKey]struct{}, len(keys)) expected := make(map[models.AlertDefinitionKey]struct{}, len(keys))
for _, k := range keys { for _, k := range keys {
expected[k] = struct{}{} 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) timeout := time.After(time.Second)
expected := make(map[alertDefinitionKey]struct{}, len(keys)) expected := make(map[models.AlertDefinitionKey]struct{}, len(keys))
for _, k := range keys { for _, k := range keys {
expected[k] = struct{}{} expected[k] = struct{}{}
} }
@ -208,7 +215,7 @@ func advanceClock(t *testing.T, mockedClock *clock.Mock) time.Time {
// t.Logf("Tick: %v", mockedClock.Now()) // t.Logf("Tick: %v", mockedClock.Now())
} }
func concatenate(keys []alertDefinitionKey) string { func concatenate(keys []models.AlertDefinitionKey) string {
s := make([]string, len(keys)) s := make([]string, len(keys))
for _, k := range keys { for _, k := range keys {
s = append(s, k.String()) s = append(s, k.String())

View File

@ -1,4 +1,4 @@
package ngalert package tests
import ( import (
"encoding/json" "encoding/json"
@ -7,18 +7,25 @@ import (
"testing" "testing"
"time" "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/api/routing"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry" "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/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require" "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() 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} cfg.FeatureToggles = map[string]bool{"ngalert": true}
ng := overrideAlertNGInRegistry(t, cfg) ng := overrideAlertNGInRegistry(t, cfg)
@ -26,18 +33,20 @@ func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl)
err := ng.Init() err := ng.Init()
require.NoError(t, err) 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 { func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) ngalert.AlertNG {
ng := AlertNG{ ng := ngalert.AlertNG{
Cfg: cfg, Cfg: cfg,
RouteRegister: routing.NewRouteRegister(), 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) { overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
if _, ok := descriptor.Instance.(*AlertNG); ok { if _, ok := descriptor.Instance.(*ngalert.AlertNG); ok {
return &registry.Descriptor{ return &registry.Descriptor{
Name: descriptor.Name, Name: descriptor.Name,
Instance: &ng, Instance: &ng,
@ -52,29 +61,30 @@ func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG {
return ng return ng
} }
func createTestAlertDefinition(t *testing.T, store *storeImpl, intervalSeconds int64) *AlertDefinition { // createTestAlertDefinition creates a dummy alert definition to be used by the tests.
cmd := saveAlertDefinitionCommand{ func createTestAlertDefinition(t *testing.T, store *store.DBstore, intervalSeconds int64) *models.AlertDefinition {
cmd := models.SaveAlertDefinitionCommand{
OrgID: 1, OrgID: 1,
Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)), Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)),
Condition: "A", Condition: "A",
Data: []eval.AlertQuery{ Data: []models.AlertQuery{
{ {
Model: json.RawMessage(`{ Model: json.RawMessage(`{
"datasource": "__expr__", "datasource": "__expr__",
"type":"math", "type":"math",
"expression":"2 + 2 > 1" "expression":"2 + 2 > 1"
}`), }`),
RelativeTimeRange: eval.RelativeTimeRange{ RelativeTimeRange: models.RelativeTimeRange{
From: eval.Duration(5 * time.Hour), From: models.Duration(5 * time.Hour),
To: eval.Duration(3 * time.Hour), To: models.Duration(3 * time.Hour),
}, },
RefID: "A", RefID: "A",
}, },
}, },
IntervalSeconds: &intervalSeconds, IntervalSeconds: &intervalSeconds,
} }
err := store.saveAlertDefinition(&cmd) err := store.SaveAlertDefinition(&cmd)
require.NoError(t, err) 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 return cmd.Result
} }

View File

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