From 4ce0a49eac03e8f37914a84c8bfaeac573582e93 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Mon, 8 Mar 2021 22:19:21 +0200 Subject: [PATCH] AlertingNG: Split into several packages (#31719) * AlertingNG: Split into several packages * Move AlertQuery to models --- go.mod | 1 + go.sum | 4 + pkg/services/ngalert/alert_definition.go | 22 --- pkg/services/ngalert/{ => api}/api.go | 140 +++++++++++---- pkg/services/ngalert/api/instance_api.go | 19 ++ pkg/services/ngalert/api/middleware.go | 23 +++ pkg/services/ngalert/eval/eval.go | 12 +- pkg/services/ngalert/fetcher.go | 15 -- pkg/services/ngalert/instance_api.go | 17 -- .../ngalert/instance_database_test.go | 163 ----------------- pkg/services/ngalert/middleware.go | 21 --- pkg/services/ngalert/models.go | 115 ------------ .../ngalert/{eval => models}/alert_query.go | 12 +- .../{eval => models}/alert_query_test.go | 6 +- pkg/services/ngalert/{ => models}/instance.go | 24 +-- .../ngalert/{ => models}/instance_labels.go | 2 +- pkg/services/ngalert/models/models.go | 147 ++++++++++++++++ pkg/services/ngalert/ngalert.go | 59 +++---- pkg/services/ngalert/schedule/fetcher.go | 17 ++ .../ngalert/{ => schedule}/schedule.go | 116 ++++++------ pkg/services/ngalert/{ => store}/database.go | 135 +++++++++----- .../ngalert/{ => store}/instance_database.go | 26 +-- .../ngalert/{ => tests}/database_test.go | 138 ++++++++------- .../ngalert/tests/instance_database_test.go | 165 ++++++++++++++++++ .../ngalert/{ => tests}/schedule_test.go | 85 ++++----- .../ngalert/{common_test.go => tests/util.go} | 42 +++-- pkg/services/ngalert/validator.go | 79 --------- 27 files changed, 840 insertions(+), 765 deletions(-) delete mode 100644 pkg/services/ngalert/alert_definition.go rename pkg/services/ngalert/{ => api}/api.go (61%) create mode 100644 pkg/services/ngalert/api/instance_api.go create mode 100644 pkg/services/ngalert/api/middleware.go delete mode 100644 pkg/services/ngalert/fetcher.go delete mode 100644 pkg/services/ngalert/instance_api.go delete mode 100644 pkg/services/ngalert/instance_database_test.go delete mode 100644 pkg/services/ngalert/middleware.go delete mode 100644 pkg/services/ngalert/models.go rename pkg/services/ngalert/{eval => models}/alert_query.go (95%) rename pkg/services/ngalert/{eval => models}/alert_query_test.go (98%) rename pkg/services/ngalert/{ => models}/instance.go (78%) rename pkg/services/ngalert/{ => models}/instance_labels.go (99%) create mode 100644 pkg/services/ngalert/models/models.go create mode 100644 pkg/services/ngalert/schedule/fetcher.go rename pkg/services/ngalert/{ => schedule}/schedule.go (73%) rename pkg/services/ngalert/{ => store}/database.go (60%) rename pkg/services/ngalert/{ => store}/instance_database.go (79%) rename pkg/services/ngalert/{ => tests}/database_test.go (73%) create mode 100644 pkg/services/ngalert/tests/instance_database_test.go rename pkg/services/ngalert/{ => tests}/schedule_test.go (64%) rename pkg/services/ngalert/{common_test.go => tests/util.go} (51%) delete mode 100644 pkg/services/ngalert/validator.go diff --git a/go.mod b/go.mod index 37097800a8b..edbbcacd894 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/prometheus/client_golang v1.9.0 github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.18.0 + github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 // indirect github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/robfig/cron/v3 v3.0.1 github.com/russellhaering/goxmldsig v1.1.0 diff --git a/go.sum b/go.sum index 4f903d0d83c..a45f0c0056e 100644 --- a/go.sum +++ b/go.sum @@ -1303,6 +1303,10 @@ github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4 h1:54z99l8 github.com/prometheus/prometheus v1.8.2-0.20201105135750-00f16d1ac3a4/go.mod h1:XYjkJiog7fyQu3puQNivZPI2pNq1C/775EIoHfDvuvY= github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quasilyte/go-ruleguard v0.3.1 h1:2KTXnHBCR4BUl8UAL2bCUorOBGC8RsmYncuDA9NEFW4= +github.com/quasilyte/go-ruleguard/dsl v0.3.1 h1:CHGOKP2LDz35P49TjW4Bx4BCfFI6ZZU/8zcneECD0q4= +github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY= +github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM= github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= diff --git a/pkg/services/ngalert/alert_definition.go b/pkg/services/ngalert/alert_definition.go deleted file mode 100644 index 20cd73b2986..00000000000 --- a/pkg/services/ngalert/alert_definition.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/ngalert/api.go b/pkg/services/ngalert/api/api.go similarity index 61% rename from pkg/services/ngalert/api.go rename to pkg/services/ngalert/api/api.go index 101eceb77f5..dc71170843e 100644 --- a/pkg/services/ngalert/api.go +++ b/pkg/services/ngalert/api/api.go @@ -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 +} diff --git a/pkg/services/ngalert/api/instance_api.go b/pkg/services/ngalert/api/instance_api.go new file mode 100644 index 00000000000..2a9966ba83b --- /dev/null +++ b/pkg/services/ngalert/api/instance_api.go @@ -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) +} diff --git a/pkg/services/ngalert/api/middleware.go b/pkg/services/ngalert/api/middleware.go new file mode 100644 index 00000000000..a31e55bbb17 --- /dev/null +++ b/pkg/services/ngalert/api/middleware.go @@ -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 + } +} diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go index 7d0d551430c..72d2b46b022 100644 --- a/pkg/services/ngalert/eval/eval.go +++ b/pkg/services/ngalert/eval/eval.go @@ -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), }) } diff --git a/pkg/services/ngalert/fetcher.go b/pkg/services/ngalert/fetcher.go deleted file mode 100644 index 64f0222ce44..00000000000 --- a/pkg/services/ngalert/fetcher.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/ngalert/instance_api.go b/pkg/services/ngalert/instance_api.go deleted file mode 100644 index 6551852d8b0..00000000000 --- a/pkg/services/ngalert/instance_api.go +++ /dev/null @@ -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) -} diff --git a/pkg/services/ngalert/instance_database_test.go b/pkg/services/ngalert/instance_database_test.go deleted file mode 100644 index 24ac2c39f7f..00000000000 --- a/pkg/services/ngalert/instance_database_test.go +++ /dev/null @@ -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) - }) -} diff --git a/pkg/services/ngalert/middleware.go b/pkg/services/ngalert/middleware.go deleted file mode 100644 index c28b5c8739e..00000000000 --- a/pkg/services/ngalert/middleware.go +++ /dev/null @@ -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 - } -} diff --git a/pkg/services/ngalert/models.go b/pkg/services/ngalert/models.go deleted file mode 100644 index da36f303abb..00000000000 --- a/pkg/services/ngalert/models.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/ngalert/eval/alert_query.go b/pkg/services/ngalert/models/alert_query.go similarity index 95% rename from pkg/services/ngalert/eval/alert_query.go rename to pkg/services/ngalert/models/alert_query.go index fb829952e66..712528f5625 100644 --- a/pkg/services/ngalert/eval/alert_query.go +++ b/pkg/services/ngalert/models/alert_query.go @@ -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 } diff --git a/pkg/services/ngalert/eval/alert_query_test.go b/pkg/services/ngalert/models/alert_query_test.go similarity index 98% rename from pkg/services/ngalert/eval/alert_query_test.go rename to pkg/services/ngalert/models/alert_query_test.go index 12054b3b03f..f3064eebc81 100644 --- a/pkg/services/ngalert/eval/alert_query_test.go +++ b/pkg/services/ngalert/models/alert_query_test.go @@ -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) diff --git a/pkg/services/ngalert/instance.go b/pkg/services/ngalert/models/instance.go similarity index 78% rename from pkg/services/ngalert/instance.go rename to pkg/services/ngalert/models/instance.go index 6c5f6380be7..b8d7b47bd2c 100644 --- a/pkg/services/ngalert/instance.go +++ b/pkg/services/ngalert/models/instance.go @@ -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") } diff --git a/pkg/services/ngalert/instance_labels.go b/pkg/services/ngalert/models/instance_labels.go similarity index 99% rename from pkg/services/ngalert/instance_labels.go rename to pkg/services/ngalert/models/instance_labels.go index 93a2d8f1d5f..a1e6852789b 100644 --- a/pkg/services/ngalert/instance_labels.go +++ b/pkg/services/ngalert/models/instance_labels.go @@ -1,4 +1,4 @@ -package ngalert +package models import ( // nolint:gosec diff --git a/pkg/services/ngalert/models/models.go b/pkg/services/ngalert/models/models.go new file mode 100644 index 00000000000..a5485aa5392 --- /dev/null +++ b/pkg/services/ngalert/models/models.go @@ -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 +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 03552ceb145..2aae9542c57 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -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 -} diff --git a/pkg/services/ngalert/schedule/fetcher.go b/pkg/services/ngalert/schedule/fetcher.go new file mode 100644 index 00000000000..19ff25b9393 --- /dev/null +++ b/pkg/services/ngalert/schedule/fetcher.go @@ -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 +} diff --git a/pkg/services/ngalert/schedule.go b/pkg/services/ngalert/schedule/schedule.go similarity index 73% rename from pkg/services/ngalert/schedule.go rename to pkg/services/ngalert/schedule/schedule.go index 05aa17f6a3f..bc4624aedfd 100644 --- a/pkg/services/ngalert/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -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{}{} } diff --git a/pkg/services/ngalert/database.go b/pkg/services/ngalert/store/database.go similarity index 60% rename from pkg/services/ngalert/database.go rename to pkg/services/ngalert/store/database.go index 4956fe23a4f..a5783acdb7e 100644 --- a/pkg/services/ngalert/database.go +++ b/pkg/services/ngalert/store/database.go @@ -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 } diff --git a/pkg/services/ngalert/instance_database.go b/pkg/services/ngalert/store/instance_database.go similarity index 79% rename from pkg/services/ngalert/instance_database.go rename to pkg/services/ngalert/store/instance_database.go index 9adea88665d..7f54e38ff79 100644 --- a/pkg/services/ngalert/instance_database.go +++ b/pkg/services/ngalert/store/instance_database.go @@ -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 } diff --git a/pkg/services/ngalert/database_test.go b/pkg/services/ngalert/tests/database_test.go similarity index 73% rename from pkg/services/ngalert/database_test.go rename to pkg/services/ngalert/tests/database_test.go index e6a382c4bab..7bba9f61e86 100644 --- a/pkg/services/ngalert/database_test.go +++ b/pkg/services/ngalert/tests/database_test.go @@ -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) }) } diff --git a/pkg/services/ngalert/tests/instance_database_test.go b/pkg/services/ngalert/tests/instance_database_test.go new file mode 100644 index 00000000000..baa2cc11a45 --- /dev/null +++ b/pkg/services/ngalert/tests/instance_database_test.go @@ -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) + }) +} diff --git a/pkg/services/ngalert/schedule_test.go b/pkg/services/ngalert/tests/schedule_test.go similarity index 64% rename from pkg/services/ngalert/schedule_test.go rename to pkg/services/ngalert/tests/schedule_test.go index 3850b74dddc..9f165b389ae 100644 --- a/pkg/services/ngalert/schedule_test.go +++ b/pkg/services/ngalert/tests/schedule_test.go @@ -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()) diff --git a/pkg/services/ngalert/common_test.go b/pkg/services/ngalert/tests/util.go similarity index 51% rename from pkg/services/ngalert/common_test.go rename to pkg/services/ngalert/tests/util.go index d9e0e4bbbf5..bdd3141ef19 100644 --- a/pkg/services/ngalert/common_test.go +++ b/pkg/services/ngalert/tests/util.go @@ -1,4 +1,4 @@ -package ngalert +package tests import ( "encoding/json" @@ -7,18 +7,25 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/ngalert/models" + + "github.com/grafana/grafana/pkg/services/ngalert" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) -func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl) { +// setupTestEnv initializes a store to used by the tests. +func setupTestEnv(t *testing.T, baseIntervalSeconds int64) *store.DBstore { cfg := setting.NewCfg() + // AlertNG is disabled by default and only if it's enabled + // its database migrations run and the relative database tables are created cfg.FeatureToggles = map[string]bool{"ngalert": true} ng := overrideAlertNGInRegistry(t, cfg) @@ -26,18 +33,20 @@ func setupTestEnv(t *testing.T, baseIntervalSeconds int64) (AlertNG, *storeImpl) err := ng.Init() require.NoError(t, err) - return ng, &storeImpl{SQLStore: ng.SQLStore, baseInterval: time.Duration(baseIntervalSeconds) * time.Second} + return &store.DBstore{SQLStore: ng.SQLStore, BaseInterval: time.Duration(baseIntervalSeconds) * time.Second} } -func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG { - ng := AlertNG{ +func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) ngalert.AlertNG { + ng := ngalert.AlertNG{ Cfg: cfg, RouteRegister: routing.NewRouteRegister(), - log: log.New("ngalert-test"), + Log: log.New("ngalert-test"), } + // hook for initialising the service after the Cfg is populated + // so that database migrations will run overrideServiceFunc := func(descriptor registry.Descriptor) (*registry.Descriptor, bool) { - if _, ok := descriptor.Instance.(*AlertNG); ok { + if _, ok := descriptor.Instance.(*ngalert.AlertNG); ok { return ®istry.Descriptor{ Name: descriptor.Name, Instance: &ng, @@ -52,29 +61,30 @@ func overrideAlertNGInRegistry(t *testing.T, cfg *setting.Cfg) AlertNG { return ng } -func createTestAlertDefinition(t *testing.T, store *storeImpl, intervalSeconds int64) *AlertDefinition { - cmd := saveAlertDefinitionCommand{ +// createTestAlertDefinition creates a dummy alert definition to be used by the tests. +func createTestAlertDefinition(t *testing.T, store *store.DBstore, intervalSeconds int64) *models.AlertDefinition { + cmd := models.SaveAlertDefinitionCommand{ OrgID: 1, Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)), Condition: "A", - Data: []eval.AlertQuery{ + Data: []models.AlertQuery{ { Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 2 > 1" }`), - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(5 * time.Hour), - To: eval.Duration(3 * time.Hour), + RelativeTimeRange: models.RelativeTimeRange{ + From: models.Duration(5 * time.Hour), + To: models.Duration(3 * time.Hour), }, RefID: "A", }, }, IntervalSeconds: &intervalSeconds, } - err := store.saveAlertDefinition(&cmd) + err := store.SaveAlertDefinition(&cmd) require.NoError(t, err) - t.Logf("alert definition: %v with interval: %d created", cmd.Result.getKey(), intervalSeconds) + t.Logf("alert definition: %v with interval: %d created", cmd.Result.GetKey(), intervalSeconds) return cmd.Result } diff --git a/pkg/services/ngalert/validator.go b/pkg/services/ngalert/validator.go deleted file mode 100644 index a1b1133284d..00000000000 --- a/pkg/services/ngalert/validator.go +++ /dev/null @@ -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 -}