AlertingNG: Split into several packages (#31719)

* AlertingNG: Split into several packages

* Move AlertQuery to models
pull/31789/head
Sofia Papagiannaki 4 years ago committed by GitHub
parent 124ef813ab
commit 4ce0a49eac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      go.mod
  2. 4
      go.sum
  3. 22
      pkg/services/ngalert/alert_definition.go
  4. 140
      pkg/services/ngalert/api/api.go
  5. 19
      pkg/services/ngalert/api/instance_api.go
  6. 23
      pkg/services/ngalert/api/middleware.go
  7. 12
      pkg/services/ngalert/eval/eval.go
  8. 15
      pkg/services/ngalert/fetcher.go
  9. 17
      pkg/services/ngalert/instance_api.go
  10. 163
      pkg/services/ngalert/instance_database_test.go
  11. 21
      pkg/services/ngalert/middleware.go
  12. 115
      pkg/services/ngalert/models.go
  13. 12
      pkg/services/ngalert/models/alert_query.go
  14. 6
      pkg/services/ngalert/models/alert_query_test.go
  15. 24
      pkg/services/ngalert/models/instance.go
  16. 2
      pkg/services/ngalert/models/instance_labels.go
  17. 147
      pkg/services/ngalert/models/models.go
  18. 59
      pkg/services/ngalert/ngalert.go
  19. 17
      pkg/services/ngalert/schedule/fetcher.go
  20. 116
      pkg/services/ngalert/schedule/schedule.go
  21. 135
      pkg/services/ngalert/store/database.go
  22. 26
      pkg/services/ngalert/store/instance_database.go
  23. 138
      pkg/services/ngalert/tests/database_test.go
  24. 165
      pkg/services/ngalert/tests/instance_database_test.go
  25. 85
      pkg/services/ngalert/tests/schedule_test.go
  26. 42
      pkg/services/ngalert/tests/util.go
  27. 79
      pkg/services/ngalert/validator.go

@ -68,6 +68,7 @@ require (
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.18.0
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 // indirect
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.0

@ -1303,6 +1303,10 @@ github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4 h1:54z99l8
github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4/go.mod h1:XYjkJiog7fyQu3puQNivZPI2pNq1C/775EIoHfDvuvY=
github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasilyte/go-ruleguard v0.3.1 h1:2KTXnHBCR4BUl8UAL2bCUorOBGC8RsmYncuDA9NEFW4=
github.com/quasilyte/go-ruleguard/dsl v0.3.1 h1:CHGOKP2LDz35P49TjW4Bx4BCfFI6ZZU/8zcneECD0q4=
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY=
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM=
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=

@ -1,22 +0,0 @@
package ngalert
import (
"fmt"
"time"
)
// timeNow makes it possible to test usage of time
var timeNow = time.Now
// preSave sets datasource and loads the updated model for each alert query.
func (alertDefinition *AlertDefinition) preSave() error {
for i, q := range alertDefinition.Data {
err := q.PreSave()
if err != nil {
return fmt.Errorf("invalid alert query %s: %w", q.RefID, err)
}
alertDefinition.Data[i] = q
}
alertDefinition.Updated = timeNow()
return nil
}

@ -1,7 +1,13 @@
package ngalert
package api
import (
"fmt"
"time"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/go-macaron/binding"
"github.com/grafana/grafana-plugin-sdk-go/data"
@ -17,26 +23,31 @@ import (
"github.com/grafana/grafana/pkg/util"
)
type apiImpl struct {
Cfg *setting.Cfg `inject:""`
DatasourceCache datasources.CacheService `inject:""`
RouteRegister routing.RouteRegister `inject:""`
// timeNow makes it possible to test usage of time
var timeNow = time.Now
// API handlers.
type API struct {
Cfg *setting.Cfg
DatasourceCache datasources.CacheService
RouteRegister routing.RouteRegister
DataService *tsdb.Service
schedule scheduleService
store store
Schedule schedule.ScheduleService
Store store.Store
}
func (api *apiImpl) registerAPIEndpoints() {
// RegisterAPIEndpoints registers API handlers
func (api *API) RegisterAPIEndpoints() {
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertDefinitions))
alertDefinitions.Get("/eval/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.alertDefinitionEvalEndpoint))
alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(evalAlertConditionCommand{}), routing.Wrap(api.conditionEvalEndpoint))
alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(ngmodels.EvalAlertConditionCommand{}), routing.Wrap(api.conditionEvalEndpoint))
alertDefinitions.Get("/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.getAlertDefinitionEndpoint))
alertDefinitions.Delete("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, routing.Wrap(api.deleteAlertDefinitionEndpoint))
alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(saveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint))
alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(updateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint))
alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(updateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint))
alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(updateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint))
alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(ngmodels.SaveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint))
alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(ngmodels.UpdateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint))
alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint))
alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint))
})
api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) {
@ -50,7 +61,7 @@ func (api *apiImpl) registerAPIEndpoints() {
}
// conditionEvalEndpoint handles POST /api/alert-definitions/eval.
func (api *apiImpl) conditionEvalEndpoint(c *models.ReqContext, cmd evalAlertConditionCommand) response.Response {
func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response {
evalCond := eval.Condition{
RefID: cmd.Condition,
OrgID: c.SignedInUser.OrgId,
@ -84,7 +95,7 @@ func (api *apiImpl) conditionEvalEndpoint(c *models.ReqContext, cmd evalAlertCon
}
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
func (api *apiImpl) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response {
func (api *API) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response {
alertDefinitionUID := c.Params(":alertDefinitionUID")
condition, err := api.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId)
@ -118,15 +129,15 @@ func (api *apiImpl) alertDefinitionEvalEndpoint(c *models.ReqContext) response.R
}
// getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionUID.
func (api *apiImpl) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
func (api *API) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
alertDefinitionUID := c.Params(":alertDefinitionUID")
query := getAlertDefinitionByUIDQuery{
query := ngmodels.GetAlertDefinitionByUIDQuery{
UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId,
}
if err := api.store.getAlertDefinitionByUID(&query); err != nil {
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
return response.Error(500, "Failed to get alert definition", err)
}
@ -134,15 +145,15 @@ func (api *apiImpl) getAlertDefinitionEndpoint(c *models.ReqContext) response.Re
}
// deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionUID.
func (api *apiImpl) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
func (api *API) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
alertDefinitionUID := c.Params(":alertDefinitionUID")
cmd := deleteAlertDefinitionByUIDCommand{
cmd := ngmodels.DeleteAlertDefinitionByUIDCommand{
UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId,
}
if err := api.store.deleteAlertDefinitionByUID(&cmd); err != nil {
if err := api.Store.DeleteAlertDefinitionByUID(&cmd); err != nil {
return response.Error(500, "Failed to delete alert definition", err)
}
@ -150,7 +161,7 @@ func (api *apiImpl) deleteAlertDefinitionEndpoint(c *models.ReqContext) response
}
// updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionUID.
func (api *apiImpl) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updateAlertDefinitionCommand) response.Response {
func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionCommand) response.Response {
cmd.UID = c.Params(":alertDefinitionUID")
cmd.OrgID = c.SignedInUser.OrgId
@ -163,7 +174,7 @@ func (api *apiImpl) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd upda
return response.Error(400, "invalid condition", err)
}
if err := api.store.updateAlertDefinition(&cmd); err != nil {
if err := api.Store.UpdateAlertDefinition(&cmd); err != nil {
return response.Error(500, "Failed to update alert definition", err)
}
@ -171,7 +182,7 @@ func (api *apiImpl) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd upda
}
// createAlertDefinitionEndpoint handles POST /api/alert-definitions.
func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd saveAlertDefinitionCommand) response.Response {
func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.SaveAlertDefinitionCommand) response.Response {
cmd.OrgID = c.SignedInUser.OrgId
evalCond := eval.Condition{
@ -183,7 +194,7 @@ func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd save
return response.Error(400, "invalid condition", err)
}
if err := api.store.saveAlertDefinition(&cmd); err != nil {
if err := api.Store.SaveAlertDefinition(&cmd); err != nil {
return response.Error(500, "Failed to create alert definition", err)
}
@ -191,26 +202,26 @@ func (api *apiImpl) createAlertDefinitionEndpoint(c *models.ReqContext, cmd save
}
// listAlertDefinitions handles GET /api/alert-definitions.
func (api *apiImpl) listAlertDefinitions(c *models.ReqContext) response.Response {
query := listAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId}
func (api *API) listAlertDefinitions(c *models.ReqContext) response.Response {
query := ngmodels.ListAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId}
if err := api.store.getOrgAlertDefinitions(&query); err != nil {
if err := api.Store.GetOrgAlertDefinitions(&query); err != nil {
return response.Error(500, "Failed to list alert definitions", err)
}
return response.JSON(200, util.DynMap{"results": query.Result})
}
func (api *apiImpl) pauseScheduler() response.Response {
err := api.schedule.Pause()
func (api *API) pauseScheduler() response.Response {
err := api.Schedule.Pause()
if err != nil {
return response.Error(500, "Failed to pause scheduler", err)
}
return response.JSON(200, util.DynMap{"message": "alert definition scheduler paused"})
}
func (api *apiImpl) unpauseScheduler() response.Response {
err := api.schedule.Unpause()
func (api *API) unpauseScheduler() response.Response {
err := api.Schedule.Unpause()
if err != nil {
return response.Error(500, "Failed to unpause scheduler", err)
}
@ -218,11 +229,11 @@ func (api *apiImpl) unpauseScheduler() response.Response {
}
// alertDefinitionPauseEndpoint handles POST /api/alert-definitions/pause.
func (api *apiImpl) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd updateAlertDefinitionPausedCommand) response.Response {
func (api *API) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response {
cmd.OrgID = c.SignedInUser.OrgId
cmd.Paused = true
err := api.store.updateAlertDefinitionPaused(&cmd)
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
if err != nil {
return response.Error(500, "Failed to pause alert definition", err)
}
@ -230,13 +241,70 @@ func (api *apiImpl) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd updat
}
// alertDefinitionUnpauseEndpoint handles POST /api/alert-definitions/unpause.
func (api *apiImpl) alertDefinitionUnpauseEndpoint(c *models.ReqContext, cmd updateAlertDefinitionPausedCommand) response.Response {
func (api *API) alertDefinitionUnpauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response {
cmd.OrgID = c.SignedInUser.OrgId
cmd.Paused = false
err := api.store.updateAlertDefinitionPaused(&cmd)
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
if err != nil {
return response.Error(500, "Failed to unpause alert definition", err)
}
return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions unpaused", cmd.ResultCount)})
}
// LoadAlertCondition returns a Condition object for the given alertDefinitionID.
func (api *API) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*eval.Condition, error) {
q := ngmodels.GetAlertDefinitionByUIDQuery{UID: alertDefinitionUID, OrgID: orgID}
if err := api.Store.GetAlertDefinitionByUID(&q); err != nil {
return nil, err
}
alertDefinition := q.Result
err := api.Store.ValidateAlertDefinition(alertDefinition, true)
if err != nil {
return nil, err
}
return &eval.Condition{
RefID: alertDefinition.Condition,
OrgID: alertDefinition.OrgID,
QueriesAndExpressions: alertDefinition.Data,
}, nil
}
func (api *API) validateCondition(c eval.Condition, user *models.SignedInUser, skipCache bool) error {
var refID string
if len(c.QueriesAndExpressions) == 0 {
return nil
}
for _, query := range c.QueriesAndExpressions {
if c.RefID == query.RefID {
refID = c.RefID
}
datasourceUID, err := query.GetDatasource()
if err != nil {
return err
}
isExpression, err := query.IsExpression()
if err != nil {
return err
}
if isExpression {
continue
}
_, err = api.DatasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
if err != nil {
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
}
}
if refID == "" {
return fmt.Errorf("condition %s not found in any query or expression", c.RefID)
}
return nil
}

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

@ -0,0 +1,23 @@
package api
import (
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/models"
)
func (api *API) validateOrgAlertDefinition(c *models.ReqContext) {
uid := c.ParamsEscape(":alertDefinitionUID")
if uid == "" {
c.JsonApiErr(403, "Permission denied", nil)
return
}
query := ngmodels.GetAlertDefinitionByUIDQuery{UID: uid, OrgID: c.SignedInUser.OrgId}
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
c.JsonApiErr(404, "Alert definition not found", nil)
return
}
}

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

@ -1,15 +0,0 @@
package ngalert
import (
"time"
)
func (sch *schedule) fetchAllDetails(now time.Time) []*AlertDefinition {
q := listAlertDefinitionsQuery{}
err := sch.store.getAlertDefinitions(&q)
if err != nil {
sch.log.Error("failed to fetch alert definitions", "now", now, "err", err)
return nil
}
return q.Result
}

@ -1,17 +0,0 @@
package ngalert
import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
)
// listAlertInstancesEndpoint handles GET /api/alert-instances.
func (api *apiImpl) listAlertInstancesEndpoint(c *models.ReqContext) response.Response {
cmd := listAlertInstancesQuery{DefinitionOrgID: c.SignedInUser.OrgId}
if err := api.store.listAlertInstances(&cmd); err != nil {
return response.Error(500, "Failed to list alert instances", err)
}
return response.JSON(200, cmd.Result)
}

@ -1,163 +0,0 @@
// +build integration
package ngalert
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAlertInstanceOperations(t *testing.T) {
_, store := setupTestEnv(t, baseIntervalSeconds)
alertDefinition1 := createTestAlertDefinition(t, store, 60)
orgID := alertDefinition1.OrgID
alertDefinition2 := createTestAlertDefinition(t, store, 60)
require.Equal(t, orgID, alertDefinition2.OrgID)
alertDefinition3 := createTestAlertDefinition(t, store, 60)
require.Equal(t, orgID, alertDefinition3.OrgID)
alertDefinition4 := createTestAlertDefinition(t, store, 60)
require.Equal(t, orgID, alertDefinition4.OrgID)
t.Run("can save and read new alert instance", func(t *testing.T) {
saveCmd := &saveAlertInstanceCommand{
DefinitionOrgID: alertDefinition1.OrgID,
DefinitionUID: alertDefinition1.UID,
State: InstanceStateFiring,
Labels: InstanceLabels{"test": "testValue"},
}
err := store.saveAlertInstance(saveCmd)
require.NoError(t, err)
getCmd := &getAlertInstanceQuery{
DefinitionOrgID: saveCmd.DefinitionOrgID,
DefinitionUID: saveCmd.DefinitionUID,
Labels: InstanceLabels{"test": "testValue"},
}
err = store.getAlertInstance(getCmd)
require.NoError(t, err)
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
require.Equal(t, alertDefinition1.OrgID, getCmd.Result.DefinitionOrgID)
require.Equal(t, alertDefinition1.UID, getCmd.Result.DefinitionUID)
})
t.Run("can save and read new alert instance with no labels", func(t *testing.T) {
saveCmd := &saveAlertInstanceCommand{
DefinitionOrgID: alertDefinition2.OrgID,
DefinitionUID: alertDefinition2.UID,
State: InstanceStateNormal,
}
err := store.saveAlertInstance(saveCmd)
require.NoError(t, err)
getCmd := &getAlertInstanceQuery{
DefinitionOrgID: saveCmd.DefinitionOrgID,
DefinitionUID: saveCmd.DefinitionUID,
}
err = store.getAlertInstance(getCmd)
require.NoError(t, err)
require.Equal(t, alertDefinition2.OrgID, getCmd.Result.DefinitionOrgID)
require.Equal(t, alertDefinition2.UID, getCmd.Result.DefinitionUID)
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
})
t.Run("can save two instances with same org_id, uid and different labels", func(t *testing.T) {
saveCmdOne := &saveAlertInstanceCommand{
DefinitionOrgID: alertDefinition3.OrgID,
DefinitionUID: alertDefinition3.UID,
State: InstanceStateFiring,
Labels: InstanceLabels{"test": "testValue"},
}
err := store.saveAlertInstance(saveCmdOne)
require.NoError(t, err)
saveCmdTwo := &saveAlertInstanceCommand{
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
DefinitionUID: saveCmdOne.DefinitionUID,
State: InstanceStateFiring,
Labels: InstanceLabels{"test": "meow"},
}
err = store.saveAlertInstance(saveCmdTwo)
require.NoError(t, err)
listCommand := &listAlertInstancesQuery{
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
DefinitionUID: saveCmdOne.DefinitionUID,
}
err = store.listAlertInstances(listCommand)
require.NoError(t, err)
require.Len(t, listCommand.Result, 2)
})
t.Run("can list all added instances in org", func(t *testing.T) {
listCommand := &listAlertInstancesQuery{
DefinitionOrgID: orgID,
}
err := store.listAlertInstances(listCommand)
require.NoError(t, err)
require.Len(t, listCommand.Result, 4)
})
t.Run("can list all added instances in org filtered by current state", func(t *testing.T) {
listCommand := &listAlertInstancesQuery{
DefinitionOrgID: orgID,
State: InstanceStateNormal,
}
err := store.listAlertInstances(listCommand)
require.NoError(t, err)
require.Len(t, listCommand.Result, 1)
})
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
saveCmdOne := &saveAlertInstanceCommand{
DefinitionOrgID: alertDefinition4.OrgID,
DefinitionUID: alertDefinition4.UID,
State: InstanceStateFiring,
Labels: InstanceLabels{"test": "testValue"},
}
err := store.saveAlertInstance(saveCmdOne)
require.NoError(t, err)
saveCmdTwo := &saveAlertInstanceCommand{
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
DefinitionUID: saveCmdOne.DefinitionUID,
State: InstanceStateNormal,
Labels: InstanceLabels{"test": "testValue"},
}
err = store.saveAlertInstance(saveCmdTwo)
require.NoError(t, err)
listCommand := &listAlertInstancesQuery{
DefinitionOrgID: alertDefinition4.OrgID,
DefinitionUID: alertDefinition4.UID,
}
err = store.listAlertInstances(listCommand)
require.NoError(t, err)
require.Len(t, listCommand.Result, 1)
require.Equal(t, saveCmdTwo.DefinitionOrgID, listCommand.Result[0].DefinitionOrgID)
require.Equal(t, saveCmdTwo.DefinitionUID, listCommand.Result[0].DefinitionUID)
require.Equal(t, saveCmdTwo.Labels, listCommand.Result[0].Labels)
require.Equal(t, saveCmdTwo.State, listCommand.Result[0].CurrentState)
require.NotEmpty(t, listCommand.Result[0].DefinitionTitle)
require.Equal(t, alertDefinition4.Title, listCommand.Result[0].DefinitionTitle)
})
}

@ -1,21 +0,0 @@
package ngalert
import (
"github.com/grafana/grafana/pkg/models"
)
func (api *apiImpl) validateOrgAlertDefinition(c *models.ReqContext) {
uid := c.ParamsEscape(":alertDefinitionUID")
if uid == "" {
c.JsonApiErr(403, "Permission denied", nil)
return
}
query := getAlertDefinitionByUIDQuery{UID: uid, OrgID: c.SignedInUser.OrgId}
if err := api.store.getAlertDefinitionByUID(&query); err != nil {
c.JsonApiErr(404, "Alert definition not found", nil)
return
}
}

@ -1,115 +0,0 @@
package ngalert
import (
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
)
var errAlertDefinitionFailedGenerateUniqueUID = errors.New("failed to generate alert definition UID")
// AlertDefinition is the model for alert definitions in Alerting NG.
type AlertDefinition struct {
ID int64 `xorm:"pk autoincr 'id'" json:"id"`
OrgID int64 `xorm:"org_id" json:"orgId"`
Title string `json:"title"`
Condition string `json:"condition"`
Data []eval.AlertQuery `json:"data"`
Updated time.Time `json:"updated"`
IntervalSeconds int64 `json:"intervalSeconds"`
Version int64 `json:"version"`
UID string `xorm:"uid" json:"uid"`
Paused bool `json:"paused"`
}
type alertDefinitionKey struct {
orgID int64
definitionUID string
}
func (k alertDefinitionKey) String() string {
return fmt.Sprintf("{orgID: %d, definitionUID: %s}", k.orgID, k.definitionUID)
}
func (alertDefinition *AlertDefinition) getKey() alertDefinitionKey {
return alertDefinitionKey{orgID: alertDefinition.OrgID, definitionUID: alertDefinition.UID}
}
// AlertDefinitionVersion is the model for alert definition versions in Alerting NG.
type AlertDefinitionVersion struct {
ID int64 `xorm:"pk autoincr 'id'"`
AlertDefinitionID int64 `xorm:"alert_definition_id"`
AlertDefinitionUID string `xorm:"alert_definition_uid"`
ParentVersion int64
RestoredFrom int64
Version int64
Created time.Time
Title string
Condition string
Data []eval.AlertQuery
IntervalSeconds int64
}
var (
// errAlertDefinitionNotFound is an error for an unknown alert definition.
errAlertDefinitionNotFound = fmt.Errorf("could not find alert definition")
)
// getAlertDefinitionByUIDQuery is the query for retrieving/deleting an alert definition by UID and organisation ID.
type getAlertDefinitionByUIDQuery struct {
UID string
OrgID int64
Result *AlertDefinition
}
type deleteAlertDefinitionByUIDCommand struct {
UID string
OrgID int64
}
// saveAlertDefinitionCommand is the query for saving a new alert definition.
type saveAlertDefinitionCommand struct {
Title string `json:"title"`
OrgID int64 `json:"-"`
Condition string `json:"condition"`
Data []eval.AlertQuery `json:"data"`
IntervalSeconds *int64 `json:"intervalSeconds"`
Result *AlertDefinition
}
// updateAlertDefinitionCommand is the query for updating an existing alert definition.
type updateAlertDefinitionCommand struct {
Title string `json:"title"`
OrgID int64 `json:"-"`
Condition string `json:"condition"`
Data []eval.AlertQuery `json:"data"`
IntervalSeconds *int64 `json:"intervalSeconds"`
UID string `json:"-"`
Result *AlertDefinition
}
type evalAlertConditionCommand struct {
Condition string `json:"condition"`
Data []eval.AlertQuery `json:"data"`
Now time.Time `json:"now"`
}
type listAlertDefinitionsQuery struct {
OrgID int64 `json:"-"`
Result []*AlertDefinition
}
type updateAlertDefinitionPausedCommand struct {
OrgID int64 `json:"-"`
UIDs []string `json:"uids"`
Paused bool `json:"-"`
ResultCount int64
}

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

@ -1,4 +1,4 @@
package eval
package models
import (
"encoding/json"
@ -186,7 +186,7 @@ func TestAlertQuery(t *testing.T) {
})
t.Run("can update model maxDataPoints (if missing)", func(t *testing.T) {
maxDataPoints, err := tc.alertQuery.getMaxDatapoints()
maxDataPoints, err := tc.alertQuery.GetMaxDatapoints()
require.NoError(t, err)
require.Equal(t, tc.expectedMaxPoints, maxDataPoints)
})
@ -198,7 +198,7 @@ func TestAlertQuery(t *testing.T) {
})
t.Run("can get the updated model with the default properties (if missing)", func(t *testing.T) {
blob, err := tc.alertQuery.getModel()
blob, err := tc.alertQuery.GetModel()
require.NoError(t, err)
model := make(map[string]interface{})
err = json.Unmarshal(blob, &model)

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

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

@ -0,0 +1,147 @@
package models
import (
"errors"
"fmt"
"time"
)
var (
// ErrAlertDefinitionNotFound is an error for an unknown alert definition.
ErrAlertDefinitionNotFound = fmt.Errorf("could not find alert definition")
// ErrAlertDefinitionFailedGenerateUniqueUID is an error for failure to generate alert definition UID
ErrAlertDefinitionFailedGenerateUniqueUID = errors.New("failed to generate alert definition UID")
)
// AlertDefinition is the model for alert definitions in Alerting NG.
type AlertDefinition struct {
ID int64 `xorm:"pk autoincr 'id'" json:"id"`
OrgID int64 `xorm:"org_id" json:"orgId"`
Title string `json:"title"`
Condition string `json:"condition"`
Data []AlertQuery `json:"data"`
Updated time.Time `json:"updated"`
IntervalSeconds int64 `json:"intervalSeconds"`
Version int64 `json:"version"`
UID string `xorm:"uid" json:"uid"`
Paused bool `json:"paused"`
}
// AlertDefinitionKey is the alert definition identifier
type AlertDefinitionKey struct {
OrgID int64
DefinitionUID string
}
func (k AlertDefinitionKey) String() string {
return fmt.Sprintf("{orgID: %d, definitionUID: %s}", k.OrgID, k.DefinitionUID)
}
// GetKey returns the alert definitions identifier
func (alertDefinition *AlertDefinition) GetKey() AlertDefinitionKey {
return AlertDefinitionKey{OrgID: alertDefinition.OrgID, DefinitionUID: alertDefinition.UID}
}
// PreSave sets datasource and loads the updated model for each alert query.
func (alertDefinition *AlertDefinition) PreSave(timeNow func() time.Time) error {
for i, q := range alertDefinition.Data {
err := q.PreSave()
if err != nil {
return fmt.Errorf("invalid alert query %s: %w", q.RefID, err)
}
alertDefinition.Data[i] = q
}
alertDefinition.Updated = timeNow()
return nil
}
// AlertDefinitionVersion is the model for alert definition versions in Alerting NG.
type AlertDefinitionVersion struct {
ID int64 `xorm:"pk autoincr 'id'"`
AlertDefinitionID int64 `xorm:"alert_definition_id"`
AlertDefinitionUID string `xorm:"alert_definition_uid"`
ParentVersion int64
RestoredFrom int64
Version int64
Created time.Time
Title string
Condition string
Data []AlertQuery
IntervalSeconds int64
}
// GetAlertDefinitionByUIDQuery is the query for retrieving/deleting an alert definition by UID and organisation ID.
type GetAlertDefinitionByUIDQuery struct {
UID string
OrgID int64
Result *AlertDefinition
}
// DeleteAlertDefinitionByUIDCommand is the command for deleting an alert definition
type DeleteAlertDefinitionByUIDCommand struct {
UID string
OrgID int64
}
// SaveAlertDefinitionCommand is the query for saving a new alert definition.
type SaveAlertDefinitionCommand struct {
Title string `json:"title"`
OrgID int64 `json:"-"`
Condition string `json:"condition"`
Data []AlertQuery `json:"data"`
IntervalSeconds *int64 `json:"intervalSeconds"`
Result *AlertDefinition
}
// UpdateAlertDefinitionCommand is the query for updating an existing alert definition.
type UpdateAlertDefinitionCommand struct {
Title string `json:"title"`
OrgID int64 `json:"-"`
Condition string `json:"condition"`
Data []AlertQuery `json:"data"`
IntervalSeconds *int64 `json:"intervalSeconds"`
UID string `json:"-"`
Result *AlertDefinition
}
// EvalAlertConditionCommand is the command for evaluating a condition
type EvalAlertConditionCommand struct {
Condition string `json:"condition"`
Data []AlertQuery `json:"data"`
Now time.Time `json:"now"`
}
// ListAlertDefinitionsQuery is the query for listing alert definitions
type ListAlertDefinitionsQuery struct {
OrgID int64 `json:"-"`
Result []*AlertDefinition
}
// UpdateAlertDefinitionPausedCommand is the command for updating an alert definitions
type UpdateAlertDefinitionPausedCommand struct {
OrgID int64 `json:"-"`
UIDs []string `json:"uids"`
Paused bool `json:"-"`
ResultCount int64
}
// Condition contains backend expressions and queries and the RefID
// of the query or expression that will be evaluated.
type Condition struct {
RefID string `json:"refId"`
OrgID int64 `json:"-"`
QueriesAndExpressions []AlertQuery `json:"queriesAndExpressions"`
}
// IsValid checks the condition's validity.
func (c Condition) IsValid() bool {
// TODO search for refIDs in QueriesAndExpressions
return len(c.QueriesAndExpressions) != 0
}

@ -4,6 +4,11 @@ import (
"context"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/api"
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/benbjohnson/clock"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -36,8 +41,8 @@ type AlertNG struct {
RouteRegister routing.RouteRegister `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
DataService *tsdb.Service `inject:""`
log log.Logger
schedule scheduleService
Log log.Logger
schedule schedule.ScheduleService
}
func init() {
@ -46,37 +51,37 @@ func init() {
// Init initializes the AlertingService.
func (ng *AlertNG) Init() error {
ng.log = log.New("ngalert")
ng.Log = log.New("ngalert")
baseInterval := baseIntervalSeconds * time.Second
store := storeImpl{baseInterval: baseInterval, SQLStore: ng.SQLStore}
store := store.DBstore{BaseInterval: baseInterval, DefaultIntervalSeconds: defaultIntervalSeconds, SQLStore: ng.SQLStore}
schedCfg := schedulerCfg{
c: clock.New(),
baseInterval: baseInterval,
logger: ng.log,
evaluator: eval.Evaluator{Cfg: ng.Cfg},
store: store,
schedCfg := schedule.SchedulerCfg{
C: clock.New(),
BaseInterval: baseInterval,
Logger: ng.Log,
MaxAttempts: maxAttempts,
Evaluator: eval.Evaluator{Cfg: ng.Cfg},
Store: store,
}
ng.schedule = newScheduler(schedCfg, ng.DataService)
ng.schedule = schedule.NewScheduler(schedCfg, ng.DataService)
api := apiImpl{
api := api.API{
Cfg: ng.Cfg,
DatasourceCache: ng.DatasourceCache,
RouteRegister: ng.RouteRegister,
DataService: ng.DataService,
schedule: ng.schedule,
store: store,
}
api.registerAPIEndpoints()
Schedule: ng.schedule,
Store: store}
api.RegisterAPIEndpoints()
return nil
}
// Run starts the scheduler
func (ng *AlertNG) Run(ctx context.Context) error {
ng.log.Debug("ngalert starting")
ng.Log.Debug("ngalert starting")
return ng.schedule.Ticker(ctx)
}
@ -100,23 +105,3 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) {
// Create alert_instance table
alertInstanceMigration(mg)
}
// LoadAlertCondition returns a Condition object for the given alertDefinitionID.
func (api *apiImpl) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*eval.Condition, error) {
q := getAlertDefinitionByUIDQuery{UID: alertDefinitionUID, OrgID: orgID}
if err := api.store.getAlertDefinitionByUID(&q); err != nil {
return nil, err
}
alertDefinition := q.Result
err := api.store.validateAlertDefinition(alertDefinition, true)
if err != nil {
return nil, err
}
return &eval.Condition{
RefID: alertDefinition.Condition,
OrgID: alertDefinition.OrgID,
QueriesAndExpressions: alertDefinition.Data,
}, nil
}

@ -0,0 +1,17 @@
package schedule
import (
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func (sch *schedule) fetchAllDetails(now time.Time) []*models.AlertDefinition {
q := models.ListAlertDefinitionsQuery{}
err := sch.store.GetAlertDefinitions(&q)
if err != nil {
sch.log.Error("failed to fetch alert definitions", "now", now, "err", err)
return nil
}
return q.Result
}

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

@ -1,4 +1,4 @@
package ngalert
package store
import (
"context"
@ -7,36 +7,51 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
type store interface {
deleteAlertDefinitionByUID(*deleteAlertDefinitionByUIDCommand) error
getAlertDefinitionByUID(*getAlertDefinitionByUIDQuery) error
getAlertDefinitions(query *listAlertDefinitionsQuery) error
getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error
saveAlertDefinition(*saveAlertDefinitionCommand) error
updateAlertDefinition(*updateAlertDefinitionCommand) error
getAlertInstance(*getAlertInstanceQuery) error
listAlertInstances(cmd *listAlertInstancesQuery) error
saveAlertInstance(cmd *saveAlertInstanceCommand) error
validateAlertDefinition(*AlertDefinition, bool) error
updateAlertDefinitionPaused(*updateAlertDefinitionPausedCommand) error
// TimeNow makes it possible to test usage of time
var TimeNow = time.Now
// AlertDefinitionMaxTitleLength is the maximum length of the alert definition titles
const AlertDefinitionMaxTitleLength = 190
// ErrEmptyTitleError is an error returned if the alert definition title is empty
var ErrEmptyTitleError = errors.New("title is empty")
// Store is the interface for persisting alert definitions and instances
type Store interface {
DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error
GetAlertDefinitionByUID(*models.GetAlertDefinitionByUIDQuery) error
GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error
GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error
SaveAlertDefinition(*models.SaveAlertDefinitionCommand) error
UpdateAlertDefinition(*models.UpdateAlertDefinitionCommand) error
GetAlertInstance(*models.GetAlertInstanceQuery) error
ListAlertInstances(cmd *models.ListAlertInstancesQuery) error
SaveAlertInstance(cmd *models.SaveAlertInstanceCommand) error
ValidateAlertDefinition(*models.AlertDefinition, bool) error
UpdateAlertDefinitionPaused(*models.UpdateAlertDefinitionPausedCommand) error
}
type storeImpl struct {
// DBstore stores the alert definitions and instances in the database.
type DBstore struct {
// the base scheduler tick rate; it's used for validating definition interval
baseInterval time.Duration
SQLStore *sqlstore.SQLStore `inject:""`
BaseInterval time.Duration
// default alert definiiton interval
DefaultIntervalSeconds int64
SQLStore *sqlstore.SQLStore `inject:""`
}
func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*AlertDefinition, error) {
func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*models.AlertDefinition, error) {
// we consider optionally enabling some caching
alertDefinition := AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
alertDefinition := models.AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
has, err := sess.Get(&alertDefinition)
if !has {
return nil, errAlertDefinitionNotFound
return nil, models.ErrAlertDefinitionNotFound
}
if err != nil {
return nil, err
@ -44,9 +59,9 @@ func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string
return &alertDefinition, nil
}
// deleteAlertDefinitionByID is a handler for deleting an alert definition.
// DeleteAlertDefinitionByUID is a handler for deleting an alert definition.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (st storeImpl) deleteAlertDefinitionByUID(cmd *deleteAlertDefinitionByUIDCommand) error {
func (st DBstore) DeleteAlertDefinitionByUID(cmd *models.DeleteAlertDefinitionByUIDCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM alert_definition WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID)
if err != nil {
@ -66,9 +81,9 @@ func (st storeImpl) deleteAlertDefinitionByUID(cmd *deleteAlertDefinitionByUIDCo
})
}
// getAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID.
// GetAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (st storeImpl) getAlertDefinitionByUID(query *getAlertDefinitionByUIDQuery) error {
func (st DBstore) GetAlertDefinitionByUID(query *models.GetAlertDefinitionByUIDQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID)
if err != nil {
@ -79,10 +94,10 @@ func (st storeImpl) getAlertDefinitionByUID(query *getAlertDefinitionByUIDQuery)
})
}
// saveAlertDefinition is a handler for saving a new alert definition.
func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
// SaveAlertDefinition is a handler for saving a new alert definition.
func (st DBstore) SaveAlertDefinition(cmd *models.SaveAlertDefinitionCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
intervalSeconds := defaultIntervalSeconds
intervalSeconds := st.DefaultIntervalSeconds
if cmd.IntervalSeconds != nil {
intervalSeconds = *cmd.IntervalSeconds
}
@ -94,7 +109,7 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
return fmt.Errorf("failed to generate UID for alert definition %q: %w", cmd.Title, err)
}
alertDefinition := &AlertDefinition{
alertDefinition := &models.AlertDefinition{
OrgID: cmd.OrgID,
Title: cmd.Title,
Condition: cmd.Condition,
@ -104,11 +119,11 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
UID: uid,
}
if err := st.validateAlertDefinition(alertDefinition, false); err != nil {
if err := st.ValidateAlertDefinition(alertDefinition, false); err != nil {
return err
}
if err := alertDefinition.preSave(); err != nil {
if err := alertDefinition.PreSave(TimeNow); err != nil {
return err
}
@ -119,7 +134,7 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
return err
}
alertDefVersion := AlertDefinitionVersion{
alertDefVersion := models.AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: alertDefinition.UID,
Version: alertDefinition.Version,
@ -138,13 +153,13 @@ func (st storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
})
}
// updateAlertDefinition is a handler for updating an existing alert definition.
// UpdateAlertDefinition is a handler for updating an existing alert definition.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) error {
func (st DBstore) UpdateAlertDefinition(cmd *models.UpdateAlertDefinitionCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID)
if err != nil {
if errors.Is(err, errAlertDefinitionNotFound) {
if errors.Is(err, models.ErrAlertDefinitionNotFound) {
return nil
}
return err
@ -168,7 +183,7 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
}
// explicitly set all fields regardless of being provided or not
alertDefinition := &AlertDefinition{
alertDefinition := &models.AlertDefinition{
ID: existingAlertDefinition.ID,
Title: title,
Condition: condition,
@ -178,11 +193,11 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
UID: existingAlertDefinition.UID,
}
if err := st.validateAlertDefinition(alertDefinition, true); err != nil {
if err := st.ValidateAlertDefinition(alertDefinition, true); err != nil {
return err
}
if err := alertDefinition.preSave(); err != nil {
if err := alertDefinition.PreSave(TimeNow); err != nil {
return err
}
@ -196,7 +211,7 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
return err
}
alertDefVersion := AlertDefinitionVersion{
alertDefVersion := models.AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: alertDefinition.UID,
ParentVersion: alertDefinition.Version,
@ -216,10 +231,10 @@ func (st storeImpl) updateAlertDefinition(cmd *updateAlertDefinitionCommand) err
})
}
// getOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
func (st storeImpl) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error {
// GetOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
func (st DBstore) GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinitions := make([]*AlertDefinition, 0)
alertDefinitions := make([]*models.AlertDefinition, 0)
q := "SELECT * FROM alert_definition WHERE org_id = ?"
if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil {
return err
@ -230,9 +245,11 @@ func (st storeImpl) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) err
})
}
func (st storeImpl) getAlertDefinitions(query *listAlertDefinitionsQuery) error {
// GetAlertDefinitions returns alert definition identifier, interval, version and pause state
// that are useful for it's scheduling.
func (st DBstore) GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alerts := make([]*AlertDefinition, 0)
alerts := make([]*models.AlertDefinition, 0)
q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_definition"
if err := sess.SQL(q).Find(&alerts); err != nil {
return err
@ -243,7 +260,8 @@ func (st storeImpl) getAlertDefinitions(query *listAlertDefinitionsQuery) error
})
}
func (st storeImpl) updateAlertDefinitionPaused(cmd *updateAlertDefinitionPausedCommand) error {
// UpdateAlertDefinitionPaused update the pause state of an alert definition.
func (st DBstore) UpdateAlertDefinitionPaused(cmd *models.UpdateAlertDefinitionPausedCommand) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if len(cmd.UIDs) == 0 {
return nil
@ -282,7 +300,7 @@ func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (strin
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&AlertDefinition{})
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&models.AlertDefinition{})
if err != nil {
return "", err
}
@ -292,5 +310,32 @@ func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (strin
}
}
return "", errAlertDefinitionFailedGenerateUniqueUID
return "", models.ErrAlertDefinitionFailedGenerateUniqueUID
}
// ValidateAlertDefinition validates the alert definition interval and organisation.
// If requireData is true checks that it contains at least one alert query
func (st DBstore) ValidateAlertDefinition(alertDefinition *models.AlertDefinition, requireData bool) error {
if !requireData && len(alertDefinition.Data) == 0 {
return fmt.Errorf("no queries or expressions are found")
}
if alertDefinition.Title == "" {
return ErrEmptyTitleError
}
if alertDefinition.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 {
return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertDefinition.IntervalSeconds)*time.Second, st.BaseInterval)
}
// enfore max name length in SQLite
if len(alertDefinition.Title) > AlertDefinitionMaxTitleLength {
return fmt.Errorf("name length should not be greater than %d", AlertDefinitionMaxTitleLength)
}
if alertDefinition.OrgID == 0 {
return fmt.Errorf("no organisation is found")
}
return nil
}

@ -1,20 +1,20 @@
package ngalert
package store
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
// getAlertInstance is a handler for retrieving an alert instance based on OrgId, AlertDefintionID, and
// GetAlertInstance is a handler for retrieving an alert instance based on OrgId, AlertDefintionID, and
// the hash of the labels.
// nolint:unused
func (st storeImpl) getAlertInstance(cmd *getAlertInstanceQuery) error {
func (st DBstore) GetAlertInstance(cmd *models.GetAlertInstanceQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
instance := AlertInstance{}
instance := models.AlertInstance{}
s := strings.Builder{}
s.WriteString(`SELECT * FROM alert_instance
WHERE
@ -43,11 +43,11 @@ func (st storeImpl) getAlertInstance(cmd *getAlertInstanceQuery) error {
})
}
// listAlertInstances is a handler for retrieving alert instances within specific organisation
// ListAlertInstances is a handler for retrieving alert instances within specific organisation
// based on various filters.
func (st storeImpl) listAlertInstances(cmd *listAlertInstancesQuery) error {
func (st DBstore) ListAlertInstances(cmd *models.ListAlertInstancesQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertInstances := make([]*listAlertInstancesQueryResult, 0)
alertInstances := make([]*models.ListAlertInstancesQueryResult, 0)
s := strings.Builder{}
params := make([]interface{}, 0)
@ -76,26 +76,26 @@ func (st storeImpl) listAlertInstances(cmd *listAlertInstancesQuery) error {
})
}
// saveAlertDefinition is a handler for saving a new alert definition.
// SaveAlertInstance is a handler for saving a new alert instance.
// nolint:unused
func (st storeImpl) saveAlertInstance(cmd *saveAlertInstanceCommand) error {
func (st DBstore) SaveAlertInstance(cmd *models.SaveAlertInstanceCommand) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
labelTupleJSON, labelsHash, err := cmd.Labels.StringAndHash()
if err != nil {
return err
}
alertInstance := &AlertInstance{
alertInstance := &models.AlertInstance{
DefinitionOrgID: cmd.DefinitionOrgID,
DefinitionUID: cmd.DefinitionUID,
Labels: cmd.Labels,
LabelsHash: labelsHash,
CurrentState: cmd.State,
CurrentStateSince: time.Now(),
CurrentStateSince: TimeNow(),
LastEvalTime: cmd.LastEvalTime,
}
if err := validateAlertInstance(alertInstance); err != nil {
if err := models.ValidateAlertInstance(alertInstance); err != nil {
return err
}

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

@ -0,0 +1,165 @@
// +build integration
package tests
import (
"testing"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/stretchr/testify/require"
)
func TestAlertInstanceOperations(t *testing.T) {
dbstore := setupTestEnv(t, baseIntervalSeconds)
alertDefinition1 := createTestAlertDefinition(t, dbstore, 60)
orgID := alertDefinition1.OrgID
alertDefinition2 := createTestAlertDefinition(t, dbstore, 60)
require.Equal(t, orgID, alertDefinition2.OrgID)
alertDefinition3 := createTestAlertDefinition(t, dbstore, 60)
require.Equal(t, orgID, alertDefinition3.OrgID)
alertDefinition4 := createTestAlertDefinition(t, dbstore, 60)
require.Equal(t, orgID, alertDefinition4.OrgID)
t.Run("can save and read new alert instance", func(t *testing.T) {
saveCmd := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition1.OrgID,
DefinitionUID: alertDefinition1.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
err := dbstore.SaveAlertInstance(saveCmd)
require.NoError(t, err)
getCmd := &models.GetAlertInstanceQuery{
DefinitionOrgID: saveCmd.DefinitionOrgID,
DefinitionUID: saveCmd.DefinitionUID,
Labels: models.InstanceLabels{"test": "testValue"},
}
err = dbstore.GetAlertInstance(getCmd)
require.NoError(t, err)
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
require.Equal(t, alertDefinition1.OrgID, getCmd.Result.DefinitionOrgID)
require.Equal(t, alertDefinition1.UID, getCmd.Result.DefinitionUID)
})
t.Run("can save and read new alert instance with no labels", func(t *testing.T) {
saveCmd := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition2.OrgID,
DefinitionUID: alertDefinition2.UID,
State: models.InstanceStateNormal,
}
err := dbstore.SaveAlertInstance(saveCmd)
require.NoError(t, err)
getCmd := &models.GetAlertInstanceQuery{
DefinitionOrgID: saveCmd.DefinitionOrgID,
DefinitionUID: saveCmd.DefinitionUID,
}
err = dbstore.GetAlertInstance(getCmd)
require.NoError(t, err)
require.Equal(t, alertDefinition2.OrgID, getCmd.Result.DefinitionOrgID)
require.Equal(t, alertDefinition2.UID, getCmd.Result.DefinitionUID)
require.Equal(t, saveCmd.Labels, getCmd.Result.Labels)
})
t.Run("can save two instances with same org_id, uid and different labels", func(t *testing.T) {
saveCmdOne := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition3.OrgID,
DefinitionUID: alertDefinition3.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
err := dbstore.SaveAlertInstance(saveCmdOne)
require.NoError(t, err)
saveCmdTwo := &models.SaveAlertInstanceCommand{
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
DefinitionUID: saveCmdOne.DefinitionUID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "meow"},
}
err = dbstore.SaveAlertInstance(saveCmdTwo)
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
DefinitionUID: saveCmdOne.DefinitionUID,
}
err = dbstore.ListAlertInstances(listQuery)
require.NoError(t, err)
require.Len(t, listQuery.Result, 2)
})
t.Run("can list all added instances in org", func(t *testing.T) {
listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: orgID,
}
err := dbstore.ListAlertInstances(listQuery)
require.NoError(t, err)
require.Len(t, listQuery.Result, 4)
})
t.Run("can list all added instances in org filtered by current state", func(t *testing.T) {
listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: orgID,
State: models.InstanceStateNormal,
}
err := dbstore.ListAlertInstances(listQuery)
require.NoError(t, err)
require.Len(t, listQuery.Result, 1)
})
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
saveCmdOne := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition4.OrgID,
DefinitionUID: alertDefinition4.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
err := dbstore.SaveAlertInstance(saveCmdOne)
require.NoError(t, err)
saveCmdTwo := &models.SaveAlertInstanceCommand{
DefinitionOrgID: saveCmdOne.DefinitionOrgID,
DefinitionUID: saveCmdOne.DefinitionUID,
State: models.InstanceStateNormal,
Labels: models.InstanceLabels{"test": "testValue"},
}
err = dbstore.SaveAlertInstance(saveCmdTwo)
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: alertDefinition4.OrgID,
DefinitionUID: alertDefinition4.UID,
}
err = dbstore.ListAlertInstances(listQuery)
require.NoError(t, err)
require.Len(t, listQuery.Result, 1)
require.Equal(t, saveCmdTwo.DefinitionOrgID, listQuery.Result[0].DefinitionOrgID)
require.Equal(t, saveCmdTwo.DefinitionUID, listQuery.Result[0].DefinitionUID)
require.Equal(t, saveCmdTwo.Labels, listQuery.Result[0].Labels)
require.Equal(t, saveCmdTwo.State, listQuery.Result[0].CurrentState)
require.NotEmpty(t, listQuery.Result[0].DefinitionTitle)
require.Equal(t, alertDefinition4.Title, listQuery.Result[0].DefinitionTitle)
})
}

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

