mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix stale values associated with states that have gone to NoData, unify values calculation (#89807)
* Unify values * Fix with latest changes on main * Fix up NaN test * Keep refIDs with -1 as value * Test that refIDs are preserved on Normal to Error transition * Alerting to err test too * Add a blurb to docs about this behavior
This commit is contained in:
parent
f763f2085b
commit
3b6a8775bb
@ -269,6 +269,8 @@ You can also configure the alert instance state when its evaluation returns an e
|
|||||||
| Normal | Sets alert instance state to `Normal`. |
|
| Normal | Sets alert instance state to `Normal`. |
|
||||||
| Keep Last State | Maintains the alert instance in its last state. Useful for mitigating temporary issues, refer to [Keep last state](ref:keep-last-state). |
|
| Keep Last State | Maintains the alert instance in its last state. Useful for mitigating temporary issues, refer to [Keep last state](ref:keep-last-state). |
|
||||||
|
|
||||||
|
When you configure the No data or Error behavior to `Alerting` or `Normal`, Grafana will attempt to keep a stable set of fields under notification `Values`. If your query returns no data or an error, Grafana re-uses the latest known set of fields in `Values`, but will use `-1` in place of the measured value.
|
||||||
|
|
||||||
## Create alerts from panels
|
## Create alerts from panels
|
||||||
|
|
||||||
Create alerts from any panel type. This means you can reuse the queries in the panel and create alerts based on them.
|
Create alerts from any panel type. This means you can reuse the queries in the panel and create alerts based on them.
|
||||||
|
@ -48,7 +48,7 @@ func Test_FormatValues(t *testing.T) {
|
|||||||
name: "with no value, it renders the evaluation string",
|
name: "with no value, it renders the evaluation string",
|
||||||
alertState: &state.State{
|
alertState: &state.State{
|
||||||
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
||||||
LatestResult: &state.Evaluation{Condition: "A", Values: map[string]*float64{}},
|
LatestResult: &state.Evaluation{Condition: "A", Values: map[string]float64{}},
|
||||||
},
|
},
|
||||||
expected: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
expected: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
||||||
},
|
},
|
||||||
@ -56,7 +56,7 @@ func Test_FormatValues(t *testing.T) {
|
|||||||
name: "with one value, it renders the single value",
|
name: "with one value, it renders the single value",
|
||||||
alertState: &state.State{
|
alertState: &state.State{
|
||||||
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
||||||
LatestResult: &state.Evaluation{Condition: "A", Values: map[string]*float64{"A": &val1}},
|
LatestResult: &state.Evaluation{Condition: "A", Values: map[string]float64{"A": val1}},
|
||||||
},
|
},
|
||||||
expected: "1.1e+00",
|
expected: "1.1e+00",
|
||||||
},
|
},
|
||||||
@ -64,7 +64,7 @@ func Test_FormatValues(t *testing.T) {
|
|||||||
name: "with two values, it renders the value based on their refID and position",
|
name: "with two values, it renders the value based on their refID and position",
|
||||||
alertState: &state.State{
|
alertState: &state.State{
|
||||||
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
||||||
LatestResult: &state.Evaluation{Condition: "B", Values: map[string]*float64{"B0": &val1, "B1": &val2}},
|
LatestResult: &state.Evaluation{Condition: "B", Values: map[string]float64{"B0": val1, "B1": val2}},
|
||||||
},
|
},
|
||||||
expected: "B0: 1.1e+00, B1: 1.4e+00",
|
expected: "B0: 1.1e+00, B1: 1.4e+00",
|
||||||
},
|
},
|
||||||
@ -72,7 +72,7 @@ func Test_FormatValues(t *testing.T) {
|
|||||||
name: "with a high number of values, it renders the value based on their refID and position using a natural order",
|
name: "with a high number of values, it renders the value based on their refID and position using a natural order",
|
||||||
alertState: &state.State{
|
alertState: &state.State{
|
||||||
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
||||||
LatestResult: &state.Evaluation{Condition: "B", Values: map[string]*float64{"B0": &val1, "B1": &val2, "B2": &val1, "B10": &val2, "B11": &val1}},
|
LatestResult: &state.Evaluation{Condition: "B", Values: map[string]float64{"B0": val1, "B1": val2, "B2": val1, "B10": val2, "B11": val1}},
|
||||||
},
|
},
|
||||||
expected: "B0: 1.1e+00, B10: 1.4e+00, B11: 1.1e+00, B1: 1.4e+00, B2: 1.1e+00",
|
expected: "B0: 1.1e+00, B10: 1.4e+00, B11: 1.1e+00, B1: 1.4e+00, B2: 1.1e+00",
|
||||||
},
|
},
|
||||||
@ -240,11 +240,10 @@ func TestRouteGetAlertStatuses(t *testing.T) {
|
|||||||
func withAlertingState() forEachState {
|
func withAlertingState() forEachState {
|
||||||
return func(s *state.State) *state.State {
|
return func(s *state.State) *state.State {
|
||||||
s.State = eval.Alerting
|
s.State = eval.Alerting
|
||||||
value := float64(1.1)
|
|
||||||
s.LatestResult = &state.Evaluation{
|
s.LatestResult = &state.Evaluation{
|
||||||
EvaluationState: eval.Alerting,
|
EvaluationState: eval.Alerting,
|
||||||
EvaluationTime: timeNow(),
|
EvaluationTime: timeNow(),
|
||||||
Values: map[string]*float64{"B": &value},
|
Values: map[string]float64{"B": float64(1.1)},
|
||||||
Condition: "B",
|
Condition: "B",
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
|
@ -86,7 +86,7 @@ func (f *fakeAlertInstanceManager) GenerateAlertInstances(orgID int64, alertRule
|
|||||||
LatestResult: &state.Evaluation{
|
LatestResult: &state.Evaluation{
|
||||||
EvaluationTime: evaluationTime.Add(1 * time.Minute),
|
EvaluationTime: evaluationTime.Add(1 * time.Minute),
|
||||||
EvaluationState: eval.Normal,
|
EvaluationState: eval.Normal,
|
||||||
Values: make(map[string]*float64),
|
Values: make(map[string]float64),
|
||||||
},
|
},
|
||||||
LastEvaluationTime: evaluationTime.Add(1 * time.Minute),
|
LastEvaluationTime: evaluationTime.Add(1 * time.Minute),
|
||||||
EvaluationDuration: evaluationDuration,
|
EvaluationDuration: evaluationDuration,
|
||||||
|
@ -80,6 +80,12 @@ func WithLabels(labels data.Labels) ResultMutator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithValues(values map[string]NumberValueCapture) ResultMutator {
|
||||||
|
return func(r *Result) {
|
||||||
|
r.Values = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type FakeLoadedMetricsReader struct {
|
type FakeLoadedMetricsReader struct {
|
||||||
fingerprints map[data.Fingerprint]struct{}
|
fingerprints map[data.Fingerprint]struct{}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package state
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -157,15 +156,6 @@ func calculateState(ctx context.Context, log log.Logger, alertRule *ngModels.Ale
|
|||||||
labels, _ := expand(ctx, log, alertRule.Title, alertRule.Labels, templateData, externalURL, result.EvaluatedAt)
|
labels, _ := expand(ctx, log, alertRule.Title, alertRule.Labels, templateData, externalURL, result.EvaluatedAt)
|
||||||
annotations, _ := expand(ctx, log, alertRule.Title, alertRule.Annotations, templateData, externalURL, result.EvaluatedAt)
|
annotations, _ := expand(ctx, log, alertRule.Title, alertRule.Annotations, templateData, externalURL, result.EvaluatedAt)
|
||||||
|
|
||||||
values := make(map[string]float64)
|
|
||||||
for refID, v := range result.Values {
|
|
||||||
if v.Value != nil {
|
|
||||||
values[refID] = *v.Value
|
|
||||||
} else {
|
|
||||||
values[refID] = math.NaN()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lbs := make(data.Labels, len(extraLabels)+len(labels)+len(resultLabels))
|
lbs := make(data.Labels, len(extraLabels)+len(labels)+len(resultLabels))
|
||||||
dupes := make(data.Labels)
|
dupes := make(data.Labels)
|
||||||
for key, val := range extraLabels {
|
for key, val := range extraLabels {
|
||||||
@ -210,7 +200,6 @@ func calculateState(ctx context.Context, log log.Logger, alertRule *ngModels.Ale
|
|||||||
Labels: lbs,
|
Labels: lbs,
|
||||||
Annotations: annotations,
|
Annotations: annotations,
|
||||||
EvaluationDuration: result.EvaluationDuration,
|
EvaluationDuration: result.EvaluationDuration,
|
||||||
Values: values,
|
|
||||||
StartsAt: result.EvaluatedAt,
|
StartsAt: result.EvaluatedAt,
|
||||||
EndsAt: result.EvaluatedAt,
|
EndsAt: result.EvaluatedAt,
|
||||||
ResultFingerprint: result.Instance.Fingerprint(), // remember original result fingerprint
|
ResultFingerprint: result.Instance.Fingerprint(), // remember original result fingerprint
|
||||||
|
@ -238,34 +238,6 @@ func Test_getOrCreate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("expected Reduce and Math expression values", func(t *testing.T) {
|
|
||||||
result := eval.Result{
|
|
||||||
Instance: models.GenerateAlertLabels(5, "result-"),
|
|
||||||
Values: map[string]eval.NumberValueCapture{
|
|
||||||
"A": {Var: "A", Value: util.Pointer(1.0)},
|
|
||||||
"B": {Var: "B", Value: util.Pointer(2.0)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rule := generateRule()
|
|
||||||
|
|
||||||
state := c.getOrCreate(context.Background(), l, rule, result, nil, url)
|
|
||||||
assert.Equal(t, map[string]float64{"A": 1, "B": 2}, state.Values)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("expected Classic Condition values", func(t *testing.T) {
|
|
||||||
result := eval.Result{
|
|
||||||
Instance: models.GenerateAlertLabels(5, "result-"),
|
|
||||||
Values: map[string]eval.NumberValueCapture{
|
|
||||||
"B0": {Var: "B", Value: util.Pointer(1.0)},
|
|
||||||
"B1": {Var: "B", Value: util.Pointer(2.0)},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rule := generateRule()
|
|
||||||
|
|
||||||
state := c.getOrCreate(context.Background(), l, rule, result, nil, url)
|
|
||||||
assert.Equal(t, map[string]float64{"B0": 1, "B1": 2}, state.Values)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("when result labels collide with system labels from LabelsUserCannotSpecify", func(t *testing.T) {
|
t.Run("when result labels collide with system labels from LabelsUserCannotSpecify", func(t *testing.T) {
|
||||||
result := eval.Result{
|
result := eval.Result{
|
||||||
Instance: models.GenerateAlertLabels(5, "result-"),
|
Instance: models.GenerateAlertLabels(5, "result-"),
|
||||||
|
@ -405,10 +405,11 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu
|
|||||||
|
|
||||||
currentState.LastEvaluationTime = result.EvaluatedAt
|
currentState.LastEvaluationTime = result.EvaluatedAt
|
||||||
currentState.EvaluationDuration = result.EvaluationDuration
|
currentState.EvaluationDuration = result.EvaluationDuration
|
||||||
|
currentState.SetNextValues(result)
|
||||||
currentState.LatestResult = &Evaluation{
|
currentState.LatestResult = &Evaluation{
|
||||||
EvaluationTime: result.EvaluatedAt,
|
EvaluationTime: result.EvaluatedAt,
|
||||||
EvaluationState: result.State,
|
EvaluationState: result.State,
|
||||||
Values: NewEvaluationValues(result.Values),
|
Values: currentState.Values,
|
||||||
Condition: alertRule.Condition,
|
Condition: alertRule.Condition,
|
||||||
}
|
}
|
||||||
currentState.LastEvaluationString = result.EvaluationString
|
currentState.LastEvaluationString = result.EvaluationString
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Not for parallel tests.
|
// Not for parallel tests.
|
||||||
@ -149,14 +150,18 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
return ngmodels.CopyRule(baseRule, mutators...)
|
return ngmodels.CopyRule(baseRule, mutators...)
|
||||||
}
|
}
|
||||||
|
|
||||||
newEvaluation := func(evalTime time.Time, evalState eval.State) *Evaluation {
|
newEvaluationWithValues := func(evalTime time.Time, evalState eval.State, values map[string]float64) *Evaluation {
|
||||||
return &Evaluation{
|
return &Evaluation{
|
||||||
EvaluationTime: evalTime,
|
EvaluationTime: evalTime,
|
||||||
EvaluationState: evalState,
|
EvaluationState: evalState,
|
||||||
Values: make(map[string]*float64),
|
Values: values,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newEvaluation := func(evalTime time.Time, evalState eval.State) *Evaluation {
|
||||||
|
return newEvaluationWithValues(evalTime, evalState, make(map[string]float64))
|
||||||
|
}
|
||||||
|
|
||||||
newResult := func(mutators ...eval.ResultMutator) eval.Result {
|
newResult := func(mutators ...eval.ResultMutator) eval.Result {
|
||||||
r := eval.Result{
|
r := eval.Result{
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
@ -894,7 +899,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
desc: "t1[1:normal] t2[NoData] at t2",
|
desc: "t1[1:normal] t2[NoData] at t2",
|
||||||
results: map[time.Time]eval.Results{
|
results: map[time.Time]eval.Results{
|
||||||
t1: {
|
t1: {
|
||||||
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
|
newResult(
|
||||||
|
eval.WithState(eval.Normal),
|
||||||
|
eval.WithLabels(labels1),
|
||||||
|
eval.WithValues(map[string]eval.NumberValueCapture{"A": {Var: "A", Value: util.Pointer(1.0)}}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
t2: {
|
t2: {
|
||||||
newResult(eval.WithState(eval.NoData), eval.WithLabels(noDataLabels)),
|
newResult(eval.WithState(eval.NoData), eval.WithLabels(noDataLabels)),
|
||||||
@ -913,6 +922,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
EndsAt: t2.Add(ResendDelay * 4),
|
EndsAt: t2.Add(ResendDelay * 4),
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
LastSentAt: &t2,
|
LastSentAt: &t2,
|
||||||
|
Values: map[string]float64{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -925,11 +935,12 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + no-data"],
|
Labels: labels["system + rule + no-data"],
|
||||||
State: eval.Alerting,
|
State: eval.Alerting,
|
||||||
StateReason: eval.NoData.String(),
|
StateReason: eval.NoData.String(),
|
||||||
LatestResult: newEvaluation(t2, eval.NoData),
|
LatestResult: newEvaluationWithValues(t2, eval.NoData, map[string]float64{}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2.Add(ResendDelay * 4),
|
EndsAt: t2.Add(ResendDelay * 4),
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
LastSentAt: &t2,
|
LastSentAt: &t2,
|
||||||
|
Values: map[string]float64{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -942,10 +953,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + no-data"],
|
Labels: labels["system + rule + no-data"],
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
StateReason: eval.NoData.String(),
|
StateReason: eval.NoData.String(),
|
||||||
LatestResult: newEvaluation(t2, eval.NoData),
|
LatestResult: newEvaluationWithValues(t2, eval.NoData, map[string]float64{}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2,
|
EndsAt: t2,
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
|
Values: map[string]float64{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -976,11 +988,12 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Alerting,
|
State: eval.Alerting,
|
||||||
StateReason: eval.NoData.String(),
|
StateReason: eval.NoData.String(),
|
||||||
LatestResult: newEvaluation(t2, eval.NoData),
|
LatestResult: newEvaluationWithValues(t2, eval.NoData, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2.Add(ResendDelay * 4),
|
EndsAt: t2.Add(ResendDelay * 4),
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
LastSentAt: &t2,
|
LastSentAt: &t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -993,10 +1006,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
StateReason: eval.NoData.String(),
|
StateReason: eval.NoData.String(),
|
||||||
LatestResult: newEvaluation(t2, eval.NoData),
|
LatestResult: newEvaluationWithValues(t2, eval.NoData, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t1,
|
StartsAt: t1,
|
||||||
EndsAt: t1,
|
EndsAt: t1,
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1009,10 +1023,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
StateReason: ngmodels.ConcatReasons(eval.NoData.String(), ngmodels.StateReasonKeepLast),
|
||||||
LatestResult: newEvaluation(t2, eval.NoData),
|
LatestResult: newEvaluationWithValues(t2, eval.NoData, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t1,
|
StartsAt: t1,
|
||||||
EndsAt: t1,
|
EndsAt: t1,
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2807,7 +2822,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(1)},
|
ruleMutators: []ngmodels.AlertRuleMutator{ngmodels.RuleMuts.WithForNTimes(1)},
|
||||||
results: map[time.Time]eval.Results{
|
results: map[time.Time]eval.Results{
|
||||||
t1: {
|
t1: {
|
||||||
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
|
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1), eval.WithValues(map[string]eval.NumberValueCapture{"A": {Var: "A", Value: util.Pointer(1.0)}})),
|
||||||
},
|
},
|
||||||
t2: {
|
t2: {
|
||||||
newResult(eval.WithError(datasourceError)),
|
newResult(eval.WithError(datasourceError)),
|
||||||
@ -2895,11 +2910,12 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
State: eval.Alerting,
|
State: eval.Alerting,
|
||||||
StateReason: eval.Error.String(),
|
StateReason: eval.Error.String(),
|
||||||
Error: datasourceError,
|
Error: datasourceError,
|
||||||
LatestResult: newEvaluation(t2, eval.Error),
|
LatestResult: newEvaluationWithValues(t2, eval.Error, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2.Add(ResendDelay * 4),
|
EndsAt: t2.Add(ResendDelay * 4),
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
LastSentAt: &t2,
|
LastSentAt: &t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2912,10 +2928,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
StateReason: eval.Error.String(),
|
StateReason: eval.Error.String(),
|
||||||
LatestResult: newEvaluation(t2, eval.Error),
|
LatestResult: newEvaluationWithValues(t2, eval.Error, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2,
|
EndsAt: t2,
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2928,11 +2945,12 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Alerting,
|
State: eval.Alerting,
|
||||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||||
LatestResult: newEvaluation(t2, eval.Error),
|
LatestResult: newEvaluationWithValues(t2, eval.Error, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2.Add(ResendDelay * 4),
|
EndsAt: t2.Add(ResendDelay * 4),
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
LastSentAt: &t2,
|
LastSentAt: &t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2943,7 +2961,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
desc: "t1[1:normal] t2[QueryError] at t2",
|
desc: "t1[1:normal] t2[QueryError] at t2",
|
||||||
results: map[time.Time]eval.Results{
|
results: map[time.Time]eval.Results{
|
||||||
t1: {
|
t1: {
|
||||||
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
|
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1), eval.WithValues(map[string]eval.NumberValueCapture{"A": {Var: "A", Value: util.Pointer(1.0)}})),
|
||||||
},
|
},
|
||||||
t2: {
|
t2: {
|
||||||
newResult(eval.WithError(datasourceError)),
|
newResult(eval.WithError(datasourceError)),
|
||||||
@ -3032,11 +3050,12 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
State: eval.Alerting,
|
State: eval.Alerting,
|
||||||
StateReason: eval.Error.String(),
|
StateReason: eval.Error.String(),
|
||||||
Error: datasourceError,
|
Error: datasourceError,
|
||||||
LatestResult: newEvaluation(t2, eval.Error),
|
LatestResult: newEvaluationWithValues(t2, eval.Error, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t2,
|
StartsAt: t2,
|
||||||
EndsAt: t2.Add(ResendDelay * 4),
|
EndsAt: t2.Add(ResendDelay * 4),
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
LastSentAt: &t2,
|
LastSentAt: &t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3049,10 +3068,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
StateReason: eval.Error.String(),
|
StateReason: eval.Error.String(),
|
||||||
LatestResult: newEvaluation(t2, eval.Error),
|
LatestResult: newEvaluationWithValues(t2, eval.Error, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t1,
|
StartsAt: t1,
|
||||||
EndsAt: t1,
|
EndsAt: t1,
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -3065,10 +3085,11 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
|
|||||||
Labels: labels["system + rule + labels1"],
|
Labels: labels["system + rule + labels1"],
|
||||||
State: eval.Normal,
|
State: eval.Normal,
|
||||||
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
StateReason: ngmodels.ConcatReasons(eval.Error.String(), ngmodels.StateReasonKeepLast),
|
||||||
LatestResult: newEvaluation(t2, eval.Error),
|
LatestResult: newEvaluationWithValues(t2, eval.Error, map[string]float64{"A": float64(-1)}),
|
||||||
StartsAt: t1,
|
StartsAt: t1,
|
||||||
EndsAt: t1,
|
EndsAt: t1,
|
||||||
LastEvaluationTime: t2,
|
LastEvaluationTime: t2,
|
||||||
|
Values: map[string]float64{"A": float64(-1)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -324,14 +324,18 @@ func TestProcessEvalResults(t *testing.T) {
|
|||||||
ExecErrState: models.ErrorErrState,
|
ExecErrState: models.ErrorErrState,
|
||||||
}
|
}
|
||||||
|
|
||||||
newEvaluation := func(evalTime time.Time, evalState eval.State) *state.Evaluation {
|
newEvaluationWithValues := func(evalTime time.Time, evalState eval.State, values map[string]float64) *state.Evaluation {
|
||||||
return &state.Evaluation{
|
return &state.Evaluation{
|
||||||
EvaluationTime: evalTime,
|
EvaluationTime: evalTime,
|
||||||
EvaluationState: evalState,
|
EvaluationState: evalState,
|
||||||
Values: make(map[string]*float64),
|
Values: values,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newEvaluation := func(evalTime time.Time, evalState eval.State) *state.Evaluation {
|
||||||
|
return newEvaluationWithValues(evalTime, evalState, make(map[string]float64))
|
||||||
|
}
|
||||||
|
|
||||||
baseRuleWith := func(mutators ...models.AlertRuleMutator) *models.AlertRule {
|
baseRuleWith := func(mutators ...models.AlertRuleMutator) *models.AlertRule {
|
||||||
r := models.CopyRule(baseRule, mutators...)
|
r := models.CopyRule(baseRule, mutators...)
|
||||||
return r
|
return r
|
||||||
@ -1378,6 +1382,76 @@ func TestProcessEvalResults(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "expected Reduce and Math expression values",
|
||||||
|
alertRule: baseRuleWith(),
|
||||||
|
expectedAnnotations: 1,
|
||||||
|
evalResults: map[time.Time]eval.Results{
|
||||||
|
t1: {
|
||||||
|
newResult(
|
||||||
|
eval.WithState(eval.Alerting),
|
||||||
|
eval.WithLabels(data.Labels{}),
|
||||||
|
eval.WithValues(map[string]eval.NumberValueCapture{
|
||||||
|
"A": {Var: "A", Labels: data.Labels{}, Value: util.Pointer(1.0)},
|
||||||
|
"B": {Var: "B", Labels: data.Labels{}, Value: util.Pointer(2.0)},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStates: []*state.State{
|
||||||
|
{
|
||||||
|
Labels: labels["system + rule"],
|
||||||
|
ResultFingerprint: data.Labels{}.Fingerprint(),
|
||||||
|
State: eval.Alerting,
|
||||||
|
LatestResult: newEvaluationWithValues(t1, eval.Alerting, map[string]float64{
|
||||||
|
"A": 1.0,
|
||||||
|
"B": 2.0,
|
||||||
|
}),
|
||||||
|
StartsAt: t1,
|
||||||
|
EndsAt: t1.Add(state.ResendDelay * 4),
|
||||||
|
LastEvaluationTime: t1,
|
||||||
|
LastSentAt: &t1,
|
||||||
|
Values: map[string]float64{
|
||||||
|
"A": 1.0,
|
||||||
|
"B": 2.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "expected Classic Condition values",
|
||||||
|
alertRule: baseRuleWith(),
|
||||||
|
expectedAnnotations: 1,
|
||||||
|
evalResults: map[time.Time]eval.Results{
|
||||||
|
t1: {
|
||||||
|
newResult(
|
||||||
|
eval.WithState(eval.Alerting),
|
||||||
|
eval.WithLabels(data.Labels{}),
|
||||||
|
eval.WithValues(map[string]eval.NumberValueCapture{
|
||||||
|
"B0": {Var: "B", Labels: data.Labels{}, Value: util.Pointer(1.0)},
|
||||||
|
"B1": {Var: "B", Labels: data.Labels{}, Value: util.Pointer(2.0)},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStates: []*state.State{
|
||||||
|
{
|
||||||
|
Labels: labels["system + rule"],
|
||||||
|
ResultFingerprint: data.Labels{}.Fingerprint(),
|
||||||
|
State: eval.Alerting,
|
||||||
|
LatestResult: newEvaluationWithValues(t1, eval.Alerting, map[string]float64{
|
||||||
|
"B0": 1.0,
|
||||||
|
"B1": 2.0,
|
||||||
|
}),
|
||||||
|
StartsAt: t1,
|
||||||
|
EndsAt: t1.Add(state.ResendDelay * 4),
|
||||||
|
LastEvaluationTime: t1,
|
||||||
|
LastSentAt: &t1,
|
||||||
|
Values: map[string]float64{
|
||||||
|
"B0": 1.0,
|
||||||
|
"B1": 2.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -1488,6 +1562,43 @@ func TestProcessEvalResults(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("converts values to NaN if not defined", func(t *testing.T) {
|
||||||
|
// We set up our own special test for this, since we need special comparison logic - NaN != NaN
|
||||||
|
instanceStore := &state.FakeInstanceStore{}
|
||||||
|
clk := clock.NewMock()
|
||||||
|
cfg := state.ManagerCfg{
|
||||||
|
Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(),
|
||||||
|
ExternalURL: nil,
|
||||||
|
InstanceStore: instanceStore,
|
||||||
|
Images: &state.NotAvailableImageService{},
|
||||||
|
Clock: clk,
|
||||||
|
Historian: &state.FakeHistorian{},
|
||||||
|
Tracer: tracing.InitializeTracerForTest(),
|
||||||
|
Log: log.New("ngalert.state.manager"),
|
||||||
|
MaxStateSaveConcurrency: 1,
|
||||||
|
}
|
||||||
|
st := state.NewManager(cfg, state.NewNoopPersister())
|
||||||
|
rule := baseRuleWith()
|
||||||
|
time := t1
|
||||||
|
res := eval.Results{newResult(
|
||||||
|
eval.WithState(eval.Alerting),
|
||||||
|
eval.WithLabels(data.Labels{}),
|
||||||
|
eval.WithEvaluatedAt(t1),
|
||||||
|
eval.WithValues(map[string]eval.NumberValueCapture{
|
||||||
|
"A": {Var: "A", Labels: data.Labels{}, Value: nil},
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
|
||||||
|
_ = st.ProcessEvalResults(context.Background(), time, rule, res, systemLabels, state.NoopSender)
|
||||||
|
|
||||||
|
states := st.GetStatesForRuleUID(rule.OrgID, rule.UID)
|
||||||
|
require.Len(t, states, 1)
|
||||||
|
state := states[0]
|
||||||
|
require.NotNil(t, state.Values)
|
||||||
|
require.Contains(t, state.Values, "A")
|
||||||
|
require.Truef(t, math.IsNaN(state.Values["A"]), "expected NaN but got %v", state.Values["A"])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("should save state to database", func(t *testing.T) {
|
t.Run("should save state to database", func(t *testing.T) {
|
||||||
instanceStore := &state.FakeInstanceStore{}
|
instanceStore := &state.FakeInstanceStore{}
|
||||||
clk := clock.New()
|
clk := clock.New()
|
||||||
@ -1623,7 +1734,7 @@ func TestStaleResultsHandler(t *testing.T) {
|
|||||||
LatestResult: &state.Evaluation{
|
LatestResult: &state.Evaluation{
|
||||||
EvaluationTime: evaluationTime,
|
EvaluationTime: evaluationTime,
|
||||||
EvaluationState: eval.Normal,
|
EvaluationState: eval.Normal,
|
||||||
Values: make(map[string]*float64),
|
Values: make(map[string]float64),
|
||||||
Condition: "A",
|
Condition: "A",
|
||||||
},
|
},
|
||||||
StartsAt: evaluationTime,
|
StartsAt: evaluationTime,
|
||||||
|
@ -163,6 +163,32 @@ func (a *State) AddErrorAnnotations(err error, rule *models.AlertRule) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *State) SetNextValues(result eval.Result) {
|
||||||
|
const sentinel = float64(-1)
|
||||||
|
|
||||||
|
// We try to provide a reasonable object for Values in the event of nodata/error.
|
||||||
|
// In order to not break templates that might refer to refIDs,
|
||||||
|
// we instead fill values with the latest known set of refIDs, but with a sentinel -1 to indicate that the value didn't exist.
|
||||||
|
if result.State == eval.NoData || result.State == eval.Error {
|
||||||
|
placeholder := make(map[string]float64, len(a.Values))
|
||||||
|
for refID := range a.Values {
|
||||||
|
placeholder[refID] = sentinel
|
||||||
|
}
|
||||||
|
a.Values = placeholder
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newValues := make(map[string]float64, len(result.Values))
|
||||||
|
for k, v := range result.Values {
|
||||||
|
if v.Value != nil {
|
||||||
|
newValues[k] = *v.Value
|
||||||
|
} else {
|
||||||
|
newValues[k] = math.NaN()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.Values = newValues
|
||||||
|
}
|
||||||
|
|
||||||
// IsNormalStateWithNoReason returns true if the state is Normal and reason is empty
|
// IsNormalStateWithNoReason returns true if the state is Normal and reason is empty
|
||||||
func IsNormalStateWithNoReason(s *State) bool {
|
func IsNormalStateWithNoReason(s *State) bool {
|
||||||
return s.State == eval.Normal && s.StateReason == ""
|
return s.State == eval.Normal && s.StateReason == ""
|
||||||
@ -206,16 +232,20 @@ type Evaluation struct {
|
|||||||
// Values contains the RefID and value of reduce and math expressions.
|
// Values contains the RefID and value of reduce and math expressions.
|
||||||
// Classic conditions can have different values for the same RefID as they can include multiple conditions.
|
// Classic conditions can have different values for the same RefID as they can include multiple conditions.
|
||||||
// For these, we use the index of the condition in addition RefID as the key e.g. "A0, A1, A2, etc.".
|
// For these, we use the index of the condition in addition RefID as the key e.g. "A0, A1, A2, etc.".
|
||||||
Values map[string]*float64
|
Values map[string]float64
|
||||||
// Condition is the refID specified as the condition in the alerting rule at the time of the evaluation.
|
// Condition is the refID specified as the condition in the alerting rule at the time of the evaluation.
|
||||||
Condition string
|
Condition string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEvaluationValues returns the labels and values for each RefID in the capture.
|
// NewEvaluationValues returns the labels and values for each RefID in the capture.
|
||||||
func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float64 {
|
func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]float64 {
|
||||||
result := make(map[string]*float64, len(m))
|
result := make(map[string]float64, len(m))
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
result[k] = v.Value
|
if v.Value != nil {
|
||||||
|
result[k] = *v.Value
|
||||||
|
} else {
|
||||||
|
result[k] = math.NaN()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -486,11 +516,7 @@ func (a *State) GetLastEvaluationValuesForCondition() map[string]float64 {
|
|||||||
|
|
||||||
for refID, value := range lastResult.Values {
|
for refID, value := range lastResult.Values {
|
||||||
if strings.Contains(refID, lastResult.Condition) {
|
if strings.Contains(refID, lastResult.Condition) {
|
||||||
if value != nil {
|
r[refID] = value
|
||||||
r[refID] = *value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r[refID] = math.NaN()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,9 +528,9 @@ func TestGetLastEvaluationValuesForCondition(t *testing.T) {
|
|||||||
eval := &Evaluation{
|
eval := &Evaluation{
|
||||||
EvaluationTime: time.Time{},
|
EvaluationTime: time.Time{},
|
||||||
EvaluationState: 0,
|
EvaluationState: 0,
|
||||||
Values: map[string]*float64{
|
Values: map[string]float64{
|
||||||
"B": util.Pointer(rand.Float64()),
|
"B": rand.Float64(),
|
||||||
"A": util.Pointer(expected),
|
"A": expected,
|
||||||
},
|
},
|
||||||
Condition: "A",
|
Condition: "A",
|
||||||
}
|
}
|
||||||
@ -543,8 +543,8 @@ func TestGetLastEvaluationValuesForCondition(t *testing.T) {
|
|||||||
eval := &Evaluation{
|
eval := &Evaluation{
|
||||||
EvaluationTime: time.Time{},
|
EvaluationTime: time.Time{},
|
||||||
EvaluationState: 0,
|
EvaluationState: 0,
|
||||||
Values: map[string]*float64{
|
Values: map[string]float64{
|
||||||
"C": util.Pointer(rand.Float64()),
|
"C": rand.Float64(),
|
||||||
},
|
},
|
||||||
Condition: "A",
|
Condition: "A",
|
||||||
}
|
}
|
||||||
@ -556,8 +556,8 @@ func TestGetLastEvaluationValuesForCondition(t *testing.T) {
|
|||||||
eval := &Evaluation{
|
eval := &Evaluation{
|
||||||
EvaluationTime: time.Time{},
|
EvaluationTime: time.Time{},
|
||||||
EvaluationState: 0,
|
EvaluationState: 0,
|
||||||
Values: map[string]*float64{
|
Values: map[string]float64{
|
||||||
"A": nil,
|
"A": math.NaN(),
|
||||||
},
|
},
|
||||||
Condition: "A",
|
Condition: "A",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user