mirror of https://github.com/grafana/grafana
Alerting: update test TestAlertingTicker to not rely on clock (#58544)
* extract method processTick * make processTick return scheduled rules * move state manager tests to state manager * update test * move all tests into one file * remove unused fieldspull/58561/head
parent
ee7348afee
commit
bad4f28d0d
@ -1,299 +0,0 @@ |
|||||||
package schedule_test |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"net/url" |
|
||||||
"runtime" |
|
||||||
"strings" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/benbjohnson/clock" |
|
||||||
"github.com/google/go-cmp/cmp" |
|
||||||
"github.com/google/go-cmp/cmp/cmpopts" |
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
|
||||||
"github.com/prometheus/client_golang/prometheus" |
|
||||||
"github.com/stretchr/testify/assert" |
|
||||||
"github.com/stretchr/testify/mock" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval" |
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/image" |
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics" |
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule" |
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/state" |
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/tests" |
|
||||||
"github.com/grafana/grafana/pkg/setting" |
|
||||||
) |
|
||||||
|
|
||||||
var testMetrics = metrics.NewNGAlert(prometheus.NewPedanticRegistry()) |
|
||||||
|
|
||||||
type evalAppliedInfo struct { |
|
||||||
alertDefKey models.AlertRuleKey |
|
||||||
now time.Time |
|
||||||
} |
|
||||||
|
|
||||||
func TestWarmStateCache(t *testing.T) { |
|
||||||
evaluationTime, err := time.Parse("2006-01-02", "2021-03-25") |
|
||||||
require.NoError(t, err) |
|
||||||
ctx := context.Background() |
|
||||||
_, dbstore := tests.SetupTestEnv(t, 1) |
|
||||||
|
|
||||||
const mainOrgID int64 = 1 |
|
||||||
rule := tests.CreateTestAlertRule(t, ctx, dbstore, 600, mainOrgID) |
|
||||||
|
|
||||||
expectedEntries := []*state.State{ |
|
||||||
{ |
|
||||||
AlertRuleUID: rule.UID, |
|
||||||
OrgID: rule.OrgID, |
|
||||||
CacheID: `[["test1","testValue1"]]`, |
|
||||||
Labels: data.Labels{"test1": "testValue1"}, |
|
||||||
State: eval.Normal, |
|
||||||
Results: []state.Evaluation{ |
|
||||||
{EvaluationTime: evaluationTime, EvaluationState: eval.Normal}, |
|
||||||
}, |
|
||||||
StartsAt: evaluationTime.Add(-1 * time.Minute), |
|
||||||
EndsAt: evaluationTime.Add(1 * time.Minute), |
|
||||||
LastEvaluationTime: evaluationTime, |
|
||||||
Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, |
|
||||||
}, { |
|
||||||
AlertRuleUID: rule.UID, |
|
||||||
OrgID: rule.OrgID, |
|
||||||
CacheID: `[["test2","testValue2"]]`, |
|
||||||
Labels: data.Labels{"test2": "testValue2"}, |
|
||||||
State: eval.Alerting, |
|
||||||
Results: []state.Evaluation{ |
|
||||||
{EvaluationTime: evaluationTime, EvaluationState: eval.Alerting}, |
|
||||||
}, |
|
||||||
StartsAt: evaluationTime.Add(-1 * time.Minute), |
|
||||||
EndsAt: evaluationTime.Add(1 * time.Minute), |
|
||||||
LastEvaluationTime: evaluationTime, |
|
||||||
Annotations: map[string]string{"testAnnoKey": "testAnnoValue"}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
labels := models.InstanceLabels{"test1": "testValue1"} |
|
||||||
_, hash, _ := labels.StringAndHash() |
|
||||||
instance1 := models.AlertInstance{ |
|
||||||
AlertInstanceKey: models.AlertInstanceKey{ |
|
||||||
RuleOrgID: rule.OrgID, |
|
||||||
RuleUID: rule.UID, |
|
||||||
LabelsHash: hash, |
|
||||||
}, |
|
||||||
CurrentState: models.InstanceStateNormal, |
|
||||||
LastEvalTime: evaluationTime, |
|
||||||
CurrentStateSince: evaluationTime.Add(-1 * time.Minute), |
|
||||||
CurrentStateEnd: evaluationTime.Add(1 * time.Minute), |
|
||||||
Labels: labels, |
|
||||||
} |
|
||||||
|
|
||||||
_ = dbstore.SaveAlertInstances(ctx, instance1) |
|
||||||
|
|
||||||
labels = models.InstanceLabels{"test2": "testValue2"} |
|
||||||
_, hash, _ = labels.StringAndHash() |
|
||||||
instance2 := models.AlertInstance{ |
|
||||||
AlertInstanceKey: models.AlertInstanceKey{ |
|
||||||
RuleOrgID: rule.OrgID, |
|
||||||
RuleUID: rule.UID, |
|
||||||
LabelsHash: hash, |
|
||||||
}, |
|
||||||
CurrentState: models.InstanceStateFiring, |
|
||||||
LastEvalTime: evaluationTime, |
|
||||||
CurrentStateSince: evaluationTime.Add(-1 * time.Minute), |
|
||||||
CurrentStateEnd: evaluationTime.Add(1 * time.Minute), |
|
||||||
Labels: labels, |
|
||||||
} |
|
||||||
_ = dbstore.SaveAlertInstances(ctx, instance2) |
|
||||||
st := state.NewManager(testMetrics.GetStateMetrics(), nil, dbstore, &image.NoopImageService{}, clock.NewMock(), &state.FakeHistorian{}) |
|
||||||
st.Warm(ctx, dbstore) |
|
||||||
|
|
||||||
t.Run("instance cache has expected entries", func(t *testing.T) { |
|
||||||
for _, entry := range expectedEntries { |
|
||||||
cacheEntry := st.Get(entry.OrgID, entry.AlertRuleUID, entry.CacheID) |
|
||||||
|
|
||||||
if diff := cmp.Diff(entry, cacheEntry, cmpopts.IgnoreFields(state.State{}, "Results")); diff != "" { |
|
||||||
t.Errorf("Result mismatch (-want +got):\n%s", diff) |
|
||||||
t.FailNow() |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func TestAlertingTicker(t *testing.T) { |
|
||||||
ctx := context.Background() |
|
||||||
_, dbstore := tests.SetupTestEnv(t, 1) |
|
||||||
|
|
||||||
alerts := make([]*models.AlertRule, 0) |
|
||||||
|
|
||||||
const mainOrgID int64 = 1 |
|
||||||
// create alert rule under main org with one second interval
|
|
||||||
alerts = append(alerts, tests.CreateTestAlertRule(t, ctx, dbstore, 1, mainOrgID)) |
|
||||||
|
|
||||||
evalAppliedCh := make(chan evalAppliedInfo, len(alerts)) |
|
||||||
stopAppliedCh := make(chan models.AlertRuleKey, len(alerts)) |
|
||||||
|
|
||||||
mockedClock := clock.NewMock() |
|
||||||
|
|
||||||
cfg := setting.UnifiedAlertingSettings{ |
|
||||||
BaseInterval: time.Second, |
|
||||||
AdminConfigPollInterval: 10 * time.Minute, // do not poll in unit tests.
|
|
||||||
} |
|
||||||
|
|
||||||
notifier := &schedule.AlertsSenderMock{} |
|
||||||
notifier.EXPECT().Send(mock.Anything, mock.Anything).Return() |
|
||||||
|
|
||||||
schedCfg := schedule.SchedulerCfg{ |
|
||||||
Cfg: cfg, |
|
||||||
C: mockedClock, |
|
||||||
EvalAppliedFunc: func(alertDefKey models.AlertRuleKey, now time.Time) { |
|
||||||
evalAppliedCh <- evalAppliedInfo{alertDefKey: alertDefKey, now: now} |
|
||||||
}, |
|
||||||
StopAppliedFunc: func(alertDefKey models.AlertRuleKey) { |
|
||||||
stopAppliedCh <- alertDefKey |
|
||||||
}, |
|
||||||
RuleStore: dbstore, |
|
||||||
Metrics: testMetrics.GetSchedulerMetrics(), |
|
||||||
AlertSender: notifier, |
|
||||||
} |
|
||||||
st := state.NewManager(testMetrics.GetStateMetrics(), nil, dbstore, &image.NoopImageService{}, clock.NewMock(), &state.FakeHistorian{}) |
|
||||||
appUrl := &url.URL{ |
|
||||||
Scheme: "http", |
|
||||||
Host: "localhost", |
|
||||||
} |
|
||||||
sched := schedule.NewScheduler(schedCfg, appUrl, st) |
|
||||||
|
|
||||||
go func() { |
|
||||||
err := sched.Run(ctx) |
|
||||||
require.NoError(t, err) |
|
||||||
}() |
|
||||||
runtime.Gosched() |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated := []models.AlertRuleKey{alerts[0].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 1st tick alert rules: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
|
|
||||||
// add alert rule under main org with three seconds interval
|
|
||||||
var threeSecInterval int64 = 3 |
|
||||||
alerts = append(alerts, tests.CreateTestAlertRule(t, ctx, dbstore, threeSecInterval, mainOrgID)) |
|
||||||
t.Logf("alert rule: %v added with interval: %d", alerts[1].GetKey(), threeSecInterval) |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated = []models.AlertRuleKey{alerts[0].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 2nd tick alert rule: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated = []models.AlertRuleKey{alerts[1].GetKey(), alerts[0].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 3rd tick alert rules: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated = []models.AlertRuleKey{alerts[0].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 4th tick alert rules: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
|
|
||||||
key := alerts[0].GetKey() |
|
||||||
err := dbstore.DeleteAlertRulesByUID(ctx, alerts[0].OrgID, alerts[0].UID) |
|
||||||
require.NoError(t, err) |
|
||||||
t.Logf("alert rule: %v deleted", key) |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated = []models.AlertRuleKey{} |
|
||||||
t.Run(fmt.Sprintf("on 5th tick alert rules: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
expectedAlertRulesStopped := []models.AlertRuleKey{alerts[0].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 5th tick alert rules: %s should be stopped", concatenate(expectedAlertRulesStopped)), func(t *testing.T) { |
|
||||||
assertStopRun(t, stopAppliedCh, expectedAlertRulesStopped...) |
|
||||||
}) |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated = []models.AlertRuleKey{alerts[1].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 6th tick alert rules: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
|
|
||||||
// create alert rule with one second interval
|
|
||||||
alerts = append(alerts, tests.CreateTestAlertRule(t, ctx, dbstore, 1, mainOrgID)) |
|
||||||
|
|
||||||
expectedAlertRulesEvaluated = []models.AlertRuleKey{alerts[2].GetKey()} |
|
||||||
t.Run(fmt.Sprintf("on 7th tick alert rules: %s should be evaluated", concatenate(expectedAlertRulesEvaluated)), func(t *testing.T) { |
|
||||||
tick := advanceClock(t, mockedClock) |
|
||||||
assertEvalRun(t, evalAppliedCh, tick, expectedAlertRulesEvaluated...) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func assertEvalRun(t *testing.T, ch <-chan evalAppliedInfo, tick time.Time, keys ...models.AlertRuleKey) { |
|
||||||
timeout := time.After(time.Second) |
|
||||||
|
|
||||||
expected := make(map[models.AlertRuleKey]struct{}, len(keys)) |
|
||||||
for _, k := range keys { |
|
||||||
expected[k] = struct{}{} |
|
||||||
} |
|
||||||
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case info := <-ch: |
|
||||||
_, ok := expected[info.alertDefKey] |
|
||||||
if !ok { |
|
||||||
t.Fatalf("alert rule: %v should not have been evaluated at: %v", info.alertDefKey, info.now) |
|
||||||
} |
|
||||||
t.Logf("alert rule: %v evaluated at: %v", info.alertDefKey, info.now) |
|
||||||
assert.Equal(t, tick, info.now) |
|
||||||
delete(expected, info.alertDefKey) |
|
||||||
case <-timeout: |
|
||||||
if len(expected) == 0 { |
|
||||||
return |
|
||||||
} |
|
||||||
t.Fatal("cycle has expired") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func assertStopRun(t *testing.T, ch <-chan models.AlertRuleKey, keys ...models.AlertRuleKey) { |
|
||||||
timeout := time.After(time.Second) |
|
||||||
|
|
||||||
expected := make(map[models.AlertRuleKey]struct{}, len(keys)) |
|
||||||
for _, k := range keys { |
|
||||||
expected[k] = struct{}{} |
|
||||||
} |
|
||||||
|
|
||||||
for { |
|
||||||
select { |
|
||||||
case alertDefKey := <-ch: |
|
||||||
_, ok := expected[alertDefKey] |
|
||||||
t.Logf("alert rule: %v stopped", alertDefKey) |
|
||||||
assert.True(t, ok) |
|
||||||
delete(expected, alertDefKey) |
|
||||||
if len(expected) == 0 { |
|
||||||
return |
|
||||||
} |
|
||||||
case <-timeout: |
|
||||||
if len(expected) == 0 { |
|
||||||
return |
|
||||||
} |
|
||||||
t.Fatal("cycle has expired") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func advanceClock(t *testing.T, mockedClock *clock.Mock) time.Time { |
|
||||||
mockedClock.Add(time.Second) |
|
||||||
return mockedClock.Now() |
|
||||||
// t.Logf("Tick: %v", mockedClock.Now())
|
|
||||||
} |
|
||||||
|
|
||||||
func concatenate(keys []models.AlertRuleKey) string { |
|
||||||
s := make([]string, len(keys)) |
|
||||||
for _, k := range keys { |
|
||||||
s = append(s, k.String()) |
|
||||||
} |
|
||||||
return fmt.Sprintf("[%s]", strings.Join(s, ",")) |
|
||||||
} |
|
Loading…
Reference in new issue