Alerting: backend "ng" code cleanup (#33578)

This commit is contained in:
Kyle Brandt 2021-04-30 13:21:57 -04:00 committed by GitHub
parent 599a9b9a6d
commit b8f01fe034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 35 additions and 1484 deletions

View File

@ -6,15 +6,11 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/go-macaron/binding"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/setting"
@ -86,41 +82,4 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) {
DatasourceCache: api.DatasourceCache,
log: logger,
}, m)
// Legacy routes; they will be removed in v8
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertDefinitions))
alertDefinitions.Get("/eval/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.alertDefinitionEvalEndpoint))
alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(ngmodels.EvalAlertConditionCommand{}), routing.Wrap(api.conditionEvalEndpoint))
alertDefinitions.Get("/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.getAlertDefinitionEndpoint))
alertDefinitions.Delete("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, routing.Wrap(api.deleteAlertDefinitionEndpoint))
alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(ngmodels.SaveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint))
alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(ngmodels.UpdateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint))
alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint))
alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint))
})
if api.Cfg.Env == setting.Dev {
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Post("/evalOld", middleware.ReqSignedIn, routing.Wrap(api.conditionEvalOldEndpoint))
})
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("/evalOldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionEvalOldEndpointByID))
})
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("/oldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionOldEndpointByID))
})
api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("/ruleGroupByOldID/:id", middleware.ReqSignedIn, routing.Wrap(api.ruleGroupByOldID))
})
}
api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) {
schedulerRouter.Post("/pause", routing.Wrap(api.pauseScheduler))
schedulerRouter.Post("/unpause", routing.Wrap(api.unpauseScheduler))
}, middleware.ReqOrgAdmin)
api.RouteRegister.Group("/api/alert-instances", func(alertInstances routing.RouteRegister) {
alertInstances.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertInstancesEndpoint))
})
}

View File

