Alerting NG: update API to expect UIDs instead of IDs (#29896)

* Change API to expect UIDs instead of ID

* Remove unnecessary transactions

When only one query is executed

* Modify API responses

* Cleanup tests

* Use globally orgID and UID for identifying alert definitions
This commit is contained in:
Sofia Papagiannaki 2021-01-07 17:45:42 +02:00 committed by GitHub
parent a6aa0024a2
commit 5560be73bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 213 additions and 212 deletions

View File

@ -15,12 +15,12 @@ import (
func (ng *AlertNG) registerAPIEndpoints() { func (ng *AlertNG) registerAPIEndpoints() {
ng.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { ng.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) {
alertDefinitions.Get("", middleware.ReqSignedIn, api.Wrap(ng.listAlertDefinitions)) alertDefinitions.Get("", middleware.ReqSignedIn, api.Wrap(ng.listAlertDefinitions))
alertDefinitions.Get("/eval/:alertDefinitionId", ng.validateOrgAlertDefinition, api.Wrap(ng.alertDefinitionEvalEndpoint)) alertDefinitions.Get("/eval/:alertDefinitionUID", ng.validateOrgAlertDefinition, api.Wrap(ng.alertDefinitionEvalEndpoint))
alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(evalAlertConditionCommand{}), api.Wrap(ng.conditionEvalEndpoint)) alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(evalAlertConditionCommand{}), api.Wrap(ng.conditionEvalEndpoint))
alertDefinitions.Get("/:alertDefinitionId", ng.validateOrgAlertDefinition, api.Wrap(ng.getAlertDefinitionEndpoint)) alertDefinitions.Get("/:alertDefinitionUID", ng.validateOrgAlertDefinition, api.Wrap(ng.getAlertDefinitionEndpoint))
alertDefinitions.Delete("/:alertDefinitionId", ng.validateOrgAlertDefinition, api.Wrap(ng.deleteAlertDefinitionEndpoint)) alertDefinitions.Delete("/:alertDefinitionUID", ng.validateOrgAlertDefinition, api.Wrap(ng.deleteAlertDefinitionEndpoint))
alertDefinitions.Post("/", middleware.ReqSignedIn, binding.Bind(saveAlertDefinitionCommand{}), api.Wrap(ng.createAlertDefinitionEndpoint)) alertDefinitions.Post("/", middleware.ReqSignedIn, binding.Bind(saveAlertDefinitionCommand{}), api.Wrap(ng.createAlertDefinitionEndpoint))
alertDefinitions.Put("/:alertDefinitionId", ng.validateOrgAlertDefinition, binding.Bind(updateAlertDefinitionCommand{}), api.Wrap(ng.updateAlertDefinitionEndpoint)) alertDefinitions.Put("/:alertDefinitionUID", ng.validateOrgAlertDefinition, binding.Bind(updateAlertDefinitionCommand{}), api.Wrap(ng.updateAlertDefinitionEndpoint))
}) })
ng.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) { ng.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) {
@ -52,11 +52,11 @@ func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertCond
}) })
} }
// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:dashboardId/:panelId/:refId". // alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID.
func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) api.Response { func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) api.Response {
alertDefinitionID := c.ParamsInt64(":alertDefinitionId") alertDefinitionUID := c.ParamsEscape(":alertDefinitionUID")
condition, err := ng.LoadAlertCondition(alertDefinitionID) condition, err := ng.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId)
if err != nil { if err != nil {
return api.Error(400, "Failed to load alert definition conditions", err) return api.Error(400, "Failed to load alert definition conditions", err)
} }
@ -67,7 +67,7 @@ func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) api.Respons
evalResults, err := eval.ConditionEval(condition, timeNow()) evalResults, err := eval.ConditionEval(condition, timeNow())
if err != nil { if err != nil {
return api.Error(400, "Failed to evaludate alert", err) return api.Error(400, "Failed to evaluate alert", err)
} }
frame := evalResults.AsDataFrame() frame := evalResults.AsDataFrame()
@ -85,44 +85,41 @@ func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) api.Respons
}) })
} }
// getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionId. // getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionUID.
func (ng *AlertNG) getAlertDefinitionEndpoint(c *models.ReqContext) api.Response { func (ng *AlertNG) getAlertDefinitionEndpoint(c *models.ReqContext) api.Response {
alertDefinitionID := c.ParamsInt64(":alertDefinitionId") alertDefinitionUID := c.ParamsEscape(":alertDefinitionUID")
query := getAlertDefinitionByIDQuery{ query := getAlertDefinitionByUIDQuery{
ID: alertDefinitionID, UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId,
} }
if err := ng.getAlertDefinitionByID(&query); err != nil { if err := ng.getAlertDefinitionByUID(&query); err != nil {
return api.Error(500, "Failed to get alert definition", err) return api.Error(500, "Failed to get alert definition", err)
} }
return api.JSON(200, &query.Result) return api.JSON(200, &query.Result)
} }
// deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionId. // deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionUID.
func (ng *AlertNG) deleteAlertDefinitionEndpoint(c *models.ReqContext) api.Response { func (ng *AlertNG) deleteAlertDefinitionEndpoint(c *models.ReqContext) api.Response {
alertDefinitionID := c.ParamsInt64(":alertDefinitionId") alertDefinitionUID := c.ParamsEscape(":alertDefinitionUID")
cmd := deleteAlertDefinitionByIDCommand{ cmd := deleteAlertDefinitionByUIDCommand{
ID: alertDefinitionID, UID: alertDefinitionUID,
OrgID: c.SignedInUser.OrgId, OrgID: c.SignedInUser.OrgId,
} }
if err := ng.deleteAlertDefinitionByID(&cmd); err != nil { if err := ng.deleteAlertDefinitionByUID(&cmd); err != nil {
return api.Error(500, "Failed to delete alert definition", err) return api.Error(500, "Failed to delete alert definition", err)
} }
if cmd.RowsAffected != 1 {
ng.log.Warn("unexpected number of rows affected on alert definition delete", "definitionID", alertDefinitionID, "rowsAffected", cmd.RowsAffected)
}
return api.Success("Alert definition deleted") return api.Success("Alert definition deleted")
} }
// updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionId. // updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionUID.
func (ng *AlertNG) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updateAlertDefinitionCommand) api.Response { func (ng *AlertNG) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updateAlertDefinitionCommand) api.Response {
cmd.ID = c.ParamsInt64(":alertDefinitionId") cmd.UID = c.ParamsEscape(":alertDefinitionUID")
cmd.OrgID = c.SignedInUser.OrgId cmd.OrgID = c.SignedInUser.OrgId
if err := ng.validateCondition(cmd.Condition, c.SignedInUser); err != nil { if err := ng.validateCondition(cmd.Condition, c.SignedInUser); err != nil {
@ -133,11 +130,7 @@ func (ng *AlertNG) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updat
return api.Error(500, "Failed to update alert definition", err) return api.Error(500, "Failed to update alert definition", err)
} }
if cmd.RowsAffected != 1 { return api.JSON(200, cmd.Result)
ng.log.Warn("unexpected number of rows affected on alert definition update", "definitionID", cmd.ID, "rowsAffected", cmd.RowsAffected)
}
return api.Success("Alert definition updated")
} }
// createAlertDefinitionEndpoint handles POST /api/alert-definitions. // createAlertDefinitionEndpoint handles POST /api/alert-definitions.
@ -152,7 +145,7 @@ func (ng *AlertNG) createAlertDefinitionEndpoint(c *models.ReqContext, cmd saveA
return api.Error(500, "Failed to create alert definition", err) return api.Error(500, "Failed to create alert definition", err)
} }
return api.JSON(200, util.DynMap{"id": cmd.Result.ID}) return api.JSON(200, cmd.Result)
} }
// listAlertDefinitions handles GET /api/alert-definitions. // listAlertDefinitions handles GET /api/alert-definitions.