@ -1,4 +1,4 @@
package ngalert
package tests
import (
"encoding/json"
@ -7,18 +7,25 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl) {
// setupTestEnv initializes a store to used by the tests.
func setupTestEnv(t *testing.T, baseIntervalSeconds int64) *store.DBstore {
cfg := setting.NewCfg()
// AlertNG is disabled by default and only if it's enabled
// its database migrations run and the relative database tables are created
cfg.FeatureToggles = map[string]bool{"ngalert": true}
ng := overrideAlertNGInRegistry(t, cfg)
@ -26,18 +33,20 @@ func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl)
err := ng.Init()
require.NoError(t, err)
return ng, &storeImpl{SQLStore: ng.SQLStore, baseInterval: time.Duration(baseIntervalSeconds) * time.Second}
return &store.DBstore{SQLStore: ng.SQLStore, BaseInterval: time.Duration(baseIntervalSeconds) * time.Second}
}
func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG {
ng := AlertNG{
func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) ngalert.AlertNG {
ng := ngalert.AlertNG{
Cfg: cfg,
RouteRegister: routing.NewRouteRegister(),
log: log.New("ngalert-test"),
Log: log.New("ngalert-test"),
}
// hook for initialising the service after the Cfg is populated
// so that database migrations will run
overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) {
if _, ok := descriptor.Instance.(*AlertNG); ok {
if _, ok := descriptor.Instance.(*ngalert.AlertNG); ok {
return &registry.Descriptor{
Name: descriptor.Name,
Instance: &ng,
@ -52,29 +61,30 @@ func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG {
return ng
}
func createTestAlertDefinition(t *testing.T, store *storeImpl, intervalSeconds int64) *AlertDefinition {
cmd := saveAlertDefinitionCommand{
// createTestAlertDefinition creates a dummy alert definition to be used by the tests.
func createTestAlertDefinition(t *testing.T, store *store.DBstore, intervalSeconds int64) *models.AlertDefinition {
cmd := models.SaveAlertDefinitionCommand{
OrgID: 1,
Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)),
Condition: "A",
Data: []eval.AlertQuery{
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasource": "__expr__",
"type":"math",
"expression":"2 + 2 > 1"
}`),
RelativeTimeRange: eval.RelativeTimeRange{
From: eval.Duration(5 * time.Hour),
To: eval.Duration(3 * time.Hour),
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(5 * time.Hour),
To: models.Duration(3 * time.Hour),
},
RefID: "A",
},
},
IntervalSeconds: &intervalSeconds,
}
err := store.saveAlertDefinition(&cmd)
err := store.SaveAlertDefinition(&cmd)
require.NoError(t, err)
t.Logf("alert definition: %v with interval: %d created", cmd.Result.getKey(), intervalSeconds)
t.Logf("alert definition: %v with interval: %d created", cmd.Result.GetKey(), intervalSeconds)
return cmd.Result
}

@ -1,79 +0,0 @@
package ngalert
import (
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
)
const alertDefinitionMaxTitleLength = 190
var errEmptyTitleError = errors.New("title is empty")
// validateAlertDefinition validates the alert definition interval and organisation.
// If requireData is true checks that it contains at least one alert query
func (st storeImpl) validateAlertDefinition(alertDefinition *AlertDefinition, requireData bool) error {
if !requireData && len(alertDefinition.Data) == 0 {
return fmt.Errorf("no queries or expressions are found")
}
if alertDefinition.Title == "" {
return errEmptyTitleError
}
if alertDefinition.IntervalSeconds%int64(st.baseInterval.Seconds()) != 0 {
return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertDefinition.IntervalSeconds)*time.Second, st.baseInterval)
}
// enfore max name length in SQLite
if len(alertDefinition.Title) > alertDefinitionMaxTitleLength {
return fmt.Errorf("name length should not be greater than %d", alertDefinitionMaxTitleLength)
}
if alertDefinition.OrgID == 0 {
return fmt.Errorf("no organisation is found")
}
return nil
}
// validateCondition validates that condition queries refer to existing datasources
func (api *apiImpl) validateCondition(c eval.Condition, user *models.SignedInUser, skipCache bool) error {
var refID string
if len(c.QueriesAndExpressions) == 0 {
return nil
}
for _, query := range c.QueriesAndExpressions {
if c.RefID == query.RefID {
refID = c.RefID
}
datasourceUID, err := query.GetDatasource()
if err != nil {
return err
}
isExpression, err := query.IsExpression()
if err != nil {
return err
}
if isExpression {
continue
}
_, err = api.DatasourceCache.GetDatasourceByUID(datasourceUID, user, skipCache)
if err != nil {
return fmt.Errorf("failed to get datasource: %s: %w", datasourceUID, err)
}
}
if refID == "" {
return fmt.Errorf("condition %s not found in any query or expression", c.RefID)
}
return nil
}
Loading…
Cancel
Save