@ -258,25 +258,6 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimode
return gettableExtendedRuleNode
}
func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode {
postableExtendedRuleNode := apimodels.PostableExtendedRuleNode{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: r.Title,
Condition: r.Condition,
Data: r.Data,
UID: r.UID,
NoDataState: apimodels.NoDataState(r.NoDataState),
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
},
}
postableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{
For: model.Duration(r.For),
Annotations: r.Annotations,
Labels: r.Labels,
}
return postableExtendedRuleNode
}
func toNamespaceErrorResponse(err error) response.Response {
if errors.Is(err, ngmodels.ErrCannotEditNamespace) {
return response.Error(http.StatusForbidden, err.Error(), err)

View File

@ -1,215 +0,0 @@
package api
import (
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
// listAlertInstancesEndpoint handles GET /api/alert-instances.
func (api *API) listAlertInstancesEndpoint(c *models.ReqContext) response.Response {
cmd := ngmodels.ListAlertInstancesQuery{DefinitionOrgID: c.SignedInUser.OrgId}
if err := api.Store.ListAlertInstances(&cmd); err != nil {
return response.Error(500, "Failed to list alert instances", err)
}
return response.JSON(200, cmd.Result)
}
// conditionEvalEndpoint handles POST /api/alert-definitions/eval.
func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response {
return conditionEval(c, cmd, api.DatasourceCache, api.DataService, api.Cfg)
}
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
func (api *API) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response {
alertDefinitionUID := c.Params(":alertDefinitionUID")
condition, err := api.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId)
if err != nil {
return response.Error(400, "Failed to load alert definition conditions", err)
}
if err := validateCondition(*condition, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
return response.Error(400, "invalid condition", err)
}
evaluator := eval.Evaluator{Cfg: api.Cfg}
evalResults, err := evaluator.ConditionEval(condition, timeNow(), api.DataService)
if err != nil {
return response.Error(400, "Failed to evaluate alert", err)
}
frame := evalResults.AsDataFrame()
return response.JSONStreaming(200, util.DynMap{
"instances": []*data.Frame{&frame},
})
}
// getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionUID.
func (api *API) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
alertDefinitionUID := c.Params(":alertDefinitionUID")
query := ngmodels.GetAlertDefinitionByUIDQuery{
UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId,
}
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
return response.Error(500, "Failed to get alert definition", err)
}
return response.JSON(200, &query.Result)
}
// deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionUID.
func (api *API) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response {
alertDefinitionUID := c.Params(":alertDefinitionUID")
cmd := ngmodels.DeleteAlertDefinitionByUIDCommand{
UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId,
}
if err := api.Store.DeleteAlertDefinitionByUID(&cmd); err != nil {
return response.Error(500, "Failed to delete alert definition", err)
}
return response.Success("Alert definition deleted")
}
// updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionUID.
func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionCommand) response.Response {
cmd.UID = c.Params(":alertDefinitionUID")
cmd.OrgID = c.SignedInUser.OrgId
evalCond := ngmodels.Condition{
Condition: cmd.Condition,
OrgID: c.SignedInUser.OrgId,
Data: cmd.Data,
}
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
return response.Error(400, "invalid condition", err)
}
if err := api.Store.UpdateAlertDefinition(&cmd); err != nil {
return response.Error(500, "Failed to update alert definition", err)
}
return response.JSON(200, cmd.Result)
}
// createAlertDefinitionEndpoint handles POST /api/alert-definitions.
func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.SaveAlertDefinitionCommand) response.Response {
cmd.OrgID = c.SignedInUser.OrgId
evalCond := ngmodels.Condition{
Condition: cmd.Condition,
OrgID: c.SignedInUser.OrgId,
Data: cmd.Data,
}
if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
return response.Error(400, "invalid condition", err)
}
if err := api.Store.SaveAlertDefinition(&cmd); err != nil {
return response.Error(500, "Failed to create alert definition", err)
}
return response.JSON(200, cmd.Result)
}
// listAlertDefinitions handles GET /api/alert-definitions.
func (api *API) listAlertDefinitions(c *models.ReqContext) response.Response {
query := ngmodels.ListAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId}
if err := api.Store.GetOrgAlertDefinitions(&query); err != nil {
return response.Error(500, "Failed to list alert definitions", err)
}
return response.JSON(200, util.DynMap{"results": query.Result})
}
func (api *API) pauseScheduler() response.Response {
err := api.Schedule.Pause()
if err != nil {
return response.Error(500, "Failed to pause scheduler", err)
}
return response.JSON(200, util.DynMap{"message": "alert definition scheduler paused"})
}
func (api *API) unpauseScheduler() response.Response {
err := api.Schedule.Unpause()
if err != nil {
return response.Error(500, "Failed to unpause scheduler", err)
}
return response.JSON(200, util.DynMap{"message": "alert definition scheduler unpaused"})
}
// alertDefinitionPauseEndpoint handles POST /api/alert-definitions/pause.
func (api *API) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response {
cmd.OrgID = c.SignedInUser.OrgId
cmd.Paused = true
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
if err != nil {
return response.Error(500, "Failed to pause alert definition", err)
}
return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions paused", cmd.ResultCount)})
}
// alertDefinitionUnpauseEndpoint handles POST /api/alert-definitions/unpause.
func (api *API) alertDefinitionUnpauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response {
cmd.OrgID = c.SignedInUser.OrgId
cmd.Paused = false
err := api.Store.UpdateAlertDefinitionPaused(&cmd)
if err != nil {
return response.Error(500, "Failed to unpause alert definition", err)
}
return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions unpaused", cmd.ResultCount)})
}
// LoadAlertCondition returns a Condition object for the given alertDefinitionID.
func (api *API) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*ngmodels.Condition, error) {
q := ngmodels.GetAlertDefinitionByUIDQuery{UID: alertDefinitionUID, OrgID: orgID}
if err := api.Store.GetAlertDefinitionByUID(&q); err != nil {
return nil, err
}
alertDefinition := q.Result
err := api.Store.ValidateAlertDefinition(alertDefinition, true)
if err != nil {
return nil, err
}
return &ngmodels.Condition{
Condition: alertDefinition.Condition,
OrgID: alertDefinition.OrgID,
Data: alertDefinition.Data,
}, nil
}
func (api *API) validateOrgAlertDefinition(c *models.ReqContext) {
uid := c.ParamsEscape(":alertDefinitionUID")
if uid == "" {
c.JsonApiErr(403, "Permission denied", nil)
return
}
query := ngmodels.GetAlertDefinitionByUIDQuery{UID: uid, OrgID: c.SignedInUser.OrgId}
if err := api.Store.GetAlertDefinitionByUID(&query); err != nil {
c.JsonApiErr(404, "Alert definition not found", nil)
return
}
}

View File

