mirror of https://github.com/grafana/grafana.git
Alerting: Add "Keep Last State" backend functionality (#83940)
* Implement keep last state for state transitions * Respect For duration when keeping state * Only keep transition from recording an annotation * Add keep last state option for nodata/error in UI
This commit is contained in:
parent
fe1ed0a9e1
commit
10dc6c6d75
|
|
@ -235,7 +235,7 @@ func transNoData(l log.Logger, s string) ngmodels.NoDataState {
|
|||
case legacymodels.NoDataSetAlerting:
|
||||
return ngmodels.Alerting
|
||||
case legacymodels.NoDataKeepState:
|
||||
return ngmodels.NoData // "keep last state" translates to no data because we now emit a special alert when the state is "noData". The result is that the evaluation will not return firing and instead we'll raise the special alert.
|
||||
return ngmodels.KeepLast
|
||||
default:
|
||||
l.Warn("Unable to translate execution of NoData state. Using default execution", "old", s, "new", ngmodels.NoData)
|
||||
return ngmodels.NoData
|
||||
|
|
@ -247,9 +247,7 @@ func transExecErr(l log.Logger, s string) ngmodels.ExecutionErrorState {
|
|||
case "", legacymodels.ExecutionErrorSetAlerting:
|
||||
return ngmodels.AlertingErrState
|
||||
case legacymodels.ExecutionErrorKeepState:
|
||||
// Keep last state is translated to error as we now emit a
|
||||
// DatasourceError alert when the state is error
|
||||
return ngmodels.ErrorErrState
|
||||
return ngmodels.KeepLastErrState
|
||||
case legacymodels.ExecutionErrorSetOk:
|
||||
return ngmodels.OkErrState
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
|
@ -53,6 +54,8 @@ func NoDataStateFromString(state string) (NoDataState, error) {
|
|||
return NoData, nil
|
||||
case string(OK):
|
||||
return OK, nil
|
||||
case string(KeepLast):
|
||||
return KeepLast, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown NoData state option %s", state)
|
||||
}
|
||||
|
|
@ -62,6 +65,7 @@ const (
|
|||
Alerting NoDataState = "Alerting"
|
||||
NoData NoDataState = "NoData"
|
||||
OK NoDataState = "OK"
|
||||
KeepLast NoDataState = "KeepLast"
|
||||
)
|
||||
|
||||
// swagger:enum ExecutionErrorState
|
||||
|
|
@ -79,6 +83,8 @@ func ErrStateFromString(opt string) (ExecutionErrorState, error) {
|
|||
return ErrorErrState, nil
|
||||
case string(OkErrState):
|
||||
return OkErrState, nil
|
||||
case string(KeepLastErrState):
|
||||
return KeepLastErrState, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown Error state option %s", opt)
|
||||
}
|
||||
|
|
@ -88,6 +94,7 @@ const (
|
|||
AlertingErrState ExecutionErrorState = "Alerting"
|
||||
ErrorErrState ExecutionErrorState = "Error"
|
||||
OkErrState ExecutionErrorState = "OK"
|
||||
KeepLastErrState ExecutionErrorState = "KeepLast"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -137,8 +144,13 @@ const (
|
|||
StateReasonPaused = "Paused"
|
||||
StateReasonUpdated = "Updated"
|
||||
StateReasonRuleDeleted = "RuleDeleted"
|
||||
StateReasonKeepLast = "KeepLast"
|
||||
)
|
||||
|
||||
func ConcatReasons(reasons ...string) string {
|
||||
return strings.Join(reasons, ", ")
|
||||
}
|
||||
|
||||
var (
|
||||
// InternalLabelNameSet are labels that grafana automatically include as part of the labelset.
|
||||
InternalLabelNameSet = map[string]struct{}{
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ func ShouldRecordAnnotation(t state.StateTransition) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Do not log transitions when keeping last state
|
||||
toKeepLast := strings.Contains(t.StateReason, models.StateReasonKeepLast) && !strings.Contains(t.PreviousStateReason, models.StateReasonKeepLast)
|
||||
if toKeepLast {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not record transitions between Normal and Normal (NoData)
|
||||
if t.State.State == eval.Normal && t.PreviousState == eval.Normal {
|
||||
if (t.State.StateReason == "" && t.PreviousStateReason == models.StateReasonNoData) ||
|
||||
|
|
|
|||
|
|
@ -316,14 +316,14 @@ func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time
|
|||
}
|
||||
|
||||
func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels, logger log.Logger) []StateTransition {
|
||||
if st.applyNoDataAndErrorToAllStates && results.IsNoData() && (alertRule.NoDataState == ngModels.Alerting || alertRule.NoDataState == ngModels.OK) { // If it is no data, check the mapping and switch all results to the new state
|
||||
if st.applyNoDataAndErrorToAllStates && results.IsNoData() && (alertRule.NoDataState == ngModels.Alerting || alertRule.NoDataState == ngModels.OK || alertRule.NoDataState == ngModels.KeepLast) { // If it is no data, check the mapping and switch all results to the new state
|
||||
// TODO aggregate UID of datasources that returned NoData into one and provide as auxiliary info, probably annotation
|
||||
transitions := st.setNextStateForAll(ctx, alertRule, results[0], logger)
|
||||
if len(transitions) > 0 {
|
||||
return transitions // if there are no current states for the rule. Create ones for each result
|
||||
}
|
||||
}
|
||||
if st.applyNoDataAndErrorToAllStates && results.IsError() && (alertRule.ExecErrState == ngModels.AlertingErrState || alertRule.ExecErrState == ngModels.OkErrState) {
|
||||
if st.applyNoDataAndErrorToAllStates && results.IsError() && (alertRule.ExecErrState == ngModels.AlertingErrState || alertRule.ExecErrState == ngModels.OkErrState || alertRule.ExecErrState == ngModels.KeepLastErrState) {
|
||||
// TODO squash all errors into one, and provide as annotation
|
||||
transitions := st.setNextStateForAll(ctx, alertRule, results[0], logger)
|
||||
if len(transitions) > 0 {
|
||||
|
|
@ -352,6 +352,7 @@ func (st *Manager) setNextStateForAll(ctx context.Context, alertRule *ngModels.A
|
|||
// Set the current state based on evaluation results
|
||||
func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRule, currentState *State, result eval.Result, logger log.Logger) StateTransition {
|
||||
start := st.clock.Now()
|
||||
|
||||
currentState.LastEvaluationTime = result.EvaluatedAt
|
||||
currentState.EvaluationDuration = result.EvaluationDuration
|
||||
currentState.Results = append(currentState.Results, Evaluation{
|
||||
|
|
@ -392,10 +393,10 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu
|
|||
switch result.State {
|
||||
case eval.Normal:
|
||||
logger.Debug("Setting next state", "handler", "resultNormal")
|
||||
resultNormal(currentState, alertRule, result, logger)
|
||||
resultNormal(currentState, alertRule, result, logger, "")
|
||||
case eval.Alerting:
|
||||
logger.Debug("Setting next state", "handler", "resultAlerting")
|
||||
resultAlerting(currentState, alertRule, result, logger)
|
||||
resultAlerting(currentState, alertRule, result, logger, "")
|
||||
case eval.Error:
|
||||
logger.Debug("Setting next state", "handler", "resultError")
|
||||
resultError(currentState, alertRule, result, logger)
|
||||
|
|
@ -412,7 +413,7 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu
|
|||
if currentState.State != result.State &&
|
||||
result.State != eval.Normal &&
|
||||
result.State != eval.Alerting {
|
||||
currentState.StateReason = result.State.String()
|
||||
currentState.StateReason = resultStateReason(result, alertRule)
|
||||
}
|
||||
|
||||
// Set Resolved property so the scheduler knows to send a postable alert
|
||||
|
|
@ -446,6 +447,14 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu
|
|||
return nextState
|
||||
}
|
||||
|
||||
func resultStateReason(result eval.Result, rule *ngModels.AlertRule) string {
|
||||
if rule.ExecErrState == ngModels.KeepLastErrState || rule.NoDataState == ngModels.KeepLast {
|
||||
return ngModels.ConcatReasons(result.State.String(), ngModels.StateReasonKeepLast)
|
||||
}
|
||||
|
||||
return result.State.String()
|
||||
}
|
||||
|
||||
func (st *Manager) GetAll(orgID int64) []*State {
|
||||
allStates := st.cache.getAll(orgID, st.doNotSaveNormalState)
|
||||
return allStates
|
||||
|
|
|
|||
|
|
@ -812,6 +812,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
ngmodels.NoData: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.NoData)),
|
||||
ngmodels.Alerting: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.Alerting)),
|
||||
ngmodels.OK: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.OK)),
|
||||
ngmodels.KeepLast: baseRuleWith(ngmodels.WithNoDataExecAs(ngmodels.KeepLast)),
|
||||
}
|
||||
|
||||
type noDataTestCase struct {
|
||||
|
|
@ -919,6 +920,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t1: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -985,6 +1004,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -1025,6 +1062,25 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1202,6 +1258,55 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels2"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
Resolved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -1345,6 +1450,76 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels2"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t2.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels2"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1533,6 +1708,53 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels2"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -1663,6 +1885,70 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels2"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels2"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1734,6 +2020,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Alerting,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -1774,6 +2078,25 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Alerting,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1889,6 +2212,39 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Normal),
|
||||
newEvaluation(t3, eval.Normal),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1955,6 +2311,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -1995,10 +2369,29 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "t1[{}:alerting] t2[NoData] t3[NoData] at t3",
|
||||
desc: "t1[{}:alerting] t2[NoData] t3[NoData] at t2,t3",
|
||||
results: map[time.Time]eval.Results{
|
||||
t1: {
|
||||
newResult(eval.WithState(eval.Alerting)),
|
||||
|
|
@ -2129,6 +2522,41 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
Resolved: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule + no-data"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -2208,6 +2636,44 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t2.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Alerting,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.NoData),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2294,6 +2760,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Alerting,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.NoDataState]map[time.Time][]StateTransition{
|
||||
ngmodels.Alerting: {
|
||||
|
|
@ -2334,6 +2818,25 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLast: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Alerting,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -2350,6 +2853,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
ngmodels.ErrorErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.ErrorErrState)),
|
||||
ngmodels.AlertingErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.AlertingErrState)),
|
||||
ngmodels.OkErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.OkErrState)),
|
||||
ngmodels.KeepLastErrState: baseRuleWith(ngmodels.WithErrorExecAs(ngmodels.KeepLastErrState)),
|
||||
}
|
||||
|
||||
cacheID := func(lbls data.Labels) string {
|
||||
|
|
@ -2472,6 +2976,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t1: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Error),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2540,6 +3062,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t1: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Error),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2613,6 +3153,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.ExecutionErrorState]map[time.Time][]StateTransition{
|
||||
ngmodels.AlertingErrState: {
|
||||
|
|
@ -2652,6 +3210,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2724,6 +3300,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTransitionsApplyNoDataErrorToAllStates: map[ngmodels.ExecutionErrorState]map[time.Time][]StateTransition{
|
||||
ngmodels.AlertingErrState: {
|
||||
|
|
@ -2765,6 +3359,25 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2886,6 +3499,39 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
State: eval.Normal,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Normal),
|
||||
newEvaluation(t3, eval.Normal),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.StateReasonMissingSeries,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Error),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t3,
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -2961,6 +3607,25 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -3049,6 +3714,24 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Alerting,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t2,
|
||||
EndsAt: t2.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -3178,6 +3861,42 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Pending,
|
||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Alerting),
|
||||
newEvaluation(t2, eval.Error),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
t3: {
|
||||
{
|
||||
PreviousState: eval.Pending,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Alerting,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t2, eval.Error),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: t3.Add(ResendDelay * 4),
|
||||
LastEvaluationTime: t3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -3248,6 +3967,25 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ngmodels.KeepLastErrState: {
|
||||
t2: {
|
||||
{
|
||||
PreviousState: eval.Normal,
|
||||
PreviousStateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||
State: &State{
|
||||
Labels: labels["system + rule"],
|
||||
State: eval.Normal,
|
||||
Results: []Evaluation{
|
||||
newEvaluation(t1, eval.Error),
|
||||
newEvaluation(t2, eval.Normal),
|
||||
},
|
||||
StartsAt: t1,
|
||||
EndsAt: t1,
|
||||
LastEvaluationTime: t2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -875,6 +875,76 @@ func TestProcessEvalResults(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "normal -> normal (NoData, KeepLastState) -> alerting -> alerting (NoData, KeepLastState) - keeps last state when result is NoData and NoDataState is KeepLast",
|
||||
alertRule: baseRuleWith(models.WithForNTimes(0), models.WithNoDataExecAs(models.KeepLast)),
|
||||
evalResults: map[time.Time]eval.Results{
|
||||
t1: {
|
||||
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
|
||||
},
|
||||
t2: {
|
||||
newResult(eval.WithState(eval.NoData), eval.WithLabels(labels1)), // TODO fix it because NoData does not have same labels
|
||||
},
|
||||
t3: {
|
||||
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
|
||||
},
|
||||
tn(4): {
|
||||
newResult(eval.WithState(eval.NoData), eval.WithLabels(labels1)), // TODO fix it because NoData does not have same labels
|
||||
},
|
||||
},
|
||||
expectedAnnotations: 1,
|
||||
expectedStates: []*state.State{
|
||||
{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
ResultFingerprint: labels1.Fingerprint(),
|
||||
State: eval.Alerting,
|
||||
StateReason: models.ConcatReasons(eval.NoData.String(), models.StateReasonKeepLast),
|
||||
Results: []state.Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.NoData),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
newEvaluation(tn(4), eval.NoData),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: tn(4).Add(state.ResendDelay * 4),
|
||||
LastEvaluationTime: tn(4),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "normal -> pending -> pending (NoData, KeepLastState) -> alerting (NoData, KeepLastState) - keep last state respects For when result is NoData",
|
||||
alertRule: baseRuleWith(models.WithForNTimes(2), models.WithNoDataExecAs(models.KeepLast)),
|
||||
evalResults: map[time.Time]eval.Results{
|
||||
t1: {
|
||||
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
|
||||
},
|
||||
t2: {
|
||||
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
|
||||
},
|
||||
t3: {
|
||||
newResult(eval.WithState(eval.NoData), eval.WithLabels(labels1)), // TODO fix it because NoData does not have same labels
|
||||
},
|
||||
tn(4): {
|
||||
newResult(eval.WithState(eval.NoData), eval.WithLabels(labels1)), // TODO fix it because NoData does not have same labels
|
||||
},
|
||||
},
|
||||
expectedAnnotations: 2,
|
||||
expectedStates: []*state.State{
|
||||
{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
ResultFingerprint: labels1.Fingerprint(),
|
||||
State: eval.Alerting,
|
||||
StateReason: models.ConcatReasons(eval.NoData.String(), models.StateReasonKeepLast),
|
||||
Results: []state.Evaluation{
|
||||
newEvaluation(t3, eval.NoData),
|
||||
newEvaluation(tn(4), eval.NoData),
|
||||
},
|
||||
StartsAt: tn(4),
|
||||
EndsAt: tn(4).Add(state.ResendDelay * 4),
|
||||
LastEvaluationTime: tn(4),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "normal -> normal when result is NoData and NoDataState is ok",
|
||||
alertRule: baseRuleWith(models.WithNoDataExecAs(models.OK)),
|
||||
|
|
@ -1012,6 +1082,76 @@ func TestProcessEvalResults(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "normal -> normal (Error, KeepLastState) -> alerting -> alerting (Error, KeepLastState) - keeps last state when result is Error and ExecErrState is KeepLast",
|
||||
alertRule: baseRuleWith(models.WithForNTimes(0), models.WithErrorExecAs(models.KeepLastErrState)),
|
||||
evalResults: map[time.Time]eval.Results{
|
||||
t1: {
|
||||
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
|
||||
},
|
||||
t2: {
|
||||
newResult(eval.WithError(expr.MakeQueryError("A", "datasource_uid_1", errors.New("this is an error"))), eval.WithLabels(labels1)), // TODO fix it because error labels are different
|
||||
},
|
||||
t3: {
|
||||
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
|
||||
},
|
||||
tn(4): {
|
||||
newResult(eval.WithError(expr.MakeQueryError("A", "datasource_uid_1", errors.New("this is an error"))), eval.WithLabels(labels1)), // TODO fix it because error labels are different
|
||||
},
|
||||
},
|
||||
expectedAnnotations: 1,
|
||||
expectedStates: []*state.State{
|
||||
{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
ResultFingerprint: labels1.Fingerprint(),
|
||||
State: eval.Alerting,
|
||||
StateReason: models.ConcatReasons(eval.Error.String(), models.StateReasonKeepLast),
|
||||
Results: []state.Evaluation{
|
||||
newEvaluation(t1, eval.Normal),
|
||||
newEvaluation(t2, eval.Error),
|
||||
newEvaluation(t3, eval.Alerting),
|
||||
newEvaluation(tn(4), eval.Error),
|
||||
},
|
||||
StartsAt: t3,
|
||||
EndsAt: tn(4).Add(state.ResendDelay * 4),
|
||||
LastEvaluationTime: tn(4),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "normal -> pending -> pending (Error, KeepLastState) -> alerting (Error, KeepLastState) - keep last state respects For when result is Error",
|
||||
alertRule: baseRuleWith(models.WithForNTimes(2), models.WithErrorExecAs(models.KeepLastErrState)),
|
||||
evalResults: map[time.Time]eval.Results{
|
||||
t1: {
|
||||
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
|
||||
},
|
||||
t2: {
|
||||
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
|
||||
},
|
||||
t3: {
|
||||
newResult(eval.WithError(expr.MakeQueryError("A", "datasource_uid_1", errors.New("this is an error"))), eval.WithLabels(labels1)), // TODO fix it because error labels are different
|
||||
},
|
||||
tn(4): {
|
||||
newResult(eval.WithError(expr.MakeQueryError("A", "datasource_uid_1", errors.New("this is an error"))), eval.WithLabels(labels1)), // TODO fix it because error labels are different
|
||||
},
|
||||
},
|
||||
expectedAnnotations: 2,
|
||||
expectedStates: []*state.State{
|
||||
{
|
||||
Labels: labels["system + rule + labels1"],
|
||||
ResultFingerprint: labels1.Fingerprint(),
|
||||
State: eval.Alerting,
|
||||
StateReason: models.ConcatReasons(eval.Error.String(), models.StateReasonKeepLast),
|
||||
Results: []state.Evaluation{
|
||||
newEvaluation(t3, eval.Error),
|
||||
newEvaluation(tn(4), eval.Error),
|
||||
},
|
||||
StartsAt: tn(4),
|
||||
EndsAt: tn(4).Add(state.ResendDelay * 4),
|
||||
LastEvaluationTime: tn(4),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "normal -> normal when result is Error and ExecErrState is OK",
|
||||
alertRule: baseRuleWith(models.WithForNTimes(6), models.WithErrorExecAs(models.OkErrState)),
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float
|
|||
return result
|
||||
}
|
||||
|
||||
func resultNormal(state *State, _ *models.AlertRule, result eval.Result, logger log.Logger) {
|
||||
func resultNormal(state *State, _ *models.AlertRule, result eval.Result, logger log.Logger, reason string) {
|
||||
if state.State == eval.Normal {
|
||||
logger.Debug("Keeping state", "state", state.State)
|
||||
} else {
|
||||
|
|
@ -206,11 +206,11 @@ func resultNormal(state *State, _ *models.AlertRule, result eval.Result, logger
|
|||
"next_ends_at",
|
||||
nextEndsAt)
|
||||
// Normal states have the same start and end timestamps
|
||||
state.SetNormal("", nextEndsAt, nextEndsAt)
|
||||
state.SetNormal(reason, nextEndsAt, nextEndsAt)
|
||||
}
|
||||
}
|
||||
|
||||
func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) {
|
||||
func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger, reason string) {
|
||||
switch state.State {
|
||||
case eval.Alerting:
|
||||
prevEndsAt := state.EndsAt
|
||||
|
|
@ -235,7 +235,7 @@ func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, lo
|
|||
state.EndsAt,
|
||||
"next_ends_at",
|
||||
nextEndsAt)
|
||||
state.SetAlerting("", result.EvaluatedAt, nextEndsAt)
|
||||
state.SetAlerting(reason, result.EvaluatedAt, nextEndsAt)
|
||||
}
|
||||
default:
|
||||
nextEndsAt := nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt)
|
||||
|
|
@ -250,7 +250,7 @@ func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, lo
|
|||
state.EndsAt,
|
||||
"next_ends_at",
|
||||
nextEndsAt)
|
||||
state.SetPending("", result.EvaluatedAt, nextEndsAt)
|
||||
state.SetPending(reason, result.EvaluatedAt, nextEndsAt)
|
||||
} else {
|
||||
logger.Debug("Changing state",
|
||||
"previous_state",
|
||||
|
|
@ -261,18 +261,20 @@ func resultAlerting(state *State, rule *models.AlertRule, result eval.Result, lo
|
|||
state.EndsAt,
|
||||
"next_ends_at",
|
||||
nextEndsAt)
|
||||
state.SetAlerting("", result.EvaluatedAt, nextEndsAt)
|
||||
state.SetAlerting(reason, result.EvaluatedAt, nextEndsAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resultError(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) {
|
||||
handlerStr := "resultError"
|
||||
|
||||
switch rule.ExecErrState {
|
||||
case models.AlertingErrState:
|
||||
logger.Debug("Execution error state is Alerting", "handler", "resultAlerting", "previous_handler", "resultError")
|
||||
resultAlerting(state, rule, result, logger)
|
||||
logger.Debug("Execution error state is Alerting", "handler", "resultAlerting", "previous_handler", handlerStr)
|
||||
resultAlerting(state, rule, result, logger, models.StateReasonError)
|
||||
// This is a special case where Alerting and Pending should also have an error and reason
|
||||
state.Error = result.Error
|
||||
state.StateReason = models.StateReasonError
|
||||
case models.ErrorErrState:
|
||||
if state.State == eval.Error {
|
||||
prevEndsAt := state.EndsAt
|
||||
|
|
@ -316,8 +318,11 @@ func resultError(state *State, rule *models.AlertRule, result eval.Result, logge
|
|||
}
|
||||
}
|
||||
case models.OkErrState:
|
||||
logger.Debug("Execution error state is Normal", "handler", "resultNormal", "previous_handler", "resultError")
|
||||
resultNormal(state, rule, result, logger)
|
||||
logger.Debug("Execution error state is Normal", "handler", "resultNormal", "previous_handler", handlerStr)
|
||||
resultNormal(state, rule, result, logger, "") // TODO: Should we add a reason?
|
||||
case models.KeepLastErrState:
|
||||
logger := logger.New("previous_handler", handlerStr)
|
||||
resultKeepLast(state, rule, result, logger)
|
||||
default:
|
||||
err := fmt.Errorf("unsupported execution error state: %s", rule.ExecErrState)
|
||||
state.SetError(err, state.StartsAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt))
|
||||
|
|
@ -326,11 +331,12 @@ func resultError(state *State, rule *models.AlertRule, result eval.Result, logge
|
|||
}
|
||||
|
||||
func resultNoData(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) {
|
||||
handlerStr := "resultNoData"
|
||||
|
||||
switch rule.NoDataState {
|
||||
case models.Alerting:
|
||||
logger.Debug("Execution no data state is Alerting", "handler", "resultAlerting", "previous_handler", "resultNoData")
|
||||
resultAlerting(state, rule, result, logger)
|
||||
state.StateReason = models.StateReasonNoData
|
||||
logger.Debug("Execution no data state is Alerting", "handler", "resultAlerting", "previous_handler", handlerStr)
|
||||
resultAlerting(state, rule, result, logger, models.StateReasonNoData)
|
||||
case models.NoData:
|
||||
if state.State == eval.NoData {
|
||||
prevEndsAt := state.EndsAt
|
||||
|
|
@ -357,9 +363,11 @@ func resultNoData(state *State, rule *models.AlertRule, result eval.Result, logg
|
|||
state.SetNoData("", result.EvaluatedAt, nextEndsAt)
|
||||
}
|
||||
case models.OK:
|
||||
logger.Debug("Execution no data state is Normal", "handler", "resultNormal", "previous_handler", "resultNoData")
|
||||
resultNormal(state, rule, result, logger)
|
||||
state.StateReason = models.StateReasonNoData
|
||||
logger.Debug("Execution no data state is Normal", "handler", "resultNormal", "previous_handler", handlerStr)
|
||||
resultNormal(state, rule, result, logger, models.StateReasonNoData)
|
||||
case models.KeepLast:
|
||||
logger := logger.New("previous_handler", handlerStr)
|
||||
resultKeepLast(state, rule, result, logger)
|
||||
default:
|
||||
err := fmt.Errorf("unsupported no data state: %s", rule.NoDataState)
|
||||
state.SetError(err, state.StartsAt, nextEndsTime(rule.IntervalSeconds, result.EvaluatedAt))
|
||||
|
|
@ -367,6 +375,31 @@ func resultNoData(state *State, rule *models.AlertRule, result eval.Result, logg
|
|||
}
|
||||
}
|
||||
|
||||
func resultKeepLast(state *State, rule *models.AlertRule, result eval.Result, logger log.Logger) {
|
||||
reason := models.ConcatReasons(result.State.String(), models.StateReasonKeepLast)
|
||||
|
||||
switch state.State {
|
||||
case eval.Alerting:
|
||||
logger.Debug("Execution keep last state is Alerting", "handler", "resultAlerting")
|
||||
resultAlerting(state, rule, result, logger, reason)
|
||||
case eval.Pending:
|
||||
// respect 'for' setting on rule
|
||||
if result.EvaluatedAt.Sub(state.StartsAt) >= rule.For {
|
||||
logger.Debug("Execution keep last state is Pending", "handler", "resultAlerting")
|
||||
resultAlerting(state, rule, result, logger, reason)
|
||||
} else {
|
||||
logger.Debug("Ignoring set next state to pending")
|
||||
}
|
||||
case eval.Normal:
|
||||
logger.Debug("Execution keep last state is Normal", "handler", "resultNormal")
|
||||
resultNormal(state, rule, result, logger, reason)
|
||||
default:
|
||||
// this should not happen, add as failsafe
|
||||
logger.Debug("Reverting invalid state to normal", "handler", "resultNormal")
|
||||
resultNormal(state, rule, result, logger, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *State) NeedsSending(resendDelay time.Duration) bool {
|
||||
switch a.State {
|
||||
case eval.Pending:
|
||||
|
|
@ -486,21 +519,28 @@ func FormatStateAndReason(state eval.State, reason string) string {
|
|||
// ParseFormattedState parses a state string in the format "state (reason)"
|
||||
// and returns the state and reason separately.
|
||||
func ParseFormattedState(stateStr string) (eval.State, string, error) {
|
||||
split := strings.Split(stateStr, " ")
|
||||
if len(split) == 0 {
|
||||
return -1, "", errors.New("invalid state format")
|
||||
p := 0
|
||||
// walk string until we find a space
|
||||
for i, c := range stateStr {
|
||||
if c == ' ' {
|
||||
p = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if p == 0 {
|
||||
p = len(stateStr)
|
||||
}
|
||||
|
||||
state, err := eval.ParseStateString(split[0])
|
||||
state, err := eval.ParseStateString(stateStr[:p])
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
|
||||
var reason string
|
||||
if len(split) > 1 {
|
||||
reason = strings.Trim(split[1], "()")
|
||||
if p == len(stateStr) {
|
||||
return state, "", nil
|
||||
}
|
||||
|
||||
reason := strings.Trim(stateStr[p+1:], "()")
|
||||
return state, reason, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -681,6 +681,15 @@ func TestParseFormattedState(t *testing.T) {
|
|||
require.Equal(t, ngmodels.StateReasonMissingSeries, reason)
|
||||
})
|
||||
|
||||
t.Run("should parse formatted state with concatenated reasons", func(t *testing.T) {
|
||||
stateStr := "Normal (Error, KeepLast)"
|
||||
s, reason, err := ParseFormattedState(stateStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, eval.Normal, s)
|
||||
require.Equal(t, ngmodels.ConcatReasons(ngmodels.StateReasonError, ngmodels.StateReasonKeepLast), reason)
|
||||
})
|
||||
|
||||
t.Run("should error on empty string", func(t *testing.T) {
|
||||
stateStr := ""
|
||||
_, _, err := ParseFormattedState(stateStr)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const options: SelectableValue[] = [
|
|||
{ value: GrafanaAlertStateDecision.NoData, label: 'No Data' },
|
||||
{ value: GrafanaAlertStateDecision.OK, label: 'OK' },
|
||||
{ value: GrafanaAlertStateDecision.Error, label: 'Error' },
|
||||
{ value: GrafanaAlertStateDecision.KeepLast, label: 'Keep Last State' },
|
||||
];
|
||||
|
||||
export const GrafanaAlertStatePicker = ({ includeNoData, includeError, ...props }: Props) => {
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export interface RulerAlertingRuleDTO extends RulerRuleBaseDTO {
|
|||
export enum GrafanaAlertStateDecision {
|
||||
Alerting = 'Alerting',
|
||||
NoData = 'NoData',
|
||||
KeepLastState = 'KeepLastState',
|
||||
KeepLast = 'KeepLast',
|
||||
OK = 'OK',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue