Chore: Sql store split for legacy alerting (#52901)

Moves ~20 sqlstore methods for legacy alerting out of sqlstore (sqlstore.Store interface) and into alerting.
This commit is contained in:
Kyle Brandt
2022-08-03 11:17:26 -04:00
committed by GitHub
parent 46b7ca12e1
commit 643d2bc890
39 changed files with 483 additions and 474 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ type UsageStatsQuerier interface {
// configured in Grafana.
func (e *AlertEngine) QueryUsageStats(ctx context.Context) (*UsageStats, error) {
cmd := &models.GetAllAlertsQuery{}
err := e.sqlStore.GetAllAlertQueryHandler(ctx, cmd)
err := e.AlertStore.GetAllAlertQueryHandler(ctx, cmd)
if err != nil {
return nil, err
}
@@ -64,7 +64,7 @@ func (e *AlertEngine) mapRulesToUsageStats(ctx context.Context, rules []*models.
result := map[string]int{}
for k, v := range typeCount {
query := &datasources.GetDataSourceQuery{Id: k}
err := e.sqlStore.GetDataSource(ctx, query)
err := e.datasourceService.GetDataSource(ctx, query)
if err != nil {
return map[string]int{}, nil
}
+11 -18
View File
@@ -12,12 +12,22 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
)
func TestAlertingUsageStats(t *testing.T) {
store := &AlertStoreMock{}
dsMock := &fd.FakeDataSourceService{
DataSources: []*datasources.DataSource{
{Id: 1, Type: datasources.DS_INFLUXDB},
{Id: 2, Type: datasources.DS_GRAPHITE},
{Id: 3, Type: datasources.DS_PROMETHEUS},
{Id: 4, Type: datasources.DS_PROMETHEUS},
},
}
ae := &AlertEngine{
sqlStore: store,
AlertStore: store,
datasourceService: dsMock,
}
store.getAllAlerts = func(ctx context.Context, query *models.GetAllAlertsQuery) error {
@@ -41,23 +51,6 @@ func TestAlertingUsageStats(t *testing.T) {
return nil
}
store.getDataSource = func(ctx context.Context, query *datasources.GetDataSourceQuery) error {
ds := map[int64]*datasources.DataSource{
1: {Type: "influxdb"},
2: {Type: "graphite"},
3: {Type: "prometheus"},
4: {Type: "prometheus"},
}
r, exist := ds[query.Id]
if !exist {
return datasources.ErrDataSourceNotFound
}
query.Result = r
return nil
}
result, err := ae.QueryUsageStats(context.Background())
require.NoError(t, err, "getAlertingUsage should not return error")
+1 -1
View File
@@ -147,7 +147,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange l
OrgId: context.Rule.OrgID,
}
if err := context.Store.GetDataSource(context.Ctx, getDsInfo); err != nil {
if err := context.GetDataSource(context.Ctx, getDsInfo); err != nil {
return nil, fmt.Errorf("could not find datasource: %w", err)
}
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/datasources"
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
@@ -138,7 +139,6 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo *
func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) {
t.Run("desc", func(t *testing.T) {
store := mockstore.NewSQLStoreMock()
store.ExpectedDatasource = &datasources.DataSource{Id: 1, Type: "graphite", JsonData: dataSourceJsonData}
ctx := &queryIntervalTestContext{}
ctx.result = &alerting.EvalContext{
@@ -146,6 +146,11 @@ func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejso
Rule: &alerting.Rule{},
RequestValidator: &validations.OSSPluginRequestValidator{},
Store: store,
DatasourceService: &fd.FakeDataSourceService{
DataSources: []*datasources.DataSource{
{Id: 1, Type: datasources.DS_GRAPHITE, JsonData: dataSourceJsonData},
},
},
}
jsonModel, err := simplejson.NewJson([]byte(`{
@@ -7,6 +7,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/services/datasources"
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
@@ -37,7 +38,6 @@ func TestQueryCondition(t *testing.T) {
setup := func() *queryConditionTestContext {
ctx := &queryConditionTestContext{}
store := mockstore.NewSQLStoreMock()
store.ExpectedDatasource = &datasources.DataSource{Id: 1, Type: "graphite"}
ctx.reducer = `{"type":"avg"}`
ctx.evaluator = `{"type":"gt","params":[100]}`
@@ -46,6 +46,11 @@ func TestQueryCondition(t *testing.T) {
Rule: &alerting.Rule{},
RequestValidator: &validations.OSSPluginRequestValidator{},
Store: store,
DatasourceService: &fd.FakeDataSourceService{
DataSources: []*datasources.DataSource{
{Id: 1, Type: datasources.DS_GRAPHITE},
},
},
}
return ctx
}
+10 -20
View File
@@ -11,6 +11,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
@@ -25,19 +26,6 @@ import (
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
// AlertStore is a subset of SQLStore API to satisfy the needs of the alerting service.
// A subset is needed to make it easier to mock during the tests.
type AlertStore interface {
GetAllAlertQueryHandler(context.Context, *models.GetAllAlertsQuery) error
GetDataSource(context.Context, *datasources.GetDataSourceQuery) error
SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error
SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error
GetAlertNotificationUidWithId(context.Context, *models.GetAlertNotificationUidQuery) error
GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error
GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error
SetAlertState(context.Context, *models.SetAlertStateCommand) error
}
// AlertEngine is the background process that
// schedules alert evaluations and makes sure notifications
// are sent.
@@ -56,9 +44,10 @@ type AlertEngine struct {
resultHandler resultHandler
usageStatsService usagestats.Service
tracer tracing.Tracer
sqlStore AlertStore
AlertStore AlertStore
dashAlertExtractor DashAlertExtractor
dashboardService dashboards.DashboardService
datasourceService datasources.DataSourceService
}
// IsDisabled returns true if the alerting service is disabled for this instance.
@@ -69,8 +58,8 @@ func (e *AlertEngine) IsDisabled() bool {
// ProvideAlertEngine returns a new AlertEngine.
func ProvideAlertEngine(renderer rendering.Service, requestValidator models.PluginRequestValidator,
dataService legacydata.RequestHandler, usageStatsService usagestats.Service, encryptionService encryption.Internal,
notificationService *notifications.NotificationService, tracer tracing.Tracer, sqlStore AlertStore, cfg *setting.Cfg,
dashAlertExtractor DashAlertExtractor, dashboardService dashboards.DashboardService) *AlertEngine {
notificationService *notifications.NotificationService, tracer tracing.Tracer, store AlertStore, cfg *setting.Cfg,
dashAlertExtractor DashAlertExtractor, dashboardService dashboards.DashboardService, cacheService *localcache.CacheService, dsService datasources.DataSourceService) *AlertEngine {
e := &AlertEngine{
Cfg: cfg,
RenderService: renderer,
@@ -78,16 +67,17 @@ func ProvideAlertEngine(renderer rendering.Service, requestValidator models.Plug
DataService: dataService,
usageStatsService: usageStatsService,
tracer: tracer,
sqlStore: sqlStore,
AlertStore: store,
dashAlertExtractor: dashAlertExtractor,
dashboardService: dashboardService,
datasourceService: dsService,
}
e.execQueue = make(chan *Job, 1000)
e.scheduler = newScheduler()
e.evalHandler = NewEvalHandler(e.DataService)
e.ruleReader = newRuleReader(sqlStore)
e.ruleReader = newRuleReader(store)
e.log = log.New("alerting.engine")
e.resultHandler = newResultHandler(e.RenderService, sqlStore, notificationService, encryptionService.GetDecryptedValue)
e.resultHandler = newResultHandler(e.RenderService, store, notificationService, encryptionService.GetDecryptedValue)
e.registerUsageMetrics()
@@ -203,7 +193,7 @@ func (e *AlertEngine) processJob(attemptID int, attemptChan chan int, cancelChan
alertCtx, cancelFn := context.WithTimeout(context.Background(), setting.AlertingEvaluationTimeout)
cancelChan <- cancelFn
alertCtx, span := e.tracer.Start(alertCtx, "alert execution")
evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator, e.sqlStore, e.dashboardService)
evalContext := NewEvalContext(alertCtx, job.Rule, e.RequestValidator, e.AlertStore, e.dashboardService, e.datasourceService)
evalContext.Ctx = alertCtx
go func() {
@@ -9,8 +9,10 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
datasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider"
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
"github.com/grafana/grafana/pkg/setting"
@@ -32,7 +34,8 @@ func TestIntegrationEngineTimeouts(t *testing.T) {
require.NoError(t, err)
tracer := tracing.InitializeTracerForTest()
engine := ProvideAlertEngine(nil, nil, nil, usMock, encService, nil, tracer, nil, cfg, nil, nil)
dsMock := &datasources.FakeDataSourceService{}
engine := ProvideAlertEngine(nil, nil, nil, usMock, encService, nil, tracer, nil, setting.NewCfg(), nil, nil, localcache.New(time.Minute, time.Minute), dsMock)
setting.AlertingNotificationTimeout = 30 * time.Second
setting.AlertingMaxAttempts = 3
engine.resultHandler = &FakeResultHandler{}
+23 -11
View File
@@ -8,10 +8,12 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider"
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
"github.com/grafana/grafana/pkg/setting"
@@ -46,15 +48,11 @@ func (handler *FakeResultHandler) handle(evalContext *EvalContext) error {
// A mock implementation of the AlertStore interface, allowing to override certain methods individually
type AlertStoreMock struct {
getAllAlerts func(context.Context, *models.GetAllAlertsQuery) error
getDataSource func(context.Context, *datasources.GetDataSourceQuery) error
getAlertNotificationsWithUidToSend func(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error
getOrCreateNotificationState func(ctx context.Context, query *models.GetOrCreateNotificationStateQuery) error
}
func (a *AlertStoreMock) GetDataSource(c context.Context, cmd *datasources.GetDataSourceQuery) error {
if a.getDataSource != nil {
return a.getDataSource(c, cmd)
}
func (a *AlertStoreMock) GetAlertById(c context.Context, cmd *models.GetAlertByIdQuery) error {
return nil
}
@@ -99,6 +97,22 @@ func (a *AlertStoreMock) SetAlertState(_ context.Context, _ *models.SetAlertStat
return nil
}
func (a *AlertStoreMock) GetAlertStatesForDashboard(_ context.Context, _ *models.GetAlertStatesForDashboardQuery) error {
return nil
}
func (a *AlertStoreMock) HandleAlertsQuery(context.Context, *models.GetAlertsQuery) error {
return nil
}
func (a *AlertStoreMock) PauseAlert(context.Context, *models.PauseAlertCommand) error {
return nil
}
func (a *AlertStoreMock) PauseAllAlerts(context.Context, *models.PauseAllAlertCommand) error {
return nil
}
func TestEngineProcessJob(t *testing.T) {
usMock := &usagestats.UsageStatsMock{T: t}
@@ -111,7 +125,10 @@ func TestEngineProcessJob(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
store := &AlertStoreMock{}
engine := ProvideAlertEngine(nil, nil, nil, usMock, encService, nil, tracer, store, cfg, nil, nil)
dsMock := &fd.FakeDataSourceService{
DataSources: []*datasources.DataSource{{Id: 1, Type: datasources.DS_PROMETHEUS}},
}
engine := ProvideAlertEngine(nil, nil, nil, usMock, encService, nil, tracer, store, setting.NewCfg(), nil, nil, localcache.New(time.Minute, time.Minute), dsMock)
setting.AlertingEvaluationTimeout = 30 * time.Second
setting.AlertingNotificationTimeout = 30 * time.Second
setting.AlertingMaxAttempts = 3
@@ -128,11 +145,6 @@ func TestEngineProcessJob(t *testing.T) {
return nil
}
store.getDataSource = func(ctx context.Context, q *datasources.GetDataSourceQuery) error {
q.Result = &datasources.DataSource{Id: 1, Type: datasources.DS_PROMETHEUS}
return nil
}
report, err := usMock.GetUsageReport(context.Background())
require.Nil(t, err)
+21 -14
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/setting"
)
@@ -38,25 +39,27 @@ type EvalContext struct {
Ctx context.Context
Store AlertStore
dashboardService dashboards.DashboardService
Store AlertStore
dashboardService dashboards.DashboardService
DatasourceService datasources.DataSourceService
}
// NewEvalContext is the EvalContext constructor.
func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator,
sqlStore AlertStore, dashboardService dashboards.DashboardService) *EvalContext {
alertStore AlertStore, dashboardService dashboards.DashboardService, dsService datasources.DataSourceService) *EvalContext {
return &EvalContext{
Ctx: alertCtx,
StartTime: time.Now(),
Rule: rule,
Logs: make([]*ResultLogEntry, 0),
EvalMatches: make([]*EvalMatch, 0),
AllMatches: make([]*EvalMatch, 0),
Log: log.New("alerting.evalContext"),
PrevAlertState: rule.State,
RequestValidator: requestValidator,
Store: sqlStore,
dashboardService: dashboardService,
Ctx: alertCtx,
StartTime: time.Now(),
Rule: rule,
Logs: make([]*ResultLogEntry, 0),
EvalMatches: make([]*EvalMatch, 0),
AllMatches: make([]*EvalMatch, 0),
Log: log.New("alerting.evalContext"),
PrevAlertState: rule.State,
RequestValidator: requestValidator,
Store: alertStore,
dashboardService: dashboardService,
DatasourceService: dsService,
}
}
@@ -220,6 +223,10 @@ func (c *EvalContext) evaluateNotificationTemplateFields() error {
return nil
}
func (c *EvalContext) GetDataSource(ctx context.Context, q *datasources.GetDataSourceQuery) error {
return c.DatasourceService.GetDataSource(ctx, q)
}
// getTemplateMatches returns the values we should use to parse the templates
func (c *EvalContext) getTemplateMatches() []*EvalMatch {
// EvalMatches represent series violating the rule threshold,
+3 -3
View File
@@ -14,7 +14,7 @@ import (
)
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil, nil)
ctx := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
t.Run("ok -> alerting", func(t *testing.T) {
ctx.PrevAlertState = models.AlertStateOK
@@ -199,7 +199,7 @@ func TestGetStateFromEvalContext(t *testing.T) {
}
for _, tc := range tcs {
evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil, nil)
evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
tc.applyFn(evalContext)
newState := evalContext.GetNewState()
@@ -391,7 +391,7 @@ func TestEvaluateNotificationTemplateFields(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
evalContext := NewEvalContext(context.Background(), &Rule{Name: "Rule name: ${value1}", Message: "Rule message: ${value2}",
Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil, nil)
Conditions: []Condition{&conditionStub{firing: true}}}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.EvalMatches = test.evalMatches
evalContext.AllMatches = test.allMatches
+14 -14
View File
@@ -29,7 +29,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
Conditions: []Condition{&conditionStub{
firing: true,
}},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -39,7 +39,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
t.Run("Show return triggered with single passing condition2", func(t *testing.T) {
context := NewEvalContext(context.Background(), &Rule{
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -52,7 +52,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
&conditionStub{firing: false, operator: "and"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -65,7 +65,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -78,7 +78,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -92,7 +92,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -106,7 +106,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -120,7 +120,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -134,7 +134,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, true, context.Firing)
@@ -148,7 +148,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -163,7 +163,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.False(t, context.NoDataFound)
@@ -174,7 +174,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
Conditions: []Condition{
&conditionStub{operator: "and", noData: true},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.Equal(t, false, context.Firing)
@@ -187,7 +187,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{operator: "and", noData: true},
&conditionStub{operator: "and", noData: false},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.True(t, context.NoDataFound)
@@ -199,7 +199,7 @@ func TestAlertingEvaluationHandler(t *testing.T) {
&conditionStub{operator: "or", noData: true},
&conditionStub{operator: "or", noData: false},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
handler.Eval(context)
require.True(t, context.NoDataFound)
+2 -2
View File
@@ -26,11 +26,11 @@ type DashAlertExtractorService struct {
log log.Logger
}
func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService, alertStore AlertStore) *DashAlertExtractorService {
func ProvideDashAlertExtractorService(datasourcePermissionsService permissions.DatasourcePermissionsService, datasourceService datasources.DataSourceService, store AlertStore) *DashAlertExtractorService {
return &DashAlertExtractorService{
datasourcePermissionsService: datasourcePermissionsService,
datasourceService: datasourceService,
alertStore: alertStore,
alertStore: store,
log: log.New("alerting.extractor"),
}
}
+1 -1
View File
@@ -200,7 +200,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
t.Run("Alert notifications are in DB", func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
sqlStore := sqlStore{db: sqlstore.InitTestDB(t)}
firstNotification := models.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
err = sqlStore.CreateAlertNotificationCommand(context.Background(), &firstNotification)
+3 -3
View File
@@ -20,18 +20,18 @@ import (
func TestNotificationService(t *testing.T) {
testRule := &Rule{Name: "Test", Message: "Something is bad"}
store := &AlertStoreMock{}
evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}, store, nil)
evalCtx := NewEvalContext(context.Background(), testRule, &validations.OSSPluginRequestValidator{}, store, nil, nil)
testRuleTemplated := &Rule{Name: "Test latency ${quantile}", Message: "Something is bad on instance ${instance}"}
evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store, nil)
evalCtxWithMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store, nil, nil)
evalCtxWithMatch.EvalMatches = []*EvalMatch{{
Tags: map[string]string{
"instance": "localhost:3000",
"quantile": "0.99",
},
}}
evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store, nil)
evalCtxWithoutMatch := NewEvalContext(context.Background(), testRuleTemplated, &validations.OSSPluginRequestValidator{}, store, nil, nil)
notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification",
evalCtx, true, func(sc *scenarioContext) {
@@ -68,7 +68,7 @@ func TestWhenAlertManagerShouldNotify(t *testing.T) {
am := &AlertmanagerNotifier{log: log.New("test.logger")}
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
State: tc.prevState,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.Rule.State = tc.newState
+1 -1
View File
@@ -170,7 +170,7 @@ func TestShouldSendAlertNotification(t *testing.T) {
for _, tc := range tcs {
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
State: tc.prevState,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
if tc.state == nil {
tc.state = &models.AlertNotificationState{}
@@ -52,7 +52,7 @@ func TestDingDingNotifier(t *testing.T) {
&alerting.Rule{
State: models.AlertStateAlerting,
Message: `{host="localhost"}`,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
_, err = notifier.genBody(evalContext, "")
require.Nil(t, err)
})
@@ -105,7 +105,7 @@ func TestOpsGenieNotifier(t *testing.T) {
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
@@ -154,7 +154,7 @@ func TestOpsGenieNotifier(t *testing.T) {
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil, nil, nil)
}, nil, nil, nil, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
@@ -203,7 +203,7 @@ func TestOpsGenieNotifier(t *testing.T) {
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil, nil, nil)
}, nil, nil, nil, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
@@ -142,7 +142,7 @@ func TestPagerdutyNotifier(t *testing.T) {
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
@@ -198,7 +198,7 @@ func TestPagerdutyNotifier(t *testing.T) {
ID: 0,
Name: "someRule",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
@@ -256,7 +256,7 @@ func TestPagerdutyNotifier(t *testing.T) {
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.IsTestRun = true
evalContext.EvalMatches = []*alerting.EvalMatch{
{
@@ -335,7 +335,7 @@ func TestPagerdutyNotifier(t *testing.T) {
{Key: "severity", Value: "warning"},
{Key: "dedup_key", Value: "key-" + strings.Repeat("x", 260)},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
@@ -414,7 +414,7 @@ func TestPagerdutyNotifier(t *testing.T) {
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "info"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
@@ -493,7 +493,7 @@ func TestPagerdutyNotifier(t *testing.T) {
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "llama"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
@@ -76,7 +76,7 @@ func TestGenPushoverBody(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
require.Nil(t, err)
@@ -87,7 +87,7 @@ func TestGenPushoverBody(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
require.Nil(t, err)
@@ -61,7 +61,7 @@ func TestTelegramNotifier(t *testing.T) {
Name: "This is an alarm",
Message: "Some kind of message.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
require.LessOrEqual(t, len(caption), 1024)
@@ -77,7 +77,7 @@ func TestTelegramNotifier(t *testing.T) {
Name: "This is an alarm",
Message: "Some kind of message.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
caption := generateImageCaption(evalContext,
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
@@ -95,7 +95,7 @@ func TestTelegramNotifier(t *testing.T) {
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
@@ -112,7 +112,7 @@ func TestTelegramNotifier(t *testing.T) {
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
@@ -93,7 +93,7 @@ func TestVictoropsNotifier(t *testing.T) {
{Key: "keyOnly"},
{Key: "severity", Value: "warning"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.IsTestRun = true
payload, err := victoropsNotifier.buildEventPayload(evalContext)
@@ -141,7 +141,7 @@ func TestVictoropsNotifier(t *testing.T) {
{Key: "keyOnly"},
{Key: "severity", Value: "warning"},
},
}, &validations.OSSPluginRequestValidator{}, nil, nil)
}, &validations.OSSPluginRequestValidator{}, nil, nil, nil)
evalContext.IsTestRun = true
payload, err := victoropsNotifier.buildEventPayload(evalContext)
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
@@ -84,7 +85,7 @@ func TestAlertRuleForParsing(t *testing.T) {
}
func TestAlertRuleModel(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
sqlStore := &sqlStore{db: sqlstore.InitTestDB(t), cache: localcache.New(time.Minute, time.Minute)}
RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
return &FakeCondition{}, nil
})
+4 -4
View File
@@ -7,21 +7,21 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type AlertNotificationService struct {
SQLStore *sqlstore.SQLStore
SQLStore AlertNotificationStore
EncryptionService encryption.Internal
NotificationService *notifications.NotificationService
}
func ProvideService(store *sqlstore.SQLStore, encryptionService encryption.Internal,
func ProvideService(store db.DB, encryptionService encryption.Internal,
notificationService *notifications.NotificationService) *AlertNotificationService {
s := &AlertNotificationService{
SQLStore: store,
SQLStore: &sqlStore{db: store},
EncryptionService: encryptionService,
NotificationService: notificationService,
}
+9 -2
View File
@@ -4,8 +4,11 @@ import (
"context"
"strings"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider"
@@ -17,7 +20,11 @@ import (
)
func TestService(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
sqlStore := &sqlStore{
db: sqlstore.InitTestDB(t),
log: &log.ConcreteLogger{},
cache: localcache.New(time.Minute, time.Minute),
}
nType := "test"
registerTestNotifier(nType)
@@ -30,7 +37,7 @@ func TestService(t *testing.T) {
encService, err := encryptionservice.ProvideEncryptionService(encProvider, usMock, settings)
require.NoError(t, err)
s := ProvideService(sqlStore, encService, nil)
s := ProvideService(sqlStore.db, encService, nil)
origSecret := setting.SecretKey
setting.SecretKey = "alert_notification_service_test"
+400
View File
@@ -0,0 +1,400 @@
package alerting
import (
"bytes"
"context"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
)
// AlertStore is a subset of SQLStore API to satisfy the needs of the alerting service.
// A subset is needed to make it easier to mock during the tests.
type AlertStore interface {
GetAlertById(context.Context, *models.GetAlertByIdQuery) error
GetAllAlertQueryHandler(context.Context, *models.GetAllAlertsQuery) error
GetAlertStatesForDashboard(context.Context, *models.GetAlertStatesForDashboardQuery) error
HandleAlertsQuery(context.Context, *models.GetAlertsQuery) error
SetAlertNotificationStateToCompleteCommand(context.Context, *models.SetAlertNotificationStateToCompleteCommand) error
SetAlertNotificationStateToPendingCommand(context.Context, *models.SetAlertNotificationStateToPendingCommand) error
GetAlertNotificationUidWithId(context.Context, *models.GetAlertNotificationUidQuery) error
GetAlertNotificationsWithUidToSend(context.Context, *models.GetAlertNotificationsWithUidToSendQuery) error
GetOrCreateAlertNotificationState(context.Context, *models.GetOrCreateNotificationStateQuery) error
SetAlertState(context.Context, *models.SetAlertStateCommand) error
PauseAlert(context.Context, *models.PauseAlertCommand) error
PauseAllAlerts(context.Context, *models.PauseAllAlertCommand) error
}
type sqlStore struct {
db db.DB
cache *localcache.CacheService
log *log.ConcreteLogger
}
func ProvideAlertStore(
db db.DB,
cacheService *localcache.CacheService) AlertStore {
return &sqlStore{
db: db,
cache: cacheService,
log: log.New("alerting.store"),
}
}
func (ss *sqlStore) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
alert := models.Alert{}
has, err := sess.ID(query.Id).Get(&alert)
if !has {
return fmt.Errorf("could not find alert")
}
if err != nil {
return err
}
query.Result = &alert
return nil
})
}
func (ss *sqlStore) GetAllAlertQueryHandler(ctx context.Context, query *models.GetAllAlertsQuery) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var alerts []*models.Alert
err := sess.SQL("select * from alert").Find(&alerts)
if err != nil {
return err
}
query.Result = alerts
return nil
})
}
func deleteAlertByIdInternal(alertId int64, reason string, sess *sqlstore.DBSession, log *log.ConcreteLogger) error {
log.Debug("Deleting alert", "id", alertId, "reason", reason)
if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", alertId); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM annotation WHERE alert_id = ?", alertId); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_id = ?", alertId); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alertId); err != nil {
return err
}
return nil
}
func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *models.GetAlertsQuery) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
builder := sqlstore.SQLBuilder{}
builder.Write(`SELECT
alert.id,
alert.dashboard_id,
alert.panel_id,
alert.name,
alert.state,
alert.new_state_date,
alert.eval_data,
alert.eval_date,
alert.execution_error,
dashboard.uid as dashboard_uid,
dashboard.slug as dashboard_slug
FROM alert
INNER JOIN dashboard on dashboard.id = alert.dashboard_id `)
builder.Write(`WHERE alert.org_id = ?`, query.OrgId)
if len(strings.TrimSpace(query.Query)) > 0 {
builder.Write(" AND alert.name "+ss.db.GetDialect().LikeStr()+" ?", "%"+query.Query+"%")
}
if len(query.DashboardIDs) > 0 {
builder.Write(` AND alert.dashboard_id IN (?` + strings.Repeat(",?", len(query.DashboardIDs)-1) + `) `)
for _, dbID := range query.DashboardIDs {
builder.AddParams(dbID)
}
}
if query.PanelId != 0 {
builder.Write(` AND alert.panel_id = ?`, query.PanelId)
}
if len(query.State) > 0 && query.State[0] != "all" {
builder.Write(` AND (`)
for i, v := range query.State {
if i > 0 {
builder.Write(" OR ")
}
if strings.HasPrefix(v, "not_") {
builder.Write("state <> ? ")
v = strings.TrimPrefix(v, "not_")
} else {
builder.Write("state = ? ")
}
builder.AddParams(v)
}
builder.Write(")")
}
if query.User.OrgRole != models.ROLE_ADMIN {
builder.WriteDashboardPermissionFilter(query.User, models.PERMISSION_VIEW)
}
builder.Write(" ORDER BY name ASC")
if query.Limit != 0 {
builder.Write(ss.db.GetDialect().Limit(query.Limit))
}
alerts := make([]*models.AlertListItemDTO, 0)
if err := sess.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&alerts); err != nil {
return err
}
for i := range alerts {
if alerts[i].ExecutionError == " " {
alerts[i].ExecutionError = ""
}
}
query.Result = alerts
return nil
})
}
func (ss *sqlStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
existingAlerts, err := GetAlertsByDashboardId2(dashID, sess)
if err != nil {
return err
}
if err := updateAlerts(existingAlerts, alerts, sess, ss.log); err != nil {
return err
}
if err := deleteMissingAlerts(existingAlerts, alerts, sess, ss.log); err != nil {
return err
}
return nil
})
}
func updateAlerts(existingAlerts []*models.Alert, alerts []*models.Alert, sess *sqlstore.DBSession, log *log.ConcreteLogger) error {
for _, alert := range alerts {
update := false
var alertToUpdate *models.Alert
for _, k := range existingAlerts {
if alert.PanelId == k.PanelId {
update = true
alert.Id = k.Id
alertToUpdate = k
break
}
}
if update {
if alertToUpdate.ContainsUpdates(alert) {
alert.Updated = timeNow()
alert.State = alertToUpdate.State
sess.MustCols("message", "for")
_, err := sess.ID(alert.Id).Update(alert)
if err != nil {
return err
}
log.Debug("Alert updated", "name", alert.Name, "id", alert.Id)
}
} else {
alert.Updated = timeNow()
alert.Created = timeNow()
alert.State = models.AlertStateUnknown
alert.NewStateDate = timeNow()
_, err := sess.Insert(alert)
if err != nil {
return err
}
log.Debug("Alert inserted", "name", alert.Name, "id", alert.Id)
}
tags := alert.GetTagsFromSettings()
if _, err := sess.Exec("DELETE FROM alert_rule_tag WHERE alert_id = ?", alert.Id); err != nil {
return err
}
if tags != nil {
tags, err := sqlstore.EnsureTagsExist(sess, tags)
if err != nil {
return err
}
for _, tag := range tags {
if _, err := sess.Exec("INSERT INTO alert_rule_tag (alert_id, tag_id) VALUES(?,?)", alert.Id, tag.Id); err != nil {
return err
}
}
}
}
return nil
}
func deleteMissingAlerts(alerts []*models.Alert, existingAlerts []*models.Alert, sess *sqlstore.DBSession, log *log.ConcreteLogger) error {
for _, missingAlert := range alerts {
missing := true
for _, k := range existingAlerts {
if missingAlert.PanelId == k.PanelId {
missing = false
break
}
}
if missing {
if err := deleteAlertByIdInternal(missingAlert.Id, "Removed from dashboard", sess, log); err != nil {
// No use trying to delete more, since we're in a transaction and it will be
// rolled back on error.
return err
}
}
}
return nil
}
func GetAlertsByDashboardId2(dashboardId int64, sess *sqlstore.DBSession) ([]*models.Alert, error) {
alerts := make([]*models.Alert, 0)
err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
if err != nil {
return []*models.Alert{}, err
}
return alerts, nil
}
func (ss *sqlStore) SetAlertState(ctx context.Context, cmd *models.SetAlertStateCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
alert := models.Alert{}
if has, err := sess.ID(cmd.AlertId).Get(&alert); err != nil {
return err
} else if !has {
return fmt.Errorf("could not find alert")
}
if alert.State == models.AlertStatePaused {
return models.ErrCannotChangeStateOnPausedAlert
}
if alert.State == cmd.State {
return models.ErrRequiresNewState
}
alert.State = cmd.State
alert.StateChanges++
alert.NewStateDate = timeNow()
alert.EvalData = cmd.EvalData
if cmd.Error == "" {
alert.ExecutionError = " " // without this space, xorm skips updating this field
} else {
alert.ExecutionError = cmd.Error
}
_, err := sess.ID(alert.Id).Update(&alert)
if err != nil {
return err
}
cmd.Result = alert
return nil
})
}
func (ss *sqlStore) PauseAlert(ctx context.Context, cmd *models.PauseAlertCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if len(cmd.AlertIds) == 0 {
return fmt.Errorf("command contains no alertids")
}
var buffer bytes.Buffer
params := make([]interface{}, 0)
buffer.WriteString(`UPDATE alert SET state = ?, new_state_date = ?`)
if cmd.Paused {
params = append(params, string(models.AlertStatePaused))
params = append(params, timeNow().UTC())
} else {
params = append(params, string(models.AlertStateUnknown))
params = append(params, timeNow().UTC())
}
buffer.WriteString(` WHERE id IN (?` + strings.Repeat(",?", len(cmd.AlertIds)-1) + `)`)
for _, v := range cmd.AlertIds {
params = append(params, v)
}
sqlOrArgs := append([]interface{}{buffer.String()}, params...)
res, err := sess.Exec(sqlOrArgs...)
if err != nil {
return err
}
cmd.ResultCount, _ = res.RowsAffected()
return nil
})
}
func (ss *sqlStore) PauseAllAlerts(ctx context.Context, cmd *models.PauseAllAlertCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
var newState string
if cmd.Paused {
newState = string(models.AlertStatePaused)
} else {
newState = string(models.AlertStateUnknown)
}
res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow().UTC())
if err != nil {
return err
}
cmd.ResultCount, _ = res.RowsAffected()
return nil
})
}
func (ss *sqlStore) GetAlertStatesForDashboard(ctx context.Context, query *models.GetAlertStatesForDashboardQuery) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = `SELECT
id,
dashboard_id,
panel_id,
state,
new_state_date
FROM alert
WHERE org_id = ? AND dashboard_id = ?`
query.Result = make([]*models.AlertStateInfoDTO, 0)
err := sess.SQL(rawSQL, query.OrgId, query.DashboardId).Find(&query.Result)
return err
})
}
+604
View File
@@ -0,0 +1,604 @@
package alerting
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
type AlertNotificationStore interface {
DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error
DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error
GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error
GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error
GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error
GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error
GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error
CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error
UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error
UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error
SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error
SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error
GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error
}
// timeNow makes it possible to test usage of time
var timeNow = time.Now
func (ss *sqlStore) DeleteAlertNotification(ctx context.Context, cmd *models.DeleteAlertNotificationCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
res, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return models.ErrAlertNotificationNotFound
}
if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_notification_state.org_id = ? AND alert_notification_state.notifier_id = ?", cmd.OrgId, cmd.Id); err != nil {
return err
}
return nil
})
}
func (ss *sqlStore) DeleteAlertNotificationWithUid(ctx context.Context, cmd *models.DeleteAlertNotificationWithUidCommand) error {
existingNotification := &models.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
if err := getAlertNotificationWithUidInternal(ctx, existingNotification, ss.db.NewSession(ctx)); err != nil {
return err
}
if existingNotification.Result == nil {
return models.ErrAlertNotificationNotFound
}
cmd.DeletedAlertNotificationId = existingNotification.Result.Id
deleteCommand := &models.DeleteAlertNotificationCommand{
Id: existingNotification.Result.Id,
OrgId: existingNotification.Result.OrgId,
}
if err := ss.DeleteAlertNotification(ctx, deleteCommand); err != nil {
return err
}
return nil
}
func (ss *sqlStore) GetAlertNotifications(ctx context.Context, query *models.GetAlertNotificationsQuery) error {
return getAlertNotificationInternal(ctx, query, ss.db.NewSession(ctx))
}
func (ss *sqlStore) GetAlertNotificationUidWithId(ctx context.Context, query *models.GetAlertNotificationUidQuery) error {
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
if cached, found := ss.cache.Get(cacheKey); found {
query.Result = cached.(string)
return nil
}
err := getAlertNotificationUidInternal(ctx, query, ss.db.NewSession(ctx))
if err != nil {
return err
}
ss.cache.Set(cacheKey, query.Result, -1) // Infinite, never changes
return nil
}
func newAlertNotificationUidCacheKey(orgID, notificationId int64) string {
return fmt.Sprintf("notification-uid-by-org-%d-and-id-%d", orgID, notificationId)
}
func (ss *sqlStore) GetAlertNotificationsWithUid(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery) error {
return getAlertNotificationWithUidInternal(ctx, query, ss.db.NewSession(ctx))
}
func (ss *sqlStore) GetAllAlertNotifications(ctx context.Context, query *models.GetAllAlertNotificationsQuery) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
results := make([]*models.AlertNotification, 0)
if err := sess.Where("org_id = ?", query.OrgId).Asc("name").Find(&results); err != nil {
return err
}
query.Result = results
return nil
})
}
func (ss *sqlStore) GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
alert_notification.id,
alert_notification.uid,
alert_notification.org_id,
alert_notification.name,
alert_notification.type,
alert_notification.created,
alert_notification.updated,
alert_notification.settings,
alert_notification.secure_settings,
alert_notification.is_default,
alert_notification.disable_resolve_message,
alert_notification.send_reminder,
alert_notification.frequency
FROM alert_notification
`)
sql.WriteString(` WHERE alert_notification.org_id = ?`)
params = append(params, query.OrgId)
sql.WriteString(` AND ((alert_notification.is_default = ?)`)
params = append(params, ss.db.GetDialect().BooleanStr(true))
if len(query.Uids) > 0 {
sql.WriteString(` OR alert_notification.uid IN (?` + strings.Repeat(",?", len(query.Uids)-1) + ")")
for _, v := range query.Uids {
params = append(params, v)
}
}
sql.WriteString(`)`)
results := make([]*models.AlertNotification, 0)
if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
return err
}
query.Result = results
return nil
})
}
func getAlertNotificationUidInternal(ctx context.Context, query *models.GetAlertNotificationUidQuery, sess *sqlstore.DBSession) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
alert_notification.uid
FROM alert_notification
`)
sql.WriteString(` WHERE alert_notification.org_id = ?`)
params = append(params, query.OrgId)
sql.WriteString(` AND alert_notification.id = ?`)
params = append(params, query.Id)
results := make([]string, 0)
if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
return err
}
if len(results) == 0 {
return models.ErrAlertNotificationFailedTranslateUniqueID
}
query.Result = results[0]
return nil
}
func getAlertNotificationInternal(ctx context.Context, query *models.GetAlertNotificationsQuery, sess *sqlstore.DBSession) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
alert_notification.id,
alert_notification.uid,
alert_notification.org_id,
alert_notification.name,
alert_notification.type,
alert_notification.created,
alert_notification.updated,
alert_notification.settings,
alert_notification.secure_settings,
alert_notification.is_default,
alert_notification.disable_resolve_message,
alert_notification.send_reminder,
alert_notification.frequency
FROM alert_notification
`)
sql.WriteString(` WHERE alert_notification.org_id = ?`)
params = append(params, query.OrgId)
if query.Name != "" || query.Id != 0 {
if query.Name != "" {
sql.WriteString(` AND alert_notification.name = ?`)
params = append(params, query.Name)
}
if query.Id != 0 {
sql.WriteString(` AND alert_notification.id = ?`)
params = append(params, query.Id)
}
}
results := make([]*models.AlertNotification, 0)
if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
return err
}
if len(results) == 0 {
query.Result = nil
} else {
query.Result = results[0]
}
return nil
}
func getAlertNotificationWithUidInternal(ctx context.Context, query *models.GetAlertNotificationsWithUidQuery, sess *sqlstore.DBSession) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
alert_notification.id,
alert_notification.uid,
alert_notification.org_id,
alert_notification.name,
alert_notification.type,
alert_notification.created,
alert_notification.updated,
alert_notification.settings,
alert_notification.secure_settings,
alert_notification.is_default,
alert_notification.disable_resolve_message,
alert_notification.send_reminder,
alert_notification.frequency
FROM alert_notification
`)
sql.WriteString(` WHERE alert_notification.org_id = ? AND alert_notification.uid = ?`)
params = append(params, query.OrgId, query.Uid)
results := make([]*models.AlertNotification, 0)
if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
return err
}
if len(results) == 0 {
query.Result = nil
} else {
query.Result = results[0]
}
return nil
}
func (ss *sqlStore) CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if cmd.Uid == "" {
uid, uidGenerationErr := generateNewAlertNotificationUid(ctx, sess, cmd.OrgId)
if uidGenerationErr != nil {
return uidGenerationErr
}
cmd.Uid = uid
}
existingQuery := &models.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
err := getAlertNotificationWithUidInternal(ctx, existingQuery, sess)
if err != nil {
return err
}
if existingQuery.Result != nil {
return models.ErrAlertNotificationWithSameUIDExists
}
// check if name exists
sameNameQuery := &models.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
if err := getAlertNotificationInternal(ctx, sameNameQuery, sess); err != nil {
return err
}
if sameNameQuery.Result != nil {
return models.ErrAlertNotificationWithSameNameExists
}
var frequency time.Duration
if cmd.SendReminder {
if cmd.Frequency == "" {
return models.ErrNotificationFrequencyNotFound
}
frequency, err = time.ParseDuration(cmd.Frequency)
if err != nil {
return err
}
}
// delete empty keys
for k, v := range cmd.SecureSettings {
if v == "" {
delete(cmd.SecureSettings, k)
}
}
alertNotification := &models.AlertNotification{
Uid: cmd.Uid,
OrgId: cmd.OrgId,
Name: cmd.Name,
Type: cmd.Type,
Settings: cmd.Settings,
SecureSettings: cmd.EncryptedSecureSettings,
SendReminder: cmd.SendReminder,
DisableResolveMessage: cmd.DisableResolveMessage,
Frequency: frequency,
Created: time.Now(),
Updated: time.Now(),
IsDefault: cmd.IsDefault,
}
if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
return err
}
cmd.Result = alertNotification
return nil
})
}
func generateNewAlertNotificationUid(ctx context.Context, 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.AlertNotification{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", models.ErrAlertNotificationFailedGenerateUniqueUid
}
func (ss *sqlStore) UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) (err error) {
current := models.AlertNotification{}
if _, err = sess.ID(cmd.Id).Get(&current); err != nil {
return err
}
if current.Id == 0 {
return models.ErrAlertNotificationNotFound
}
// check if name exists
sameNameQuery := &models.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
if err := getAlertNotificationInternal(ctx, sameNameQuery, sess); err != nil {
return err
}
if sameNameQuery.Result != nil && sameNameQuery.Result.Id != current.Id {
return fmt.Errorf("alert notification name %q already exists", cmd.Name)
}
// delete empty keys
for k, v := range cmd.SecureSettings {
if v == "" {
delete(cmd.SecureSettings, k)
}
}
current.Updated = time.Now()
current.Settings = cmd.Settings
current.SecureSettings = cmd.EncryptedSecureSettings
current.Name = cmd.Name
current.Type = cmd.Type
current.IsDefault = cmd.IsDefault
current.SendReminder = cmd.SendReminder
current.DisableResolveMessage = cmd.DisableResolveMessage
if cmd.Uid != "" {
current.Uid = cmd.Uid
}
if current.SendReminder {
if cmd.Frequency == "" {
return models.ErrNotificationFrequencyNotFound
}
frequency, err := time.ParseDuration(cmd.Frequency)
if err != nil {
return err
}
current.Frequency = frequency
}
sess.UseBool("is_default", "send_reminder", "disable_resolve_message")
if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
return err
} else if affected == 0 {
return fmt.Errorf("could not update alert notification")
}
cmd.Result = &current
return nil
})
}
func (ss *sqlStore) UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error {
getAlertNotificationWithUidQuery := &models.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
if err := getAlertNotificationWithUidInternal(ctx, getAlertNotificationWithUidQuery, ss.db.NewSession(ctx)); err != nil {
return err
}
current := getAlertNotificationWithUidQuery.Result
if current == nil {
return models.ErrAlertNotificationNotFound
}
if cmd.NewUid == "" {
cmd.NewUid = cmd.Uid
}
updateNotification := &models.UpdateAlertNotificationCommand{
Id: current.Id,
Uid: cmd.NewUid,
Name: cmd.Name,
Type: cmd.Type,
SendReminder: cmd.SendReminder,
DisableResolveMessage: cmd.DisableResolveMessage,
Frequency: cmd.Frequency,
IsDefault: cmd.IsDefault,
Settings: cmd.Settings,
SecureSettings: cmd.SecureSettings,
OrgId: cmd.OrgId,
}
if err := ss.UpdateAlertNotification(ctx, updateNotification); err != nil {
return err
}
cmd.Result = updateNotification.Result
return nil
}
func (ss *sqlStore) SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToCompleteCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
version := cmd.Version
var current models.AlertNotificationState
if _, err := sess.ID(cmd.Id).Get(&current); err != nil {
return err
}
newVersion := cmd.Version + 1
sql := `UPDATE alert_notification_state SET
state = ?,
version = ?,
updated_at = ?
WHERE
id = ?`
_, err := sess.Exec(sql, models.AlertNotificationStateCompleted, newVersion, timeNow().Unix(), cmd.Id)
if err != nil {
return err
}
if current.Version != version {
ss.log.Error("notification state out of sync. the notification is marked as complete but has been modified between set as pending and completion.", "notifierId", current.NotifierId)
}
return nil
})
}
func (ss *sqlStore) SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
newVersion := cmd.Version + 1
sql := `UPDATE alert_notification_state SET
state = ?,
version = ?,
updated_at = ?,
alert_rule_state_updated_version = ?
WHERE
id = ? AND
(version = ? OR alert_rule_state_updated_version < ?)`
res, err := sess.Exec(sql,
models.AlertNotificationStatePending,
newVersion,
timeNow().Unix(),
cmd.AlertRuleStateUpdatedVersion,
cmd.Id,
cmd.Version,
cmd.AlertRuleStateUpdatedVersion)
if err != nil {
return err
}
affected, _ := res.RowsAffected()
if affected == 0 {
return models.ErrAlertNotificationStateVersionConflict
}
cmd.ResultVersion = newVersion
return nil
})
}
func (ss *sqlStore) GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
nj := &models.AlertNotificationState{}
exist, err := getAlertNotificationState(ctx, sess, cmd, nj)
// if exists, return it, otherwise create it with default values
if err != nil {
return err
}
if exist {
cmd.Result = nj
return nil
}
notificationState := &models.AlertNotificationState{
OrgId: cmd.OrgId,
AlertId: cmd.AlertId,
NotifierId: cmd.NotifierId,
State: models.AlertNotificationStateUnknown,
UpdatedAt: timeNow().Unix(),
}
if _, err := sess.Insert(notificationState); err != nil {
if ss.db.GetDialect().IsUniqueConstraintViolation(err) {
exist, err = getAlertNotificationState(ctx, sess, cmd, nj)
if err != nil {
return err
}
if !exist {
return errors.New("should not happen")
}
cmd.Result = nj
return nil
}
return err
}
cmd.Result = notificationState
return nil
})
}
func getAlertNotificationState(ctx context.Context, sess *sqlstore.DBSession, cmd *models.GetOrCreateNotificationStateQuery, nj *models.AlertNotificationState) (bool, error) {
return sess.
Where("alert_notification_state.org_id = ?", cmd.OrgId).
Where("alert_notification_state.alert_id = ?", cmd.AlertId).
Where("alert_notification_state.notifier_id = ?", cmd.NotifierId).
Get(nj)
}
@@ -0,0 +1,499 @@
package alerting
import (
"context"
"errors"
"regexp"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/require"
)
func TestIntegrationAlertNotificationSQLAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
var store *sqlStore
setup := func() {
store = &sqlStore{
db: sqlstore.InitTestDB(t),
log: log.New(),
cache: localcache.New(time.Minute, time.Minute)}
}
t.Run("Alert notification state", func(t *testing.T) {
setup()
var alertID int64 = 7
var orgID int64 = 5
var notifierID int64 = 10
oldTimeNow := timeNow
now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC)
timeNow = func() time.Time { return now }
defer func() { timeNow = oldTimeNow }()
t.Run("Get no existing state should create a new state", func(t *testing.T) {
query := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err := store.GetOrCreateAlertNotificationState(context.Background(), query)
require.Nil(t, err)
require.NotNil(t, query.Result)
require.Equal(t, models.AlertNotificationStateUnknown, query.Result.State)
require.Equal(t, int64(0), query.Result.Version)
require.Equal(t, now.Unix(), query.Result.UpdatedAt)
t.Run("Get existing state should not create a new state", func(t *testing.T) {
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err := store.GetOrCreateAlertNotificationState(context.Background(), query2)
require.Nil(t, err)
require.NotNil(t, query2.Result)
require.Equal(t, query.Result.Id, query2.Result.Id)
require.Equal(t, now.Unix(), query2.Result.UpdatedAt)
})
t.Run("Update existing state to pending with correct version should update database", func(t *testing.T) {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := store.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Nil(t, err)
require.Equal(t, int64(1), cmd.ResultVersion)
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = store.GetOrCreateAlertNotificationState(context.Background(), query2)
require.Nil(t, err)
require.Equal(t, int64(1), query2.Result.Version)
require.Equal(t, models.AlertNotificationStatePending, query2.Result.State)
require.Equal(t, now.Unix(), query2.Result.UpdatedAt)
t.Run("Update existing state to completed should update database", func(t *testing.T) {
s := *query.Result
setStateCmd := models.SetAlertNotificationStateToCompleteCommand{
Id: s.Id,
Version: cmd.ResultVersion,
}
err := store.SetAlertNotificationStateToCompleteCommand(context.Background(), &setStateCmd)
require.Nil(t, err)
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = store.GetOrCreateAlertNotificationState(context.Background(), query3)
require.Nil(t, err)
require.Equal(t, int64(2), query3.Result.Version)
require.Equal(t, models.AlertNotificationStateCompleted, query3.Result.State)
require.Equal(t, now.Unix(), query3.Result.UpdatedAt)
})
t.Run("Update existing state to completed should update database. regardless of version", func(t *testing.T) {
s := *query.Result
unknownVersion := int64(1000)
cmd := models.SetAlertNotificationStateToCompleteCommand{
Id: s.Id,
Version: unknownVersion,
}
err := store.SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd)
require.Nil(t, err)
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = store.GetOrCreateAlertNotificationState(context.Background(), query3)
require.Nil(t, err)
require.Equal(t, unknownVersion+1, query3.Result.Version)
require.Equal(t, models.AlertNotificationStateCompleted, query3.Result.State)
require.Equal(t, now.Unix(), query3.Result.UpdatedAt)
})
})
t.Run("Update existing state to pending with incorrect version should return version mismatch error", func(t *testing.T) {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.NotifierId,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := store.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Equal(t, models.ErrAlertNotificationStateVersionConflict, err)
})
t.Run("Updating existing state to pending with incorrect version since alert rule state update version is higher", func(t *testing.T) {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: 1000,
}
err := store.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Nil(t, err)
require.Equal(t, int64(1), cmd.ResultVersion)
})
t.Run("different version and same alert state change version should return error", func(t *testing.T) {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := store.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Error(t, err)
})
})
})
t.Run("Alert notifications should be empty", func(t *testing.T) {
setup()
cmd := &models.GetAlertNotificationsQuery{
OrgId: 2,
Name: "email",
}
err := store.GetAlertNotifications(context.Background(), cmd)
require.Nil(t, err)
require.Nil(t, cmd.Result)
})
t.Run("Cannot save alert notifier with send reminder = true", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops",
Type: "email",
OrgId: 1,
SendReminder: true,
Settings: simplejson.New(),
}
t.Run("and missing frequency", func(t *testing.T) {
err := store.CreateAlertNotificationCommand(context.Background(), cmd)
require.Equal(t, models.ErrNotificationFrequencyNotFound, err)
})
t.Run("invalid frequency", func(t *testing.T) {
cmd.Frequency = "invalid duration"
err := store.CreateAlertNotificationCommand(context.Background(), cmd)
require.True(t, regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString(
err.Error()))
})
})
t.Run("Cannot update alert notifier with send reminder = false", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops update",
Type: "email",
OrgId: 1,
SendReminder: false,
Settings: simplejson.New(),
}
err := store.CreateAlertNotificationCommand(context.Background(), cmd)
require.Nil(t, err)
updateCmd := &models.UpdateAlertNotificationCommand{
Id: cmd.Result.Id,
SendReminder: true,
}
t.Run("and missing frequency", func(t *testing.T) {
err := store.UpdateAlertNotification(context.Background(), updateCmd)
require.Equal(t, models.ErrNotificationFrequencyNotFound, err)
})
t.Run("invalid frequency", func(t *testing.T) {
updateCmd.Frequency = "invalid duration"
err := store.UpdateAlertNotification(context.Background(), updateCmd)
require.Error(t, err)
require.True(t, regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString(
err.Error()))
})
})
t.Run("Can save Alert Notification", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops",
Type: "email",
OrgId: 1,
SendReminder: true,
Frequency: "10s",
Settings: simplejson.New(),
}
err := store.CreateAlertNotificationCommand(context.Background(), cmd)
require.Nil(t, err)
require.NotEqual(t, 0, cmd.Result.Id)
require.NotEqual(t, 0, cmd.Result.OrgId)
require.Equal(t, "email", cmd.Result.Type)
require.Equal(t, 10*time.Second, cmd.Result.Frequency)
require.False(t, cmd.Result.DisableResolveMessage)
require.NotEmpty(t, cmd.Result.Uid)
t.Run("Cannot save Alert Notification with the same name", func(t *testing.T) {
err = store.CreateAlertNotificationCommand(context.Background(), cmd)
require.Error(t, err)
})
t.Run("Cannot save Alert Notification with the same name and another uid", func(t *testing.T) {
anotherUidCmd := &models.CreateAlertNotificationCommand{
Name: cmd.Name,
Type: cmd.Type,
OrgId: 1,
SendReminder: cmd.SendReminder,
Frequency: cmd.Frequency,
Settings: cmd.Settings,
Uid: "notifier1",
}
err = store.CreateAlertNotificationCommand(context.Background(), anotherUidCmd)
require.Error(t, err)
})
t.Run("Can save Alert Notification with another name and another uid", func(t *testing.T) {
anotherUidCmd := &models.CreateAlertNotificationCommand{
Name: "another ops",
Type: cmd.Type,
OrgId: 1,
SendReminder: cmd.SendReminder,
Frequency: cmd.Frequency,
Settings: cmd.Settings,
Uid: "notifier2",
}
err = store.CreateAlertNotificationCommand(context.Background(), anotherUidCmd)
require.Nil(t, err)
})
t.Run("Can update alert notification", func(t *testing.T) {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: cmd.Result.Id,
}
err := store.UpdateAlertNotification(context.Background(), newCmd)
require.Nil(t, err)
require.Equal(t, "NewName", newCmd.Result.Name)
require.Equal(t, 60*time.Second, newCmd.Result.Frequency)
require.True(t, newCmd.Result.DisableResolveMessage)
})
t.Run("Can update alert notification to disable sending of reminders", func(t *testing.T) {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: false,
Settings: simplejson.New(),
Id: cmd.Result.Id,
}
err := store.UpdateAlertNotification(context.Background(), newCmd)
require.Nil(t, err)
require.False(t, newCmd.Result.SendReminder)
})
})
t.Run("Can search using an array of ids", func(t *testing.T) {
setup()
cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
require.Nil(t, store.CreateAlertNotificationCommand(context.Background(), &cmd1))
require.Nil(t, store.CreateAlertNotificationCommand(context.Background(), &cmd2))
require.Nil(t, store.CreateAlertNotificationCommand(context.Background(), &cmd3))
require.Nil(t, store.CreateAlertNotificationCommand(context.Background(), &cmd4))
require.Nil(t, store.CreateAlertNotificationCommand(context.Background(), &otherOrg))
t.Run("search", func(t *testing.T) {
query := &models.GetAlertNotificationsWithUidToSendQuery{
Uids: []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"},
OrgId: 1,
}
err := store.GetAlertNotificationsWithUidToSend(context.Background(), query)
require.Nil(t, err)
require.Equal(t, 3, len(query.Result))
})
t.Run("all", func(t *testing.T) {
query := &models.GetAllAlertNotificationsQuery{
OrgId: 1,
}
err := store.GetAllAlertNotifications(context.Background(), query)
require.Nil(t, err)
require.Equal(t, 4, len(query.Result))
require.Equal(t, cmd4.Name, query.Result[0].Name)
require.Equal(t, cmd1.Name, query.Result[1].Name)
require.Equal(t, cmd3.Name, query.Result[2].Name)
require.Equal(t, cmd2.Name, query.Result[3].Name)
})
})
t.Run("Notification Uid by Id Caching", func(t *testing.T) {
setup()
notification := &models.CreateAlertNotificationCommand{Uid: "aNotificationUid", OrgId: 1, Name: "aNotificationUid"}
err := store.CreateAlertNotificationCommand(context.Background(), notification)
require.Nil(t, err)
byUidQuery := &models.GetAlertNotificationsWithUidQuery{
Uid: notification.Uid,
OrgId: notification.OrgId,
}
notificationByUidErr := store.GetAlertNotificationsWithUid(context.Background(), byUidQuery)
require.Nil(t, notificationByUidErr)
t.Run("Can cache notification Uid", func(t *testing.T) {
byIdQuery := &models.GetAlertNotificationUidQuery{
Id: byUidQuery.Result.Id,
OrgId: byUidQuery.Result.OrgId,
}
cacheKey := newAlertNotificationUidCacheKey(byIdQuery.OrgId, byIdQuery.Id)
resultBeforeCaching, foundBeforeCaching := store.cache.Get(cacheKey)
require.False(t, foundBeforeCaching)
require.Nil(t, resultBeforeCaching)
notificationByIdErr := store.GetAlertNotificationUidWithId(context.Background(), byIdQuery)
require.Nil(t, notificationByIdErr)
resultAfterCaching, foundAfterCaching := store.cache.Get(cacheKey)
require.True(t, foundAfterCaching)
require.Equal(t, notification.Uid, resultAfterCaching)
})
t.Run("Retrieves from cache when exists", func(t *testing.T) {
query := &models.GetAlertNotificationUidQuery{
Id: 999,
OrgId: 100,
}
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
store.cache.Set(cacheKey, "a-cached-uid", -1)
err := store.GetAlertNotificationUidWithId(context.Background(), query)
require.Nil(t, err)
require.Equal(t, "a-cached-uid", query.Result)
})
t.Run("Returns an error without populating cache when the notification doesn't exist in the database", func(t *testing.T) {
query := &models.GetAlertNotificationUidQuery{
Id: -1,
OrgId: 100,
}
err := store.GetAlertNotificationUidWithId(context.Background(), query)
require.Equal(t, "", query.Result)
require.Error(t, err)
require.True(t, errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID))
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
result, found := store.cache.Get(cacheKey)
require.False(t, found)
require.Nil(t, result)
})
})
t.Run("Cannot update non-existing Alert Notification", func(t *testing.T) {
setup()
updateCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: 1,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: 1,
}
err := store.UpdateAlertNotification(context.Background(), updateCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
t.Run("using UID", func(t *testing.T) {
updateWithUidCmd := &models.UpdateAlertNotificationWithUidCommand{
Name: "NewName",
Type: "webhook",
OrgId: 1,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Uid: "uid",
NewUid: "newUid",
}
err := store.UpdateAlertNotificationWithUid(context.Background(), updateWithUidCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
})
})
t.Run("Can delete Alert Notification", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops update",
Type: "email",
OrgId: 1,
SendReminder: false,
Settings: simplejson.New(),
}
err := store.CreateAlertNotificationCommand(context.Background(), cmd)
require.Nil(t, err)
deleteCmd := &models.DeleteAlertNotificationCommand{
Id: cmd.Result.Id,
OrgId: 1,
}
err = store.DeleteAlertNotification(context.Background(), deleteCmd)
require.Nil(t, err)
t.Run("using UID", func(t *testing.T) {
err := store.CreateAlertNotificationCommand(context.Background(), cmd)
require.Nil(t, err)
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
Uid: cmd.Result.Uid,
OrgId: 1,
}
err = store.DeleteAlertNotificationWithUid(context.Background(), deleteWithUidCmd)
require.Nil(t, err)
require.Equal(t, cmd.Result.Id, deleteWithUidCmd.DeletedAlertNotificationId)
})
})
t.Run("Cannot delete non-existing Alert Notification", func(t *testing.T) {
setup()
deleteCmd := &models.DeleteAlertNotificationCommand{
Id: 1,
OrgId: 1,
}
err := store.DeleteAlertNotification(context.Background(), deleteCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
t.Run("using UID", func(t *testing.T) {
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
Uid: "uid",
OrgId: 1,
}
err = store.DeleteAlertNotificationWithUid(context.Background(), deleteWithUidCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
})
})
}
+426
View File
@@ -0,0 +1,426 @@
package alerting
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/require"
)
func mockTimeNow() {
var timeSeed int64
timeNow = func() time.Time {
loc := time.FixedZone("MockZoneUTC-5", -5*60*60)
fakeNow := time.Unix(timeSeed, 0).In(loc)
timeSeed++
return fakeNow
}
}
func resetTimeNow() {
timeNow = time.Now
}
func TestIntegrationAlertingDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
mockTimeNow()
defer resetTimeNow()
var store *sqlStore
var testDash *models.Dashboard
var items []*models.Alert
setup := func(t *testing.T) {
store = &sqlStore{
db: sqlstore.InitTestDB(t),
log: log.New(),
}
testDash = insertTestDashboard(t, store.db, "dashboard with alerts", 1, 0, false, "alert")
evalData, err := simplejson.NewJson([]byte(`{"test": "test"}`))
require.Nil(t, err)
items = []*models.Alert{
{
PanelId: 1,
DashboardId: testDash.Id,
OrgId: testDash.OrgId,
Name: "Alerting title",
Message: "Alerting message",
Settings: simplejson.New(),
Frequency: 1,
EvalData: evalData,
},
}
err = store.SaveAlerts(context.Background(), testDash.Id, items)
require.Nil(t, err)
}
t.Run("Can set new states", func(t *testing.T) {
setup(t)
// Get alert so we can use its ID in tests
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := store.HandleAlertsQuery(context.Background(), &alertQuery)
require.Nil(t, err2)
insertedAlert := alertQuery.Result[0]
t.Run("new state ok", func(t *testing.T) {
cmd := &models.SetAlertStateCommand{
AlertId: insertedAlert.Id,
State: models.AlertStateOK,
}
err := store.SetAlertState(context.Background(), cmd)
require.Nil(t, err)
})
alert, _ := getAlertById(t, insertedAlert.Id, store)
stateDateBeforePause := alert.NewStateDate
t.Run("can pause all alerts", func(t *testing.T) {
err := store.pauseAllAlerts(t, true)
require.Nil(t, err)
t.Run("cannot updated paused alert", func(t *testing.T) {
cmd := &models.SetAlertStateCommand{
AlertId: insertedAlert.Id,
State: models.AlertStateOK,
}
err = store.SetAlertState(context.Background(), cmd)
require.Error(t, err)
})
t.Run("alert is paused", func(t *testing.T) {
alert, _ = getAlertById(t, insertedAlert.Id, store)
currentState := alert.State
require.Equal(t, models.AlertStatePaused, currentState)
})
t.Run("pausing alerts should update their NewStateDate", func(t *testing.T) {
alert, _ = getAlertById(t, insertedAlert.Id, store)
stateDateAfterPause := alert.NewStateDate
require.True(t, stateDateBeforePause.Before(stateDateAfterPause))
})
t.Run("unpausing alerts should update their NewStateDate again", func(t *testing.T) {
err := store.pauseAllAlerts(t, false)
require.Nil(t, err)
alert, _ = getAlertById(t, insertedAlert.Id, store)
stateDateAfterUnpause := alert.NewStateDate
require.True(t, stateDateBeforePause.Before(stateDateAfterUnpause))
})
})
})
t.Run("Can read properties", func(t *testing.T) {
setup(t)
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := store.HandleAlertsQuery(context.Background(), &alertQuery)
alert := alertQuery.Result[0]
require.Nil(t, err2)
require.Greater(t, alert.Id, int64(0))
require.Equal(t, testDash.Id, alert.DashboardId)
require.Equal(t, int64(1), alert.PanelId)
require.Equal(t, "Alerting title", alert.Name)
require.Equal(t, models.AlertStateUnknown, alert.State)
require.NotNil(t, alert.NewStateDate)
require.NotNil(t, alert.EvalData)
require.Equal(t, "test", alert.EvalData.Get("test").MustString())
require.NotNil(t, alert.EvalDate)
require.Equal(t, "", alert.ExecutionError)
require.NotNil(t, alert.DashboardUid)
require.Equal(t, "dashboard-with-alerts", alert.DashboardSlug)
})
t.Run("Viewer can read alerts", func(t *testing.T) {
setup(t)
viewerUser := &models.SignedInUser{OrgRole: models.ROLE_VIEWER, OrgId: 1}
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser}
err2 := store.HandleAlertsQuery(context.Background(), &alertQuery)
require.Nil(t, err2)
require.Equal(t, 1, len(alertQuery.Result))
})
t.Run("Alerts with same dashboard id and panel id should update", func(t *testing.T) {
setup(t)
modifiedItems := items
modifiedItems[0].Name = "Name"
err := store.SaveAlerts(context.Background(), testDash.Id, items)
t.Run("Can save alerts with same dashboard and panel id", func(t *testing.T) {
require.Nil(t, err)
})
t.Run("Alerts should be updated", func(t *testing.T) {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := store.HandleAlertsQuery(context.Background(), &query)
require.Nil(t, err2)
require.Equal(t, 1, len(query.Result))
require.Equal(t, "Name", query.Result[0].Name)
t.Run("Alert state should not be updated", func(t *testing.T) {
require.Equal(t, models.AlertStateUnknown, query.Result[0].State)
})
})
t.Run("Updates without changes should be ignored", func(t *testing.T) {
err3 := store.SaveAlerts(context.Background(), testDash.Id, items)
require.Nil(t, err3)
})
})
t.Run("Multiple alerts per dashboard", func(t *testing.T) {
setup(t)
multipleItems := []*models.Alert{
{
DashboardId: testDash.Id,
PanelId: 1,
Name: "1",
OrgId: 1,
Settings: simplejson.New(),
},
{
DashboardId: testDash.Id,
PanelId: 2,
Name: "2",
OrgId: 1,
Settings: simplejson.New(),
},
{
DashboardId: testDash.Id,
PanelId: 3,
Name: "3",
OrgId: 1,
Settings: simplejson.New(),
},
}
err := store.SaveAlerts(context.Background(), testDash.Id, multipleItems)
t.Run("Should save 3 dashboards", func(t *testing.T) {
require.Nil(t, err)
queryForDashboard := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := store.HandleAlertsQuery(context.Background(), &queryForDashboard)
require.Nil(t, err2)
require.Equal(t, 3, len(queryForDashboard.Result))
})
t.Run("should updated two dashboards and delete one", func(t *testing.T) {
missingOneAlert := multipleItems[:2]
err = store.SaveAlerts(context.Background(), testDash.Id, missingOneAlert)
t.Run("should delete the missing alert", func(t *testing.T) {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := store.HandleAlertsQuery(context.Background(), &query)
require.Nil(t, err2)
require.Equal(t, 2, len(query.Result))
})
})
})
t.Run("When dashboard is removed", func(t *testing.T) {
setup(t)
items := []*models.Alert{
{
PanelId: 1,
DashboardId: testDash.Id,
Name: "Alerting title",
Message: "Alerting message",
},
}
err := store.SaveAlerts(context.Background(), testDash.Id, items)
require.Nil(t, err)
err = store.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
dash := models.Dashboard{Id: testDash.Id, OrgId: 1}
_, err := sess.Delete(dash)
return err
})
require.Nil(t, err)
t.Run("Alerts should be removed", func(t *testing.T) {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := store.HandleAlertsQuery(context.Background(), &query)
require.Nil(t, err2)
require.Equal(t, 0, len(query.Result))
})
})
}
func TestIntegrationPausingAlerts(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
mockTimeNow()
defer resetTimeNow()
t.Run("Given an alert", func(t *testing.T) {
sqlStore := sqlStore{db: sqlstore.InitTestDB(t), log: log.New()}
testDash := insertTestDashboard(t, sqlStore.db, "dashboard with alerts", 1, 0, false, "alert")
alert, err := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New(), sqlStore)
require.Nil(t, err)
stateDateBeforePause := alert.NewStateDate
stateDateAfterPause := stateDateBeforePause
// Get alert so we can use its ID in tests
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := sqlStore.HandleAlertsQuery(context.Background(), &alertQuery)
require.Nil(t, err2)
insertedAlert := alertQuery.Result[0]
t.Run("when paused", func(t *testing.T) {
_, err := sqlStore.pauseAlert(t, testDash.OrgId, insertedAlert.Id, true)
require.Nil(t, err)
t.Run("the NewStateDate should be updated", func(t *testing.T) {
alert, err := getAlertById(t, insertedAlert.Id, &sqlStore)
require.Nil(t, err)
stateDateAfterPause = alert.NewStateDate
require.True(t, stateDateBeforePause.Before(stateDateAfterPause))
})
})
t.Run("when unpaused", func(t *testing.T) {
_, err := sqlStore.pauseAlert(t, testDash.OrgId, insertedAlert.Id, false)
require.Nil(t, err)
t.Run("the NewStateDate should be updated again", func(t *testing.T) {
alert, err := getAlertById(t, insertedAlert.Id, &sqlStore)
require.Nil(t, err)
stateDateAfterUnpause := alert.NewStateDate
require.True(t, stateDateAfterPause.Before(stateDateAfterUnpause))
})
})
})
}
func (ss *sqlStore) pauseAlert(t *testing.T, orgId int64, alertId int64, pauseState bool) (int64, error) {
cmd := &models.PauseAlertCommand{
OrgId: orgId,
AlertIds: []int64{alertId},
Paused: pauseState,
}
err := ss.PauseAlert(context.Background(), cmd)
require.Nil(t, err)
return cmd.ResultCount, err
}
func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json, ss sqlStore) (*models.Alert, error) {
items := []*models.Alert{
{
PanelId: 1,
DashboardId: dashId,
OrgId: orgId,
Name: title,
Message: message,
Settings: settings,
Frequency: 1,
},
}
err := ss.SaveAlerts(context.Background(), dashId, items)
return items[0], err
}
func getAlertById(t *testing.T, id int64, ss *sqlStore) (*models.Alert, error) {
q := &models.GetAlertByIdQuery{
Id: id,
}
err := ss.GetAlertById(context.Background(), q)
require.Nil(t, err)
return q.Result, err
}
func (ss *sqlStore) pauseAllAlerts(t *testing.T, pauseState bool) error {
cmd := &models.PauseAllAlertCommand{
Paused: pauseState,
}
err := ss.PauseAllAlerts(context.Background(), cmd)
require.Nil(t, err)
return err
}
func insertTestDashboard(t *testing.T, db db.DB, title string, orgId int64,
folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
t.Helper()
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: isFolder,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
var dash *models.Dashboard
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
dash = cmd.GetDashboardModel()
dash.SetVersion(1)
dash.Created = time.Now()
dash.Updated = time.Now()
dash.Uid = util.GenerateShortUID()
_, err := sess.Insert(dash)
return err
})
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.Id)
dash.Data.Set("uid", dash.Uid)
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
dashVersion := &dashver.DashboardVersion{
DashboardID: dash.Id,
ParentVersion: dash.Version,
RestoredFrom: cmd.RestoredFrom,
Version: dash.Version,
Created: time.Now(),
CreatedBy: dash.UpdatedBy,
Message: cmd.Message,
Data: dash.Data,
}
require.NoError(t, err)
if affectedRows, err := sess.Insert(dashVersion); err != nil {
return err
} else if affectedRows == 0 {
return dashboards.ErrDashboardNotFound
}
return nil
})
require.NoError(t, err)
return dash
}
+1 -1
View File
@@ -57,7 +57,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
ID: rand.Int63(),
}
ctx := NewEvalContext(context.Background(), testRule, fakeRequestValidator{}, nil, nil)
ctx := NewEvalContext(context.Background(), testRule, fakeRequestValidator{}, nil, nil, nil)
if cmd.Settings.Get("uploadImage").MustBool(true) {
ctx.ImagePublicURL = "https://grafana.com/assets/img/blog/mixed_styles.png"
}
+2 -2
View File
@@ -25,14 +25,14 @@ func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID
if alert.PanelId != panelID {
continue
}
rule, err := NewRuleFromDBAlert(context.Background(), e.sqlStore, alert, true)
rule, err := NewRuleFromDBAlert(context.Background(), e.AlertStore, alert, true)
if err != nil {
return nil, err
}
handler := NewEvalHandler(e.DataService)
context := NewEvalContext(context.Background(), rule, fakeRequestValidator{}, e.sqlStore, nil)
context := NewEvalContext(context.Background(), rule, fakeRequestValidator{}, e.AlertStore, nil, e.datasourceService)
context.IsTestRun = true
context.IsDebug = true