@ -1,294 +0,0 @@
package api
import (
"fmt"
"time"
"github.com/prometheus/common/model"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/expr/translate"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/util"
)
// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld.
func (api *API) conditionEvalOldEndpoint(c *models.ReqContext) response.Response {
b, err := c.Req.Body().Bytes()
if err != nil {
response.Error(400, "failed to read body", err)
}
evalCond, err := translate.DashboardAlertConditions(b, c.OrgId)
if err != nil {
return response.Error(400, "Failed to translate alert conditions", err)
}
if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
return response.Error(400, "invalid condition", err)
}
//now := cmd.Now
//if now.IsZero() {
//now := timeNow()
//}
evaluator := eval.Evaluator{Cfg: api.Cfg}
evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService)
if err != nil {
return response.Error(400, "Failed to evaluate conditions", err)
}
frame := evalResults.AsDataFrame()
return response.JSONStreaming(200, util.DynMap{
"instances": []*data.Frame{&frame},
})
}
// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld.
func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Response {
id := c.ParamsInt64("id")
if id == 0 {
return response.Error(400, "missing id", nil)
}
getAlert := &models.GetAlertByIdQuery{
Id: id,
}
if err := bus.Dispatch(getAlert); err != nil {
return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err)
}
if getAlert.Result.OrgId != c.SignedInUser.OrgId {
return response.Error(403, "alert does not match organization of user", nil)
}
settings := getAlert.Result.Settings
sb, err := settings.ToDB()
if err != nil {
return response.Error(400, "failed to marshal alert settings", err)
}
evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId)
if err != nil {
return response.Error(400, "Failed to translate alert conditions", err)
}
if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil {
return response.Error(400, "invalid condition", err)
}
//now := cmd.Now
//if now.IsZero() {
//now := timeNow()
//}
evaluator := eval.Evaluator{Cfg: api.Cfg}
evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService)
if err != nil {
return response.Error(400, "Failed to evaluate conditions", err)
}
frame := evalResults.AsDataFrame()
return response.JSONStreaming(200, util.DynMap{
"instances": []*data.Frame{&frame},
})
}
// conditionEvalEndpoint handles POST /api/alert-definitions/oldByID.
func (api *API) conditionOldEndpointByID(c *models.ReqContext) response.Response {
id := c.ParamsInt64("id")
if id == 0 {
return response.Error(400, "missing id", nil)
}
getAlert := &models.GetAlertByIdQuery{
Id: id,
}
if err := bus.Dispatch(getAlert); err != nil {
return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err)
}
if getAlert.Result.OrgId != c.SignedInUser.OrgId {
return response.Error(403, "alert does not match organization of user", nil)
}
settings := getAlert.Result.Settings
sb, err := settings.ToDB()
if err != nil {
return response.Error(400, "failed to marshal alert settings", err)
}
evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId)
if err != nil {
return response.Error(400, "Failed to translate alert conditions", err)
}
return response.JSON(200, evalCond)
}
// ruleGroupByOldID handles POST /api/alert-definitions/ruleGroupByOldID.
func (api *API) ruleGroupByOldID(c *models.ReqContext) response.Response {
id := c.ParamsInt64("id")
if id == 0 {
return response.Error(400, "missing id", nil)
}
save := c.Query("save") == "true"
// Get dashboard alert definition from database.
oldAlert, status, err := transGetAlertById(id, *c.SignedInUser)
if err != nil {
return response.Error(status, "failed to get alert", fmt.Errorf("failed to get alert for alert id %v: %w", id, err))
}
// Translate the dashboard's alerts conditions into SSE queries and conditions.
sseCond, err := transToSSECondition(oldAlert, *c.SignedInUser)
if err != nil {
return response.Error(400, "failed to translate alert conditions",
fmt.Errorf("failed to translate alert conditions for alert id %v: %w", id, err))
}
// Get the dashboard that contains the dashboard Alert.
oldAlertsDash, status, err := transGetAlertsDashById(oldAlert.DashboardId, *c.SignedInUser)
if err != nil {
return response.Error(status, "failed to get alert's dashboard", fmt.Errorf("failed to get dashboard for alert id %v, %w", id, err))
}
isGeneralFolder := oldAlertsDash.FolderId == 0 && !oldAlertsDash.IsFolder
var namespaceUID string
if isGeneralFolder {
namespaceUID = "General"
} else {
// Get the folder that contains the dashboard that contains the dashboard alert.
getFolder := &models.GetDashboardQuery{
Id: oldAlertsDash.FolderId,
OrgId: oldAlertsDash.OrgId,
}
if err := bus.Dispatch(getFolder); err != nil {
return response.Error(400, fmt.Sprintf("could find folder %v for alert with id %v", getFolder.Id, id), err)
}
namespaceUID = getFolder.Result.Uid
}
noDataSetting, execErrSetting, err := transNoDataExecSettings(oldAlert, *c.SignedInUser)
if err != nil {
return response.Error(400, "unable to translate nodata/exec error settings",
fmt.Errorf("unable to translate nodata/exec error settings for alert id %v: %w", id, err))
}
ruleTags := map[string]string{}
for k, v := range oldAlert.Settings.Get("alertRuleTags").MustMap() {
sV, ok := v.(string)
if !ok {
return response.Error(400, "unable to unmarshal rule tags",
fmt.Errorf("unexpected type %T for tag %v", v, k))
}
ruleTags[k] = sV
}
rule := ngmodels.AlertRule{
Title: oldAlert.Name,
Data: sseCond.Data,
Condition: sseCond.Condition,
NoDataState: *noDataSetting,
ExecErrState: *execErrSetting,
For: oldAlert.For,
Annotations: ruleTags,
}
rgc := apimodels.PostableRuleGroupConfig{
// TODO? Generate new name on conflict?
Name: oldAlert.Name,
Interval: transAdjustInterval(oldAlert.Frequency),
Rules: []apimodels.PostableExtendedRuleNode{
toPostableExtendedRuleNode(rule),
},
}
cmd := store.UpdateRuleGroupCmd{
OrgID: oldAlert.OrgId,
NamespaceUID: namespaceUID,
RuleGroupConfig: rgc,
}
if !save {
return response.JSON(200, cmd)
}
// note: Update rule group will set the Interval within the grafana_alert from
// the interval of the group.
err = api.RuleStore.UpdateRuleGroup(cmd)
if err != nil {
return response.JSON(400, util.DynMap{
"message:": "failed to save alert rule",
"error": err.Error(),
"cmd": cmd,
})
}
return response.JSON(200, cmd)
}
func transAdjustInterval(freq int64) model.Duration {
// 10 corresponds to the SchedulerCfg, but TODO not worrying about fetching for now.
var baseFreq int64 = 10
if freq <= baseFreq {
return model.Duration(time.Second * 10)
}
return model.Duration(time.Duration((freq - (freq % baseFreq))) * time.Second)
}
func transGetAlertById(id int64, user models.SignedInUser) (*models.Alert, int, error) {
getAlert := &models.GetAlertByIdQuery{
Id: id,
}
if err := bus.Dispatch(getAlert); err != nil {
return nil, 400, fmt.Errorf("could find alert with id %v: %w", id, err)
}
if getAlert.Result.OrgId != user.OrgId {
return nil, 403, fmt.Errorf("alert does not match organization of user")
}
return getAlert.Result, 0, nil
}
func transGetAlertsDashById(dashboardId int64, user models.SignedInUser) (*models.Dashboard, int, error) {
getDash := &models.GetDashboardQuery{
Id: dashboardId,
OrgId: user.OrgId,
}
if err := bus.Dispatch(getDash); err != nil {
return nil, 400, fmt.Errorf("could find dashboard with id %v: %w", dashboardId, err)
}
return getDash.Result, 0, nil
}
func transToSSECondition(m *models.Alert, user models.SignedInUser) (*ngmodels.Condition, error) {
sb, err := m.Settings.ToDB()
if err != nil {
return nil, fmt.Errorf("failed to marshal alert settings: %w", err)
}
evalCond, err := translate.DashboardAlertConditions(sb, user.OrgId)
if err != nil {
return nil, fmt.Errorf("failed to translate dashboard alert to SSE conditions: %w", err)
}
return evalCond, nil
}
func transNoDataExecSettings(m *models.Alert, user models.SignedInUser) (*ngmodels.NoDataState, *ngmodels.ExecutionErrorState, error) {
oldNoData := m.Settings.Get("noDataState").MustString()
noDataSetting, err := transNoData(oldNoData)
if err != nil {
return nil, nil, err
}
oldExecErr := m.Settings.Get("executionErrorState").MustString()
execErrSetting, err := transExecErr(oldExecErr)
if err != nil {
return nil, nil, err
}
return &noDataSetting, &execErrSetting, nil
}
func transNoData(s string) (ngmodels.NoDataState, error) {
switch s {
case "ok":
return ngmodels.OK, nil
case "no_data":
return ngmodels.NoData, nil
case "alerting":
return ngmodels.Alerting, nil
case "keep_state":
return ngmodels.KeepLastState, nil
}
return ngmodels.NoData, fmt.Errorf("unrecognized No Data setting %v", s)
}
func transExecErr(s string) (ngmodels.ExecutionErrorState, error) {
switch s {
case "alerting":
return ngmodels.AlertingErrState, nil
case "KeepLastState":
return ngmodels.KeepLastStateErrState, nil
}
return ngmodels.AlertingErrState, fmt.Errorf("unrecognized Execution Error setting %v", s)
}