View File

@ -80,6 +80,6 @@ func createTestAlertDefinition(t *testing.T, ng *AlertNG, intervalSeconds int64)
} }
err := ng.saveAlertDefinition(&cmd) err := ng.saveAlertDefinition(&cmd)
require.NoError(t, err) require.NoError(t, err)
t.Logf("alert definition: %d with interval: %d created", cmd.Result.ID, intervalSeconds) t.Logf("alert definition: %v with interval: %d created", cmd.Result.getKey(), intervalSeconds)
return cmd.Result return cmd.Result
} }

View File

@ -9,9 +9,10 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func getAlertDefinitionByID(alertDefinitionID int64, sess *sqlstore.DBSession) (*AlertDefinition, error) { func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*AlertDefinition, error) {
alertDefinition := AlertDefinition{} // we consider optionally enabling some caching
has, err := sess.ID(alertDefinitionID).Get(&alertDefinition) alertDefinition := AlertDefinition{OrgID: orgID, UID: alertDefinitionUID}
has, err := sess.Get(&alertDefinition)
if !has { if !has {
return nil, errAlertDefinitionNotFound return nil, errAlertDefinitionNotFound
} }
@ -23,20 +24,19 @@ func getAlertDefinitionByID(alertDefinitionID int64, sess *sqlstore.DBSession) (
// deleteAlertDefinitionByID is a handler for deleting an alert definition. // deleteAlertDefinitionByID is a handler for deleting an alert definition.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. // It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (ng *AlertNG) deleteAlertDefinitionByID(cmd *deleteAlertDefinitionByIDCommand) error { func (ng *AlertNG) deleteAlertDefinitionByUID(cmd *deleteAlertDefinitionByUIDCommand) error {
return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
res, err := sess.Exec("DELETE FROM alert_definition WHERE id = ?", cmd.ID) res, err := sess.Exec("DELETE FROM alert_definition WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID)
if err != nil { if err != nil {
return err return err
} }
rowsAffected, err := res.RowsAffected() _, err = res.RowsAffected()
if err != nil { if err != nil {
return err return err
} }
cmd.RowsAffected = rowsAffected
_, err = sess.Exec("DELETE FROM alert_definition_version WHERE alert_definition_id = ?", cmd.ID) _, err = sess.Exec("DELETE FROM alert_definition_version WHERE alert_definition_uid = ?", cmd.UID)
if err != nil { if err != nil {
return err return err
} }
@ -45,11 +45,11 @@ func (ng *AlertNG) deleteAlertDefinitionByID(cmd *deleteAlertDefinitionByIDComma
}) })
} }
// getAlertDefinitionByID is a handler for retrieving an alert definition from that database by its ID. // getAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID.
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. // It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (ng *AlertNG) getAlertDefinitionByID(query *getAlertDefinitionByIDQuery) error { func (ng *AlertNG) getAlertDefinitionByUID(query *getAlertDefinitionByUIDQuery) error {
return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return ng.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinition, err := getAlertDefinitionByID(query.ID, sess) alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID)
if err != nil { if err != nil {
return err return err
} }
@ -118,42 +118,14 @@ func (ng *AlertNG) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error {
// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. // It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID.
func (ng *AlertNG) updateAlertDefinition(cmd *updateAlertDefinitionCommand) error { func (ng *AlertNG) updateAlertDefinition(cmd *updateAlertDefinitionCommand) error {
return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinition := &AlertDefinition{ existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID)
ID: cmd.ID,
Title: cmd.Title,
Condition: cmd.Condition.RefID,
Data: cmd.Condition.QueriesAndExpressions,
OrgID: cmd.OrgID,
}
if cmd.IntervalSeconds != nil {
alertDefinition.IntervalSeconds = *cmd.IntervalSeconds
}
if err := ng.validateAlertDefinition(alertDefinition, true); err != nil {
return err
}
if err := alertDefinition.preSave(); err != nil {
return err
}
existingAlertDefinition, err := getAlertDefinitionByID(alertDefinition.ID, sess)
if err != nil { if err != nil {
if errors.Is(err, errAlertDefinitionNotFound) { if errors.Is(err, errAlertDefinitionNotFound) {
cmd.Result = alertDefinition
cmd.RowsAffected = 0
return nil return nil
} }
return err return err
} }
alertDefinition.Version = existingAlertDefinition.Version + 1
affectedRows, err := sess.ID(cmd.ID).Update(alertDefinition)
if err != nil {
return err
}
title := cmd.Title title := cmd.Title
if title == "" { if title == "" {
title = existingAlertDefinition.Title title = existingAlertDefinition.Title
@ -171,30 +143,55 @@ func (ng *AlertNG) updateAlertDefinition(cmd *updateAlertDefinitionCommand) erro
intervalSeconds = &existingAlertDefinition.IntervalSeconds intervalSeconds = &existingAlertDefinition.IntervalSeconds
} }
// explicitly set all fields regardless of being provided or not
alertDefinition := &AlertDefinition{
ID: existingAlertDefinition.ID,
Title: title,
Condition: condition,
Data: data,
OrgID: existingAlertDefinition.OrgID,
IntervalSeconds: *intervalSeconds,
UID: existingAlertDefinition.UID,
}
if err := ng.validateAlertDefinition(alertDefinition, true); err != nil {
return err
}
if err := alertDefinition.preSave(); err != nil {
return err
}
alertDefinition.Version = existingAlertDefinition.Version + 1
_, err = sess.ID(existingAlertDefinition.ID).Update(alertDefinition)
if err != nil {
return err
}
alertDefVersion := AlertDefinitionVersion{ alertDefVersion := AlertDefinitionVersion{
AlertDefinitionID: alertDefinition.ID, AlertDefinitionID: alertDefinition.ID,
AlertDefinitionUID: existingAlertDefinition.UID, AlertDefinitionUID: alertDefinition.UID,
ParentVersion: existingAlertDefinition.Version, ParentVersion: alertDefinition.Version,
Version: alertDefinition.Version, Version: alertDefinition.Version,
Condition: condition, Condition: alertDefinition.Condition,
Created: alertDefinition.Updated, Created: alertDefinition.Updated,
Title: title, Title: alertDefinition.Title,
Data: data, Data: alertDefinition.Data,
IntervalSeconds: *intervalSeconds, IntervalSeconds: alertDefinition.IntervalSeconds,
} }
if _, err := sess.Insert(alertDefVersion); err != nil { if _, err := sess.Insert(alertDefVersion); err != nil {
return err return err
} }
cmd.Result = alertDefinition cmd.Result = alertDefinition
cmd.RowsAffected = affectedRows
return nil return nil
}) })
} }
// getOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation. // getOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation.
func (ng *AlertNG) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error { func (ng *AlertNG) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error {
return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return ng.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alertDefinitions := make([]*AlertDefinition, 0) alertDefinitions := make([]*AlertDefinition, 0)
q := "SELECT * FROM alert_definition WHERE org_id = ?" q := "SELECT * FROM alert_definition WHERE org_id = ?"
if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil { if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil {
@ -207,9 +204,9 @@ func (ng *AlertNG) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) erro
} }
func (ng *AlertNG) getAlertDefinitions(query *listAlertDefinitionsQuery) error { func (ng *AlertNG) getAlertDefinitions(query *listAlertDefinitionsQuery) error {
return ng.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return ng.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
alerts := make([]*AlertDefinition, 0) alerts := make([]*AlertDefinition, 0)
q := "SELECT id, interval_seconds, version FROM alert_definition" q := "SELECT uid, org_id, interval_seconds, version FROM alert_definition"
if err := sess.SQL(q).Find(&alerts); err != nil { if err := sess.SQL(q).Find(&alerts); err != nil {
return err return err
} }
@ -218,7 +215,6 @@ func (ng *AlertNG) getAlertDefinitions(query *listAlertDefinitionsQuery) error {
return nil return nil
}) })
} }
func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (string, error) { func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (string, error) {
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
uid := util.GenerateShortUID() uid := util.GenerateShortUID()

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -63,6 +64,8 @@ func TestCreatingAlertDefinition(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
ng := setupTestEnv(t) ng := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
q := saveAlertDefinitionCommand{ q := saveAlertDefinitionCommand{
OrgID: 1, OrgID: 1,
Title: tc.inputTitle, Title: tc.inputTitle,
@ -103,14 +106,15 @@ func TestCreatingAlertDefinition(t *testing.T) {
} }
func TestUpdatingAlertDefinition(t *testing.T) { func TestUpdatingAlertDefinition(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
t.Run("zero rows affected when updating unknown alert", func(t *testing.T) { t.Run("zero rows affected when updating unknown alert", func(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
ng := setupTestEnv(t) ng := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
q := updateAlertDefinitionCommand{ q := updateAlertDefinitionCommand{
ID: 1, UID: "unknown",
OrgID: 1, OrgID: 1,
Title: "something completely different", Title: "something completely different",
Condition: eval.Condition{ Condition: eval.Condition{
@ -134,11 +138,15 @@ func TestUpdatingAlertDefinition(t *testing.T) {
err := ng.updateAlertDefinition(&q) err := ng.updateAlertDefinition(&q)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(0), q.RowsAffected)
}) })
t.Run("updating existing alert", func(t *testing.T) { t.Run("updating existing alert", func(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
ng := setupTestEnv(t) ng := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
var initialInterval int64 = 120 var initialInterval int64 = 120
alertDefinition := createTestAlertDefinition(t, ng, initialInterval) alertDefinition := createTestAlertDefinition(t, ng, initialInterval)
created := alertDefinition.Updated created := alertDefinition.Updated
@ -159,7 +167,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
inputOrgID: alertDefinition.OrgID, inputOrgID: alertDefinition.OrgID,
inputTitle: "something completely different", inputTitle: "something completely different",
expectedIntervalSeconds: initialInterval, expectedIntervalSeconds: initialInterval,
expectedUpdated: time.Unix(2, 0).UTC(), expectedUpdated: time.Unix(1, 0).UTC(),
}, },
{ {
desc: "should update interval if it's provided", desc: "should update interval if it's provided",
@ -167,7 +175,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
inputOrgID: alertDefinition.OrgID, inputOrgID: alertDefinition.OrgID,
inputTitle: "something completely different", inputTitle: "something completely different",
expectedIntervalSeconds: customInterval, expectedIntervalSeconds: customInterval,
expectedUpdated: time.Unix(3, 0).UTC(), expectedUpdated: time.Unix(2, 0).UTC(),
}, },
{ {
desc: "should not update organisation if it's provided", desc: "should not update organisation if it's provided",
@ -175,7 +183,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
inputOrgID: 0, inputOrgID: 0,
inputTitle: "something completely different", inputTitle: "something completely different",
expectedIntervalSeconds: customInterval, expectedIntervalSeconds: customInterval,
expectedUpdated: time.Unix(4, 0).UTC(), expectedUpdated: time.Unix(3, 0).UTC(),
}, },
{ {
desc: "should not update alert definition if the name it's too big", desc: "should not update alert definition if the name it's too big",
@ -187,7 +195,7 @@ func TestUpdatingAlertDefinition(t *testing.T) {
} }
q := updateAlertDefinitionCommand{ q := updateAlertDefinitionCommand{
ID: (*alertDefinition).ID, UID: (*alertDefinition).UID,
Title: "something completely different", Title: "something completely different",
Condition: eval.Condition{ Condition: eval.Condition{
RefID: "B", RefID: "B",
@ -224,20 +232,16 @@ func TestUpdatingAlertDefinition(t *testing.T) {
case tc.expectedError != nil: case tc.expectedError != nil:
require.Error(t, err) require.Error(t, err)
getAlertDefinitionByIDQuery := getAlertDefinitionByIDQuery{ID: (*alertDefinition).ID} assert.Equal(t, previousAlertDefinition.Title, q.Result.Title)
err = ng.getAlertDefinitionByID(&getAlertDefinitionByIDQuery) assert.Equal(t, previousAlertDefinition.Condition, q.Result.Condition)
require.NoError(t, err) assert.Equal(t, len(previousAlertDefinition.Data), len(q.Result.Data))
assert.Equal(t, previousAlertDefinition.Title, getAlertDefinitionByIDQuery.Result.Title) assert.Equal(t, previousAlertDefinition.IntervalSeconds, q.Result.IntervalSeconds)
assert.Equal(t, previousAlertDefinition.Condition, getAlertDefinitionByIDQuery.Result.Condition) assert.Equal(t, previousAlertDefinition.Updated, q.Result.Updated)
assert.Equal(t, len(previousAlertDefinition.Data), len(getAlertDefinitionByIDQuery.Result.Data)) assert.Equal(t, previousAlertDefinition.Version, q.Result.Version)
assert.Equal(t, previousAlertDefinition.IntervalSeconds, getAlertDefinitionByIDQuery.Result.IntervalSeconds) assert.Equal(t, previousAlertDefinition.OrgID, q.Result.OrgID)
assert.Equal(t, previousAlertDefinition.Updated, getAlertDefinitionByIDQuery.Result.Updated) assert.Equal(t, previousAlertDefinition.UID, q.Result.UID)
assert.Equal(t, previousAlertDefinition.Version, getAlertDefinitionByIDQuery.Result.Version)
assert.Equal(t, previousAlertDefinition.OrgID, getAlertDefinitionByIDQuery.Result.OrgID)
assert.Equal(t, previousAlertDefinition.UID, getAlertDefinitionByIDQuery.Result.UID)
default: default:
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(1), q.RowsAffected)
assert.Equal(t, int64(1), q.Result.ID) assert.Equal(t, int64(1), q.Result.ID)
assert.True(t, q.Result.Updated.After(lastUpdated)) assert.True(t, q.Result.Updated.After(lastUpdated))
assert.Equal(t, tc.expectedUpdated, q.Result.Updated) assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
@ -245,19 +249,16 @@ func TestUpdatingAlertDefinition(t *testing.T) {
assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID) assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID)
getAlertDefinitionByIDQuery := getAlertDefinitionByIDQuery{ID: (*alertDefinition).ID} assert.Equal(t, "something completely different", q.Result.Title)
err = ng.getAlertDefinitionByID(&getAlertDefinitionByIDQuery) assert.Equal(t, "B", q.Result.Condition)
require.NoError(t, err) assert.Equal(t, 1, len(q.Result.Data))
assert.Equal(t, "something completely different", getAlertDefinitionByIDQuery.Result.Title) assert.Equal(t, tc.expectedUpdated, q.Result.Updated)
assert.Equal(t, "B", getAlertDefinitionByIDQuery.Result.Condition) assert.Equal(t, tc.expectedIntervalSeconds, q.Result.IntervalSeconds)
assert.Equal(t, 1, len(getAlertDefinitionByIDQuery.Result.Data)) assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version)
assert.Equal(t, tc.expectedUpdated, getAlertDefinitionByIDQuery.Result.Updated) assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID)
assert.Equal(t, tc.expectedIntervalSeconds, getAlertDefinitionByIDQuery.Result.IntervalSeconds) assert.Equal(t, alertDefinition.UID, q.Result.UID)
assert.Equal(t, previousAlertDefinition.Version+1, getAlertDefinitionByIDQuery.Result.Version)
assert.Equal(t, alertDefinition.OrgID, getAlertDefinitionByIDQuery.Result.OrgID)
assert.Equal(t, alertDefinition.UID, getAlertDefinitionByIDQuery.Result.UID)
previousAlertDefinition = getAlertDefinitionByIDQuery.Result previousAlertDefinition = q.Result
} }
}) })
@ -269,29 +270,30 @@ func TestUpdatingAlertDefinition(t *testing.T) {
func TestDeletingAlertDefinition(t *testing.T) { func TestDeletingAlertDefinition(t *testing.T) {
t.Run("zero rows affected when deleting unknown alert", func(t *testing.T) { t.Run("zero rows affected when deleting unknown alert", func(t *testing.T) {
ng := setupTestEnv(t) ng := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
q := deleteAlertDefinitionByIDCommand{ q := deleteAlertDefinitionByUIDCommand{
ID: 1, UID: "unknown",
OrgID: 1, OrgID: 1,
} }
err := ng.deleteAlertDefinitionByID(&q) err := ng.deleteAlertDefinitionByUID(&q)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(0), q.RowsAffected)
}) })
t.Run("deleting successfully existing alert", func(t *testing.T) { t.Run("deleting successfully existing alert", func(t *testing.T) {
ng := setupTestEnv(t) ng := setupTestEnv(t)
t.Cleanup(registry.ClearOverrides)
alertDefinition := createTestAlertDefinition(t, ng, 60) alertDefinition := createTestAlertDefinition(t, ng, 60)
q := deleteAlertDefinitionByIDCommand{ q := deleteAlertDefinitionByUIDCommand{
ID: (*alertDefinition).ID, UID: (*alertDefinition).UID,
OrgID: 1, OrgID: 1,
} }
err := ng.deleteAlertDefinitionByID(&q) err := ng.deleteAlertDefinitionByUID(&q)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(1), q.RowsAffected)
}) })
} }

View File

@ -5,10 +5,10 @@ import (
) )
func (ng *AlertNG) validateOrgAlertDefinition(c *models.ReqContext) { func (ng *AlertNG) validateOrgAlertDefinition(c *models.ReqContext) {
id := c.ParamsInt64(":alertDefinitionId") uid := c.ParamsEscape(":alertDefinitionUID")
query := getAlertDefinitionByIDQuery{ID: id} query := getAlertDefinitionByUIDQuery{UID: uid, OrgID: c.SignedInUser.OrgId}
if err := ng.getAlertDefinitionByID(&query); err != nil { if err := ng.getAlertDefinitionByUID(&query); err != nil {
c.JsonApiErr(404, "Alert definition not found", nil) c.JsonApiErr(404, "Alert definition not found", nil)
return return
} }

View File

@ -23,6 +23,19 @@ type AlertDefinition struct {
UID string `xorm:"uid"` UID string `xorm:"uid"`
} }
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. // AlertDefinitionVersion is the model for alert definition versions in Alerting NG.
type AlertDefinitionVersion struct { type AlertDefinitionVersion struct {
ID int64 `xorm:"pk autoincr 'id'"` ID int64 `xorm:"pk autoincr 'id'"`
@ -44,19 +57,17 @@ var (
errAlertDefinitionNotFound = fmt.Errorf("could not find alert definition") errAlertDefinitionNotFound = fmt.Errorf("could not find alert definition")
) )
// getAlertDefinitionByIDQuery is the query for retrieving/deleting an alert definition by ID. // getAlertDefinitionByUIDQuery is the query for retrieving/deleting an alert definition by UID and organisation ID.
type getAlertDefinitionByIDQuery struct { type getAlertDefinitionByUIDQuery struct {
ID int64 UID string
OrgID int64 OrgID int64
Result *AlertDefinition Result *AlertDefinition
} }
type deleteAlertDefinitionByIDCommand struct { type deleteAlertDefinitionByUIDCommand struct {
ID int64 UID string
OrgID int64 OrgID int64
RowsAffected int64
} }
// saveAlertDefinitionCommand is the query for saving a new alert definition. // saveAlertDefinitionCommand is the query for saving a new alert definition.
@ -71,15 +82,13 @@ type saveAlertDefinitionCommand struct {
// updateAlertDefinitionCommand is the query for updating an existing alert definition. // updateAlertDefinitionCommand is the query for updating an existing alert definition.
type updateAlertDefinitionCommand struct { type updateAlertDefinitionCommand struct {
ID int64 `json:"-"`
Title string `json:"title"` Title string `json:"title"`
OrgID int64 `json:"-"` OrgID int64 `json:"-"`
Condition eval.Condition `json:"condition"` Condition eval.Condition `json:"condition"`
IntervalSeconds *int64 `json:"interval_seconds"` IntervalSeconds *int64 `json:"interval_seconds"`
UID string `json:"-"` UID string `json:"-"`
RowsAffected int64 Result *AlertDefinition
Result *AlertDefinition
} }
type evalAlertConditionCommand struct { type evalAlertConditionCommand struct {

View File

@ -77,12 +77,12 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) {
} }
// LoadAlertCondition returns a Condition object for the given alertDefinitionID. // LoadAlertCondition returns a Condition object for the given alertDefinitionID.
func (ng *AlertNG) LoadAlertCondition(alertDefinitionID int64) (*eval.Condition, error) { func (ng *AlertNG) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*eval.Condition, error) {
getAlertDefinitionByIDQuery := getAlertDefinitionByIDQuery{ID: alertDefinitionID} q := getAlertDefinitionByUIDQuery{UID: alertDefinitionUID, OrgID: orgID}
if err := ng.getAlertDefinitionByID(&getAlertDefinitionByIDQuery); err != nil { if err := ng.getAlertDefinitionByUID(&q); err != nil {
return nil, err return nil, err
} }
alertDefinition := getAlertDefinitionByIDQuery.Result alertDefinition := q.Result
err := ng.validateAlertDefinition(alertDefinition, true) err := ng.validateAlertDefinition(alertDefinition, true)
if err != nil { if err != nil {

View File

@ -13,8 +13,8 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, definitionID int64, evalCh <-chan *evalContext) error { func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, key alertDefinitionKey, evalCh <-chan *evalContext) error {
ng.log.Debug("alert definition routine started", "definitionID", definitionID) ng.log.Debug("alert definition routine started", "key", key)
evalRunning := false evalRunning := false
var start, end time.Time var start, end time.Time
@ -32,14 +32,14 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, definitionID in
// fetch latest alert definition version // fetch latest alert definition version
if alertDefinition == nil || alertDefinition.Version < ctx.version { if alertDefinition == nil || alertDefinition.Version < ctx.version {
q := getAlertDefinitionByIDQuery{ID: definitionID} q := getAlertDefinitionByUIDQuery{OrgID: key.orgID, UID: key.definitionUID}
err := ng.getAlertDefinitionByID(&q) err := ng.getAlertDefinitionByUID(&q)
if err != nil { if err != nil {
ng.schedule.log.Error("failed to fetch alert definition", "alertDefinitionID", alertDefinition.ID) ng.schedule.log.Error("failed to fetch alert definition", "key", key)
return err return err
} }
alertDefinition = q.Result alertDefinition = q.Result
ng.schedule.log.Debug("new alert definition version fetched", "alertDefinitionID", alertDefinition.ID, "version", alertDefinition.Version) ng.schedule.log.Debug("new alert definition version fetched", "key", key, "version", alertDefinition.Version)
} }
condition := eval.Condition{ condition := eval.Condition{
@ -50,11 +50,11 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, definitionID in
results, err := eval.ConditionEval(&condition, ctx.now) results, err := eval.ConditionEval(&condition, ctx.now)
end = timeNow() end = timeNow()
if err != nil { if err != nil {
ng.schedule.log.Error("failed to evaluate alert definition", "definitionID", definitionID, "attempt", attempt, "now", ctx.now, "duration", end.Sub(start), "error", err) ng.schedule.log.Error("failed to evaluate alert definition", "key", key, "attempt", attempt, "now", ctx.now, "duration", end.Sub(start), "error", err)
return err return err
} }
for _, r := range results { for _, r := range results {
ng.schedule.log.Info("alert definition result", "definitionID", definitionID, "attempt", attempt, "now", ctx.now, "duration", end.Sub(start), "instance", r.Instance, "state", r.State.String()) ng.schedule.log.Info("alert definition result", "key", key, "attempt", attempt, "now", ctx.now, "duration", end.Sub(start), "instance", r.Instance, "state", r.State.String())
} }
return nil return nil
} }
@ -64,7 +64,7 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, definitionID in
defer func() { defer func() {
evalRunning = false evalRunning = false
if ng.schedule.evalApplied != nil { if ng.schedule.evalApplied != nil {
ng.schedule.evalApplied(definitionID, ctx.now) ng.schedule.evalApplied(key, ctx.now)
} }
}() }()
@ -75,9 +75,9 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, definitionID in
} }
} }
}() }()
case id := <-ng.schedule.stop: case k := <-ng.schedule.stop:
if id == definitionID { if k == key {
ng.schedule.log.Debug("stopping alert definition routine", "definitionID", definitionID) ng.schedule.log.Debug("stopping alert definition routine", "key", key)
// interrupt evaluation if it's running // interrupt evaluation if it's running
return nil return nil
} }
@ -95,7 +95,7 @@ type schedule struct {
registry alertDefinitionRegistry registry alertDefinitionRegistry
// broadcast channel for stopping definition routines // broadcast channel for stopping definition routines
stop chan int64 stop chan alertDefinitionKey
maxAttempts int64 maxAttempts int64
@ -106,17 +106,17 @@ type schedule struct {
// evalApplied is only used for tests: test code can set it to non-nil // evalApplied is only used for tests: test code can set it to non-nil
// function, and then it'll be called from the event loop whenever the // function, and then it'll be called from the event loop whenever the
// message from evalApplied is handled. // message from evalApplied is handled.
evalApplied func(int64, time.Time) evalApplied func(alertDefinitionKey, time.Time)
log log.Logger log log.Logger
} }
// newScheduler returns a new schedule. // newScheduler returns a new schedule.
func newScheduler(c clock.Clock, baseInterval time.Duration, logger log.Logger, evalApplied func(int64, time.Time)) *schedule { func newScheduler(c clock.Clock, baseInterval time.Duration, logger log.Logger, evalApplied func(alertDefinitionKey, time.Time)) *schedule {
ticker := alerting.NewTicker(c.Now(), time.Second*0, c, int64(baseInterval.Seconds())) ticker := alerting.NewTicker(c.Now(), time.Second*0, c, int64(baseInterval.Seconds()))
sch := schedule{ sch := schedule{
registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[int64]alertDefinitionInfo)}, registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)},
stop: make(chan int64), stop: make(chan alertDefinitionKey),
maxAttempts: maxAttempts, maxAttempts: maxAttempts,
clock: c, clock: c,
baseInterval: baseInterval, baseInterval: baseInterval,
@ -161,37 +161,39 @@ func (ng *AlertNG) alertingTicker(grafanaCtx context.Context) error {
registeredDefinitions := ng.schedule.registry.keyMap() registeredDefinitions := ng.schedule.registry.keyMap()
type readyToRunItem struct { type readyToRunItem struct {
id int64 key alertDefinitionKey
definitionInfo alertDefinitionInfo definitionInfo alertDefinitionInfo
} }
readyToRun := make([]readyToRunItem, 0) readyToRun := make([]readyToRunItem, 0)
for _, item := range alertDefinitions { for _, item := range alertDefinitions {
itemID := item.ID itemUID := item.UID
itemOrgID := item.OrgID
key := item.getKey()
itemVersion := item.Version itemVersion := item.Version
newRoutine := !ng.schedule.registry.exists(itemID) newRoutine := !ng.schedule.registry.exists(key)
definitionInfo := ng.schedule.registry.getOrCreateInfo(itemID, itemVersion) definitionInfo := ng.schedule.registry.getOrCreateInfo(key, itemVersion)
invalidInterval := item.IntervalSeconds%int64(ng.schedule.baseInterval.Seconds()) != 0 invalidInterval := item.IntervalSeconds%int64(ng.schedule.baseInterval.Seconds()) != 0
if newRoutine && !invalidInterval { if newRoutine && !invalidInterval {
dispatcherGroup.Go(func() error { dispatcherGroup.Go(func() error {
return ng.definitionRoutine(ctx, itemID, definitionInfo.ch) return ng.definitionRoutine(ctx, key, definitionInfo.ch)
}) })
} }
if invalidInterval { if invalidInterval {
// this is expected to be always false // this is expected to be always false
// give that we validate interval during alert definition updates // give that we validate interval during alert definition updates
ng.schedule.log.Debug("alert definition with invalid interval will be ignored: interval should be divided exactly by scheduler interval", "definitionID", itemID, "interval", time.Duration(item.IntervalSeconds)*time.Second, "scheduler interval", ng.schedule.baseInterval) ng.schedule.log.Debug("alert definition with invalid interval will be ignored: interval should be divided exactly by scheduler interval", "definitionUID", itemUID, "orgID", itemOrgID, "interval", time.Duration(item.IntervalSeconds)*time.Second, "scheduler interval", ng.schedule.baseInterval)
continue continue
} }
itemFrequency := item.IntervalSeconds / int64(ng.schedule.baseInterval.Seconds()) itemFrequency := item.IntervalSeconds / int64(ng.schedule.baseInterval.Seconds())
if item.IntervalSeconds != 0 && tickNum%itemFrequency == 0 { if item.IntervalSeconds != 0 && tickNum%itemFrequency == 0 {
readyToRun = append(readyToRun, readyToRunItem{id: itemID, definitionInfo: definitionInfo}) readyToRun = append(readyToRun, readyToRunItem{key: key, definitionInfo: definitionInfo})
} }
// remove the alert definition from the registered alert definitions // remove the alert definition from the registered alert definitions
delete(registeredDefinitions, itemID) delete(registeredDefinitions, key)
} }
var step int64 = 0 var step int64 = 0
@ -208,9 +210,9 @@ func (ng *AlertNG) alertingTicker(grafanaCtx context.Context) error {
} }
// unregister and stop routines of the deleted alert definitions // unregister and stop routines of the deleted alert definitions
for id := range registeredDefinitions { for key := range registeredDefinitions {
ng.schedule.stop <- id ng.schedule.stop <- key
ng.schedule.registry.del(id) ng.schedule.registry.del(key)
} }
case <-grafanaCtx.Done(): case <-grafanaCtx.Done():
err := dispatcherGroup.Wait() err := dispatcherGroup.Wait()
@ -221,42 +223,42 @@ func (ng *AlertNG) alertingTicker(grafanaCtx context.Context) error {
type alertDefinitionRegistry struct { type alertDefinitionRegistry struct {
mu sync.Mutex mu sync.Mutex
alertDefinitionInfo map[int64]alertDefinitionInfo alertDefinitionInfo map[alertDefinitionKey]alertDefinitionInfo
} }
// getOrCreateInfo returns the channel for the specific alert definition // getOrCreateInfo returns the channel for the specific alert definition
// if it does not exists creates one and returns it // if it does not exists creates one and returns it
func (r *alertDefinitionRegistry) getOrCreateInfo(definitionID int64, definitionVersion int64) alertDefinitionInfo { func (r *alertDefinitionRegistry) getOrCreateInfo(key alertDefinitionKey, definitionVersion int64) alertDefinitionInfo {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
info, ok := r.alertDefinitionInfo[definitionID] info, ok := r.alertDefinitionInfo[key]
if !ok { if !ok {
r.alertDefinitionInfo[definitionID] = alertDefinitionInfo{ch: make(chan *evalContext), version: definitionVersion} r.alertDefinitionInfo[key] = alertDefinitionInfo{ch: make(chan *evalContext), version: definitionVersion}
return r.alertDefinitionInfo[definitionID] return r.alertDefinitionInfo[key]
} }
info.version = definitionVersion info.version = definitionVersion
r.alertDefinitionInfo[definitionID] = info r.alertDefinitionInfo[key] = info
return info return info
} }
func (r *alertDefinitionRegistry) exists(definitionID int64) bool { func (r *alertDefinitionRegistry) exists(key alertDefinitionKey) bool {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
_, ok := r.alertDefinitionInfo[definitionID] _, ok := r.alertDefinitionInfo[key]
return ok return ok
} }
func (r *alertDefinitionRegistry) del(definitionID int64) { func (r *alertDefinitionRegistry) del(key alertDefinitionKey) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
delete(r.alertDefinitionInfo, definitionID) delete(r.alertDefinitionInfo, key)
} }
func (r *alertDefinitionRegistry) iter() <-chan int64 { func (r *alertDefinitionRegistry) iter() <-chan alertDefinitionKey {
c := make(chan int64) c := make(chan alertDefinitionKey)
f := func() { f := func() {
r.mu.Lock() r.mu.Lock()
@ -272,10 +274,10 @@ func (r *alertDefinitionRegistry) iter() <-chan int64 {
return c return c
} }
func (r *alertDefinitionRegistry) keyMap() map[int64]struct{} { func (r *alertDefinitionRegistry) keyMap() map[alertDefinitionKey]struct{} {
definitionsIDs := make(map[int64]struct{}) definitionsIDs := make(map[alertDefinitionKey]struct{})
for definitionID := range r.iter() { for k := range r.iter() {
definitionsIDs[definitionID] = struct{}{} definitionsIDs[k] = struct{}{}
} }
return definitionsIDs return definitionsIDs
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"runtime" "runtime"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -18,8 +17,8 @@ import (
) )
type evalAppliedInfo struct { type evalAppliedInfo struct {
alertDefID int64 alertDefKey alertDefinitionKey
now time.Time now time.Time
} }
func TestAlertingTicker(t *testing.T) { func TestAlertingTicker(t *testing.T) {
@ -39,8 +38,8 @@ func TestAlertingTicker(t *testing.T) {
evalAppliedCh := make(chan evalAppliedInfo, len(alerts)) evalAppliedCh := make(chan evalAppliedInfo, len(alerts))
ng.schedule.evalApplied = func(alertDefID int64, now time.Time) { ng.schedule.evalApplied = func(alertDefKey alertDefinitionKey, now time.Time) {
evalAppliedCh <- evalAppliedInfo{alertDefID: alertDefID, now: now} evalAppliedCh <- evalAppliedInfo{alertDefKey: alertDefKey, now: now}
} }
ctx := context.Background() ctx := context.Background()
@ -50,7 +49,7 @@ func TestAlertingTicker(t *testing.T) {
}() }()
runtime.Gosched() runtime.Gosched()
expectedAlertDefinitionsEvaluated := []int64{alerts[1].ID} expectedAlertDefinitionsEvaluated := []alertDefinitionKey{alerts[1].getKey()}
t.Run(fmt.Sprintf("on 1st tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 1st tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
@ -59,42 +58,42 @@ func TestAlertingTicker(t *testing.T) {
// change alert definition interval to three seconds // change alert definition interval to three seconds
var threeSecInterval int64 = 3 var threeSecInterval int64 = 3
err := ng.updateAlertDefinition(&updateAlertDefinitionCommand{ err := ng.updateAlertDefinition(&updateAlertDefinitionCommand{
ID: alerts[0].ID, UID: alerts[0].UID,
IntervalSeconds: &threeSecInterval, IntervalSeconds: &threeSecInterval,
OrgID: alerts[0].OrgID, OrgID: alerts[0].OrgID,
}) })
require.NoError(t, err) require.NoError(t, err)
t.Logf("alert definition: %d interval reset to: %d", alerts[0].ID, threeSecInterval) t.Logf("alert definition: %v interval reset to: %d", alerts[0].getKey(), threeSecInterval)
expectedAlertDefinitionsEvaluated = []int64{alerts[1].ID} expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[1].getKey()}
t.Run(fmt.Sprintf("on 2nd tick alert definition: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 2nd tick alert definition: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
expectedAlertDefinitionsEvaluated = []int64{alerts[1].ID, alerts[0].ID} expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[1].getKey(), alerts[0].getKey()}
t.Run(fmt.Sprintf("on 3rd tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 3rd tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
expectedAlertDefinitionsEvaluated = []int64{alerts[1].ID} expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[1].getKey()}
t.Run(fmt.Sprintf("on 4th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 4th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
err = ng.deleteAlertDefinitionByID(&deleteAlertDefinitionByIDCommand{ID: alerts[1].ID}) err = ng.deleteAlertDefinitionByUID(&deleteAlertDefinitionByUIDCommand{UID: alerts[1].UID, OrgID: alerts[1].OrgID})
require.NoError(t, err) require.NoError(t, err)
t.Logf("alert definition: %d deleted", alerts[1].ID) t.Logf("alert definition: %v deleted", alerts[1].getKey())
expectedAlertDefinitionsEvaluated = []int64{} expectedAlertDefinitionsEvaluated = []alertDefinitionKey{}
t.Run(fmt.Sprintf("on 5th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 5th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
expectedAlertDefinitionsEvaluated = []int64{alerts[0].ID} expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[0].getKey()}
t.Run(fmt.Sprintf("on 6th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 6th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
@ -103,29 +102,29 @@ func TestAlertingTicker(t *testing.T) {
// create alert definition with one second interval // create alert definition with one second interval
alerts = append(alerts, createTestAlertDefinition(t, ng, 1)) alerts = append(alerts, createTestAlertDefinition(t, ng, 1))
expectedAlertDefinitionsEvaluated = []int64{alerts[2].ID} expectedAlertDefinitionsEvaluated = []alertDefinitionKey{alerts[2].getKey()}
t.Run(fmt.Sprintf("on 7th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) { t.Run(fmt.Sprintf("on 7th tick alert definitions: %s should be evaluated", concatenate(expectedAlertDefinitionsEvaluated)), func(t *testing.T) {
tick := advanceClock(t, mockedClock) tick := advanceClock(t, mockedClock)
assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...) assertEvalRun(t, evalAppliedCh, tick, expectedAlertDefinitionsEvaluated...)
}) })
} }
func assertEvalRun(t *testing.T, ch <-chan evalAppliedInfo, tick time.Time, ids ...int64) { func assertEvalRun(t *testing.T, ch <-chan evalAppliedInfo, tick time.Time, keys ...alertDefinitionKey) {
timeout := time.After(time.Second) timeout := time.After(time.Second)
expected := make(map[int64]struct{}, len(ids)) expected := make(map[alertDefinitionKey]struct{}, len(keys))
for _, id := range ids { for _, k := range keys {
expected[id] = struct{}{} expected[k] = struct{}{}
} }
for { for {
select { select {
case info := <-ch: case info := <-ch:
_, ok := expected[info.alertDefID] _, ok := expected[info.alertDefKey]
t.Logf("alert definition: %d evaluated at: %v", info.alertDefID, info.now) t.Logf("alert definition: %v evaluated at: %v", info.alertDefKey, info.now)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, tick, info.now) assert.Equal(t, tick, info.now)
delete(expected, info.alertDefID) delete(expected, info.alertDefKey)
if len(expected) == 0 { if len(expected) == 0 {
return return
} }
@ -144,10 +143,10 @@ func advanceClock(t *testing.T, mockedClock *clock.Mock) time.Time {
// t.Logf("Tick: %v", mockedClock.Now()) // t.Logf("Tick: %v", mockedClock.Now())
} }
func concatenate(ids []int64) string { func concatenate(keys []alertDefinitionKey) string {
s := make([]string, len(ids)) s := make([]string, len(keys))
for _, id := range ids { for _, k := range keys {
s = append(s, strconv.FormatInt(id, 10)) s = append(s, k.String())
} }
return fmt.Sprintf("[%s]", strings.TrimLeft(strings.Join(s, ","), ",")) return fmt.Sprintf("[%s]", strings.TrimLeft(strings.Join(s, ","), ","))
} }