View File

@ -80,7 +80,6 @@ type FetchUniqueOrgIdsQuery struct {
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"`
Labels InstanceLabels `json:"labels"`
LabelsHash string `json:"labeHash"`
CurrentState InstanceStateType `json:"currentState"`

View File

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

View File

@ -1,16 +1,11 @@
package store
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
// TimeNow makes it possible to test usage of time
@ -21,17 +16,9 @@ const AlertDefinitionMaxTitleLength = 190
// Store is the interface for persisting alert definitions and instances
type Store interface {
DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error
GetAlertDefinitionByUID(*models.GetAlertDefinitionByUIDQuery) error
GetAlertDefinitions(*models.ListAlertDefinitionsQuery) error
GetOrgAlertDefinitions(*models.ListAlertDefinitionsQuery) error
SaveAlertDefinition(*models.SaveAlertDefinitionCommand) error
UpdateAlertDefinition(*models.UpdateAlertDefinitionCommand) error
GetAlertInstance(*models.GetAlertInstanceQuery) error
ListAlertInstances(*models.ListAlertInstancesQuery) error
SaveAlertInstance(*models.SaveAlertInstanceCommand) error
ValidateAlertDefinition(*models.AlertDefinition, bool) error
UpdateAlertDefinitionPaused(*models.UpdateAlertDefinitionPausedCommand) error
FetchOrgIds(cmd *models.FetchUniqueOrgIdsQuery) error
}
@ -50,297 +37,3 @@ type DBstore struct {
DefaultIntervalSeconds int64
SQLStore *sqlstore.SQLStore `inject:""`
}
func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*models.AlertDefinition, error) {
// we consider optionally enabling some caching
alertDefinition := models.AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
has, err := sess.Get(&alertDefinition)
if !has {
return nil, models.ErrAlertDefinitionNotFound
}
if err != nil {
return nil, err
}
return &alertDefinition, nil
}
// DeleteAlertDefinitionByUID is a handler for deleting an alert definition.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (st DBstore) DeleteAlertDefinitionByUID(cmd *models.DeleteAlertDefinitionByUIDCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM alert_definition WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID)
if err != nil {
return err
}
_, err = sess.Exec("DELETE FROM alert_definition_version WHERE alert_definition_uid = ?", cmd.UID)
if err != nil {
return err
}
_, err = sess.Exec("DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid = ?", cmd.OrgID, cmd.UID)
if err != nil {
return err
}
return nil
})
}
// GetAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (st DBstore) GetAlertDefinitionByUID(query *models.GetAlertDefinitionByUIDQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID)
if err != nil {
return err
}
query.Result = alertDefinition
return nil
})
}
// SaveAlertDefinition is a handler for saving a new alert definition.
func (st DBstore) SaveAlertDefinition(cmd *models.SaveAlertDefinitionCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
intervalSeconds := st.DefaultIntervalSeconds
if cmd.IntervalSeconds != nil {
intervalSeconds = *cmd.IntervalSeconds
}
var initialVersion int64 = 1
uid, err := generateNewAlertDefinitionUID(sess, cmd.OrgID)
if err != nil {
return fmt.Errorf("failed to generate UID for alert definition %q: %w", cmd.Title, err)
}
alertDefinition := &models.AlertDefinition{
OrgID: cmd.OrgID,
Title: cmd.Title,
Condition: cmd.Condition,
Data: cmd.Data,
IntervalSeconds: intervalSeconds,
Version: initialVersion,
UID: uid,
}
if err := st.ValidateAlertDefinition(alertDefinition, false); err != nil {
return err
}
if err := alertDefinition.PreSave(TimeNow); err != nil {
return err
}
if _, err := sess.Insert(alertDefinition); err != nil {
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "title") {
return fmt.Errorf("an alert definition with the title '%s' already exists: %w", cmd.Title, err)
}
return err
}
alertDefVersion := models.AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: alertDefinition.UID,
Version: alertDefinition.Version,
Created: alertDefinition.Updated,
Condition: alertDefinition.Condition,
Title: alertDefinition.Title,
Data: alertDefinition.Data,
IntervalSeconds: alertDefinition.IntervalSeconds,
}
if _, err := sess.Insert(alertDefVersion); err != nil {
return err
}
cmd.Result = alertDefinition
return nil
})
}
// UpdateAlertDefinition is a handler for updating an existing alert definition.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (st DBstore) UpdateAlertDefinition(cmd *models.UpdateAlertDefinitionCommand) error {
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID)
if err != nil {
if errors.Is(err, models.ErrAlertDefinitionNotFound) {
return nil
}
return err
}
title := cmd.Title
if title == "" {
title = existingAlertDefinition.Title
}
condition := cmd.Condition
if condition == "" {
condition = existingAlertDefinition.Condition
}
data := cmd.Data
if data == nil {
data = existingAlertDefinition.Data
}
intervalSeconds := cmd.IntervalSeconds
if intervalSeconds == nil {
intervalSeconds = &existingAlertDefinition.IntervalSeconds
}
// explicitly set all fields regardless of being provided or not
alertDefinition := &models.AlertDefinition{
ID: existingAlertDefinition.ID,
Title: title,
Condition: condition,
Data: data,
OrgID: existingAlertDefinition.OrgID,
IntervalSeconds: *intervalSeconds,
UID: existingAlertDefinition.UID,
}
if err := st.ValidateAlertDefinition(alertDefinition, true); err != nil {
return err
}
if err := alertDefinition.PreSave(TimeNow); err != nil {
return err
}
alertDefinition.Version = existingAlertDefinition.Version + 1
_, err = sess.ID(existingAlertDefinition.ID).Update(alertDefinition)
if err != nil {
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "title") {
return fmt.Errorf("an alert definition with the title '%s' already exists: %w", cmd.Title, err)
}
return err
}
alertDefVersion := models.AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: alertDefinition.UID,
ParentVersion: alertDefinition.Version,
Version: alertDefinition.Version,
Condition: alertDefinition.Condition,
Created: alertDefinition.Updated,
Title: alertDefinition.Title,
Data: alertDefinition.Data,
IntervalSeconds: alertDefinition.IntervalSeconds,
}
if _, err := sess.Insert(alertDefVersion); err != nil {
return err
}
cmd.Result = alertDefinition
return nil
})
}
// GetOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
func (st DBstore) GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinitions := make([]*models.AlertDefinition, 0)
q := "SELECT * FROM alert_definition WHERE org_id = ?"
if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil {
return err
}
query.Result = alertDefinitions
return nil
})
}
// GetAlertDefinitions returns alert definition identifier, interval, version and pause state
// that are useful for it's scheduling.
func (st DBstore) GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alerts := make([]*models.AlertDefinition, 0)
q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_definition"
if err := sess.SQL(q).Find(&alerts); err != nil {
return err
}
query.Result = alerts
return nil
})
}
// UpdateAlertDefinitionPaused update the pause state of an alert definition.
func (st DBstore) UpdateAlertDefinitionPaused(cmd *models.UpdateAlertDefinitionPausedCommand) error {
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if len(cmd.UIDs) == 0 {
return nil
}
placeHolders := strings.Builder{}
const separator = ", "
separatorVar := separator
params := []interface{}{cmd.Paused, cmd.OrgID}
for i, UID := range cmd.UIDs {
if i == len(cmd.UIDs)-1 {
separatorVar = ""
}
placeHolders.WriteString(fmt.Sprintf("?%s", separatorVar))
params = append(params, UID)
}
sql := fmt.Sprintf("UPDATE alert_definition SET paused = ? WHERE org_id = ? AND uid IN (%s)", placeHolders.String())
// prepend sql statement to params
var i interface{}
params = append(params, i)
copy(params[1:], params[0:])
params[0] = sql
res, err := sess.Exec(params...)
if err != nil {
return err
}
if resultCount, err := res.RowsAffected(); err == nil {
cmd.ResultCount = resultCount
}
return nil
})
}
func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&models.AlertDefinition{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", models.ErrAlertDefinitionFailedGenerateUniqueUID
}
// ValidateAlertDefinition validates the alert definition interval and organisation.
// If requireData is true checks that it contains at least one alert query
func (st DBstore) ValidateAlertDefinition(alertDefinition *models.AlertDefinition, requireData bool) error {
if !requireData && len(alertDefinition.Data) == 0 {
return fmt.Errorf("no queries or expressions are found")
}
if alertDefinition.Title == "" {
return fmt.Errorf("title is empty")
}
if alertDefinition.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 {
return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertDefinition.IntervalSeconds)*time.Second, st.BaseInterval)
}
// enfore max name length in SQLite
if len(alertDefinition.Title) > AlertDefinitionMaxTitleLength {
return fmt.Errorf("name length should not be greater than %d", AlertDefinitionMaxTitleLength)
}
if alertDefinition.OrgID == 0 {
return fmt.Errorf("no organisation is found")
}
return nil
}

View File

@ -1,450 +0,0 @@
// +build integration
package tests
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"testing"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const baseIntervalSeconds = 10
func mockTimeNow() {
var timeSeed int64
store.TimeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0).UTC()
timeSeed++
return fakeNow
}
}
func resetTimeNow() {
store.TimeNow = time.Now
}
func TestCreatingAlertDefinition(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
var customIntervalSeconds int64 = 120
testCases := []struct {
desc string
inputIntervalSeconds *int64
inputTitle string
expectedError error
expectedInterval int64
expectedUpdated time.Time
}{
{
desc: "should create successfully an alert definition with default interval",
inputIntervalSeconds: nil,
inputTitle: "a name",
expectedInterval: dbstore.DefaultIntervalSeconds,
expectedUpdated: time.Unix(0, 0).UTC(),
},
{
desc: "should create successfully an alert definition with custom interval",
inputIntervalSeconds: &customIntervalSeconds,
inputTitle: "another name",
expectedInterval: customIntervalSeconds,
expectedUpdated: time.Unix(1, 0).UTC(),
},
{
desc: "should fail to create an alert definition with too big name",
inputIntervalSeconds: &customIntervalSeconds,
inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
expectedError: errors.New(""),
},
{
desc: "should fail to create an alert definition with empty title",
inputIntervalSeconds: &customIntervalSeconds,
inputTitle: "",
expectedError: fmt.Errorf("title is empty"),
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
q := models.SaveAlertDefinitionCommand{
OrgID: 1,
Title: tc.inputTitle,
Condition: "B",
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type":"math",
"expression":"2 + 3 > 1"
}`),
RefID: "B",
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(time.Duration(5) * time.Hour),
To: models.Duration(time.Duration(3) * time.Hour),
},
},
},
}
if tc.inputIntervalSeconds != nil {
q.IntervalSeconds = tc.inputIntervalSeconds
}
err := dbstore.SaveAlertDefinition(&q)
switch {
case tc.expectedError != nil:
require.Error(t, err)
default:
require.NoError(t, err)
assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
assert.Equal(t, tc.expectedInterval, q.Result.IntervalSeconds)
assert.Equal(t, int64(1), q.Result.Version)
}
})
}
}
func TestCreatingConflictionAlertDefinition(t *testing.T) {
t.Run("Should fail to create alert definition with conflicting org_id, title", func(t *testing.T) {
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
q := models.SaveAlertDefinitionCommand{
OrgID: 1,
Title: "title",
Condition: "B",
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type":"math",
"expression":"2 + 3 > 1"
}`),
RefID: "B",
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(time.Duration(5) * time.Hour),
To: models.Duration(time.Duration(3) * time.Hour),
},
},
},
}
err := dbstore.SaveAlertDefinition(&q)
require.NoError(t, err)
err = dbstore.SaveAlertDefinition(&q)
require.Error(t, err)
assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err))
})
}
func TestUpdatingAlertDefinition(t *testing.T) {
t.Run("zero rows affected when updating unknown alert", func(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
q := models.UpdateAlertDefinitionCommand{
UID: "unknown",
OrgID: 1,
Title: "something completely different",
Condition: "A",
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type":"math",
"expression":"2 + 2 > 1"
}`),
RefID: "A",
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(time.Duration(5) * time.Hour),
To: models.Duration(time.Duration(3) * time.Hour),
},
},
},
}
err := dbstore.UpdateAlertDefinition(&q)
require.NoError(t, err)
})
t.Run("updating existing alert", func(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
var initialInterval int64 = 120
alertDefinition := createTestAlertDefinition(t, dbstore, initialInterval)
created := alertDefinition.Updated
var customInterval int64 = 30
testCases := []struct {
desc string
inputOrgID int64
inputTitle string
inputInterval *int64
expectedError error
expectedIntervalSeconds int64
expectedUpdated time.Time
expectedTitle string
}{
{
desc: "should not update previous interval if it's not provided",
inputInterval: nil,
inputOrgID: alertDefinition.OrgID,
inputTitle: "something completely different",
expectedIntervalSeconds: initialInterval,
expectedUpdated: time.Unix(1, 0).UTC(),
expectedTitle: "something completely different",
},
{
desc: "should update interval if it's provided",
inputInterval: &customInterval,
inputOrgID: alertDefinition.OrgID,
inputTitle: "something completely different",
expectedIntervalSeconds: customInterval,
expectedUpdated: time.Unix(2, 0).UTC(),
expectedTitle: "something completely different",
},
{
desc: "should not update organisation if it's provided",
inputInterval: &customInterval,
inputOrgID: 0,
inputTitle: "something completely different",
expectedIntervalSeconds: customInterval,
expectedUpdated: time.Unix(3, 0).UTC(),
expectedTitle: "something completely different",
},
{
desc: "should not update alert definition if the title it's too big",
inputInterval: &customInterval,
inputOrgID: 0,
inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1),
expectedError: errors.New(""),
},
{
desc: "should not update alert definition title if the title is empty",
inputInterval: &customInterval,
inputOrgID: 0,
inputTitle: "",
expectedIntervalSeconds: customInterval,
expectedUpdated: time.Unix(4, 0).UTC(),
expectedTitle: "something completely different",
},
}
q := models.UpdateAlertDefinitionCommand{
UID: (*alertDefinition).UID,
Condition: "B",
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type":"math",
"expression":"2 + 3 > 1"
}`),
RefID: "B",
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(5 * time.Hour),
To: models.Duration(3 * time.Hour),
},
},
},
}
lastUpdated := created
previousAlertDefinition := alertDefinition
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
if tc.inputInterval != nil {
q.IntervalSeconds = tc.inputInterval
}
if tc.inputOrgID != 0 {
q.OrgID = tc.inputOrgID
}
q.Title = tc.inputTitle
err := dbstore.UpdateAlertDefinition(&q)
switch {
case tc.expectedError != nil:
require.Error(t, err)
assert.Equal(t, previousAlertDefinition.Title, q.Result.Title)
assert.Equal(t, previousAlertDefinition.Condition, q.Result.Condition)
assert.Equal(t, len(previousAlertDefinition.Data), len(q.Result.Data))
assert.Equal(t, previousAlertDefinition.IntervalSeconds, q.Result.IntervalSeconds)
assert.Equal(t, previousAlertDefinition.Updated, q.Result.Updated)
assert.Equal(t, previousAlertDefinition.Version, q.Result.Version)
assert.Equal(t, previousAlertDefinition.OrgID, q.Result.OrgID)
assert.Equal(t, previousAlertDefinition.UID, q.Result.UID)
default:
require.NoError(t, err)
assert.Equal(t, previousAlertDefinition.ID, q.Result.ID)
assert.Equal(t, previousAlertDefinition.UID, q.Result.UID)
assert.True(t, q.Result.Updated.After(lastUpdated))
assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version)
assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID)
assert.Equal(t, "something completely different", q.Result.Title)
assert.Equal(t, "B", q.Result.Condition)
assert.Equal(t, 1, len(q.Result.Data))
assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
assert.Equal(t, tc.expectedIntervalSeconds, q.Result.IntervalSeconds)
assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version)
assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID)
assert.Equal(t, alertDefinition.UID, q.Result.UID)
previousAlertDefinition = q.Result
}
})
}
})
}
func TestUpdatingConflictingAlertDefinition(t *testing.T) {
t.Run("should fail to update alert definition with reserved title", func(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
var initialInterval int64 = 120
alertDef1 := createTestAlertDefinition(t, dbstore, initialInterval)
alertDef2 := createTestAlertDefinition(t, dbstore, initialInterval)
q := models.UpdateAlertDefinitionCommand{
UID: (*alertDef2).UID,
Title: alertDef1.Title,
Condition: "B",
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type":"math",
"expression":"2 + 3 > 1"
}`),
RefID: "B",
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(5 * time.Hour),
To: models.Duration(3 * time.Hour),
},
},
},
}
err := dbstore.UpdateAlertDefinition(&q)
require.Error(t, err)
assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err))
})
}
func TestDeletingAlertDefinition(t *testing.T) {
t.Run("zero rows affected when deleting unknown alert", func(t *testing.T) {
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
q := models.DeleteAlertDefinitionByUIDCommand{
UID: "unknown",
OrgID: 1,
}
err := dbstore.DeleteAlertDefinitionByUID(&q)
require.NoError(t, err)
})
t.Run("deleting successfully existing alert", func(t *testing.T) {
dbstore := setupTestEnv(t, baseIntervalSeconds)
t.Cleanup(registry.ClearOverrides)
alertDefinition := createTestAlertDefinition(t, dbstore, 60)
q := models.DeleteAlertDefinitionByUIDCommand{
UID: (*alertDefinition).UID,
OrgID: 1,
}
// save an instance for the definition
saveCmd := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition.OrgID,
DefinitionUID: alertDefinition.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
err := dbstore.SaveAlertInstance(saveCmd)
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: alertDefinition.OrgID,
DefinitionUID: alertDefinition.UID,
}
err = dbstore.ListAlertInstances(listQuery)
require.NoError(t, err)
require.Len(t, listQuery.Result, 1)
err = dbstore.DeleteAlertDefinitionByUID(&q)
require.NoError(t, err)
// assert that alert instance is deleted
err = dbstore.ListAlertInstances(listQuery)
require.NoError(t, err)
require.Len(t, listQuery.Result, 0)
})
}
func getLongString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = 'a'
}
return string(b)
}
// createTestAlertDefinition creates a dummy alert definition to be used by the tests.
func createTestAlertDefinition(t *testing.T, dbstore *store.DBstore, intervalSeconds int64) *models.AlertDefinition {
cmd := models.SaveAlertDefinitionCommand{
OrgID: 1,
Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)),
Condition: "A",
Data: []models.AlertQuery{
{
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type":"math",
"expression":"2 + 2 > 1"
}`),
RelativeTimeRange: models.RelativeTimeRange{
From: models.Duration(5 * time.Hour),
To: models.Duration(3 * time.Hour),
},
RefID: "A",
},
},
IntervalSeconds: &intervalSeconds,
}
err := dbstore.SaveAlertDefinition(&cmd)
require.NoError(t, err)
t.Logf("alert definition: %v with interval: %d created", cmd.Result.GetKey(), intervalSeconds)
return cmd.Result
}

View File

@ -4,31 +4,44 @@ package tests
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/stretchr/testify/require"
)
const baseIntervalSeconds = 10
func mockTimeNow() {
var timeSeed int64
store.TimeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0).UTC()
timeSeed++
return fakeNow
}
}
func TestAlertInstanceOperations(t *testing.T) {
dbstore := setupTestEnv(t, baseIntervalSeconds)
alertDefinition1 := createTestAlertDefinition(t, dbstore, 60)
orgID := alertDefinition1.OrgID
alertRule1 := createTestAlertRule(t, dbstore, 60)
orgID := alertRule1.OrgID
alertDefinition2 := createTestAlertDefinition(t, dbstore, 60)
require.Equal(t, orgID, alertDefinition2.OrgID)
alertRule2 := createTestAlertRule(t, dbstore, 60)
require.Equal(t, orgID, alertRule2.OrgID)
alertDefinition3 := createTestAlertDefinition(t, dbstore, 60)
require.Equal(t, orgID, alertDefinition3.OrgID)
alertRule3 := createTestAlertRule(t, dbstore, 60)
require.Equal(t, orgID, alertRule3.OrgID)
alertDefinition4 := createTestAlertDefinition(t, dbstore, 60)
require.Equal(t, orgID, alertDefinition4.OrgID)
alertRule4 := createTestAlertRule(t, dbstore, 60)
require.Equal(t, orgID, alertRule4.OrgID)
t.Run("can save and read new alert instance", func(t *testing.T) {
saveCmd := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition1.OrgID,
DefinitionUID: alertDefinition1.UID,
DefinitionOrgID: alertRule1.OrgID,
DefinitionUID: alertRule1.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
@ -45,14 +58,14 @@ func TestAlertInstanceOperations(t *testing.T) {
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)
require.Equal(t, alertRule1.OrgID, getCmd.Result.DefinitionOrgID)
require.Equal(t, alertRule1.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,
DefinitionOrgID: alertRule2.OrgID,
DefinitionUID: alertRule2.UID,
State: models.InstanceStateNormal,
Labels: models.InstanceLabels{},
}
@ -67,15 +80,15 @@ func TestAlertInstanceOperations(t *testing.T) {
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, alertRule2.OrgID, getCmd.Result.DefinitionOrgID)
require.Equal(t, alertRule2.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,
DefinitionOrgID: alertRule3.OrgID,
DefinitionUID: alertRule3.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
@ -128,8 +141,8 @@ func TestAlertInstanceOperations(t *testing.T) {
t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) {
saveCmdOne := &models.SaveAlertInstanceCommand{
DefinitionOrgID: alertDefinition4.OrgID,
DefinitionUID: alertDefinition4.UID,
DefinitionOrgID: alertRule4.OrgID,
DefinitionUID: alertRule4.UID,
State: models.InstanceStateFiring,
Labels: models.InstanceLabels{"test": "testValue"},
}
@ -147,8 +160,8 @@ func TestAlertInstanceOperations(t *testing.T) {
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
DefinitionOrgID: alertDefinition4.OrgID,
DefinitionUID: alertDefinition4.UID,
DefinitionOrgID: alertRule4.OrgID,
DefinitionUID: alertRule4.UID,
}
err = dbstore.ListAlertInstances(listQuery)
@ -160,7 +173,5 @@ func TestAlertInstanceOperations(t *testing.T) {
require.Equal(t, saveCmdTwo.DefinitionUID, listQuery.Result[0].DefinitionUID)
require.Equal(t, saveCmdTwo.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)
})
}