Alerting: Classic conditions can now display multiple values (#46971)

* Alerting: Extract classic condition values by RefID

* uncapitalise function

* update documentation

* Update pkg/services/ngalert/eval/extract_md.go

Co-authored-by: George Robinson <george.robinson@grafana.com>

* Update pkg/services/ngalert/state/state.go

Co-authored-by: George Robinson <george.robinson@grafana.com>

* Update pkg/services/ngalert/state/state.go

Co-authored-by: George Robinson <george.robinson@grafana.com>

* Update pkg/services/ngalert/eval/extract_md.go

Co-authored-by: George Robinson <george.robinson@grafana.com>

* Update docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update pkg/services/ngalert/eval/extract_md.go

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Run prettier

Co-authored-by: George Robinson <george.robinson@grafana.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
gotjosh 2022-03-29 20:33:03 +01:00 committed by GitHub
parent feaa4a5c64
commit 84e5f336fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 21 deletions

View File

@ -31,8 +31,8 @@ Labels are key-value pairs that contain information about, and are used to uniqu
The following template variables are available when expanding annotations and labels. The following template variables are available when expanding annotations and labels.
| Name | Description | | Name | Description |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a [classic condition]({{< relref "./create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}). | | $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a [classic condition]({{< relref "./create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}). |
| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. This is unavailable when the rule uses a classic condition | | $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. If the rule uses classic conditions, then a combination of the `refID` and position of the condition is used. For example, `{{ $values.A0.Value }}` or `{{ $values.A1.Value }}` |
| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. | | $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. |

View File

@ -20,8 +20,10 @@ func extractEvalString(frame *data.Frame) (s string) {
if evalMatches, ok := frame.Meta.Custom.([]classic.EvalMatch); ok { if evalMatches, ok := frame.Meta.Custom.([]classic.EvalMatch); ok {
sb := strings.Builder{} sb := strings.Builder{}
// TODO: Should we simplify when we only have one match and use the name notation of $labels.A?
for i, m := range evalMatches { for i, m := range evalMatches {
sb.WriteString("[ ") sb.WriteString("[ ")
sb.WriteString(fmt.Sprintf("var='%s%v' ", frame.RefID, i))
sb.WriteString(fmt.Sprintf("metric='%s' ", m.Metric)) sb.WriteString(fmt.Sprintf("metric='%s' ", m.Metric))
sb.WriteString(fmt.Sprintf("labels={%s} ", m.Labels)) sb.WriteString(fmt.Sprintf("labels={%s} ", m.Labels))
@ -66,10 +68,10 @@ func extractEvalString(frame *data.Frame) (s string) {
return "" return ""
} }
// extractValues returns the RefID and value for all reduce and math expressions // extractValues returns the RefID and value for all classic conditions, reduce, and math expressions in the frame.
// in the frame. It does not return values for classic conditions as the values // For classic conditions the same refID can have multiple values due to multiple conditions, for them we use the index of
// in classic conditions do not have a RefID. It returns nil if there are // the condition in addition to the refID to distinguish between different values.
// no results in the frame. // It returns nil if there are no results in the frame.
func extractValues(frame *data.Frame) map[string]NumberValueCapture { func extractValues(frame *data.Frame) map[string]NumberValueCapture {
if frame == nil { if frame == nil {
return nil return nil
@ -77,6 +79,24 @@ func extractValues(frame *data.Frame) map[string]NumberValueCapture {
if frame.Meta == nil || frame.Meta.Custom == nil { if frame.Meta == nil || frame.Meta.Custom == nil {
return nil return nil
} }
if matches, ok := frame.Meta.Custom.([]classic.EvalMatch); ok {
// Classic evaluations only have a single match but it can contain multiple conditions.
// Conditions have a strict ordering which we can rely on to distinguish between values.
v := make(map[string]NumberValueCapture, len(matches))
for i, match := range matches {
// In classic conditions we use refID and the condition position as a way to distinguish between values.
// We can guarantee determinism as conditions are ordered and this order is preserved when marshaling.
refID := fmt.Sprintf("%s%d", frame.RefID, i)
v[refID] = NumberValueCapture{
Var: frame.RefID,
Labels: match.Labels,
Value: match.Value,
}
}
return v
}
if caps, ok := frame.Meta.Custom.([]NumberValueCapture); ok { if caps, ok := frame.Meta.Custom.([]NumberValueCapture); ok {
v := make(map[string]NumberValueCapture, len(caps)) v := make(map[string]NumberValueCapture, len(caps))
for _, c := range caps { for _, c := range caps {

View File

@ -20,15 +20,15 @@ func TestExtractEvalString(t *testing.T) {
inFrame: newMetaFrame([]classic.EvalMatch{ inFrame: newMetaFrame([]classic.EvalMatch{
{Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(32.3)}, {Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(32.3)},
}, ptr.Float64(1)), }, ptr.Float64(1)),
outString: `[ metric='Test' labels={host=foo} value=32.3 ]`, outString: `[ var='0' metric='Test' labels={host=foo} value=32.3 ]`,
}, },
{ {
desc: "2 EvalMatches", desc: "2 EvalMatches",
inFrame: newMetaFrame([]classic.EvalMatch{ inFrame: newMetaFrame([]classic.EvalMatch{
{Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(32.3)}, {Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(32.3)},
{Metric: "Test", Labels: data.Labels{"host": "baz"}, Value: ptr.Float64(10)}, {Metric: "Test", Labels: data.Labels{"host": "baz"}, Value: ptr.Float64(10)},
}, ptr.Float64(1)), }, ptr.Float64(1), withRefID("A")),
outString: `[ metric='Test' labels={host=foo} value=32.3 ], [ metric='Test' labels={host=baz} value=10 ]`, outString: `[ var='A0' metric='Test' labels={host=foo} value=32.3 ], [ var='A1' metric='Test' labels={host=baz} value=10 ]`,
}, },
{ {
desc: "3 EvalMatches", desc: "3 EvalMatches",
@ -36,8 +36,8 @@ func TestExtractEvalString(t *testing.T) {
{Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(32.3)}, {Metric: "Test", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(32.3)},
{Metric: "Test", Labels: data.Labels{"host": "baz"}, Value: ptr.Float64(10)}, {Metric: "Test", Labels: data.Labels{"host": "baz"}, Value: ptr.Float64(10)},
{Metric: "TestA", Labels: data.Labels{"host": "zip"}, Value: ptr.Float64(11)}, {Metric: "TestA", Labels: data.Labels{"host": "zip"}, Value: ptr.Float64(11)},
}, ptr.Float64(1)), }, ptr.Float64(1), withRefID("A")),
outString: `[ metric='Test' labels={host=foo} value=32.3 ], [ metric='Test' labels={host=baz} value=10 ], [ metric='TestA' labels={host=zip} value=11 ]`, outString: `[ var='A0' metric='Test' labels={host=foo} value=32.3 ], [ var='A1' metric='Test' labels={host=baz} value=10 ], [ var='A2' metric='TestA' labels={host=zip} value=11 ]`,
}, },
} }
for _, tc := range cases { for _, tc := range cases {
@ -57,11 +57,23 @@ func TestExtractValues(t *testing.T) {
inFrame: newMetaFrame(nil, ptr.Float64(1)), inFrame: newMetaFrame(nil, ptr.Float64(1)),
values: nil, values: nil,
}, { }, {
desc: "Classic condition frame returns nil", desc: "Classic condition frame with one match",
inFrame: newMetaFrame([]classic.EvalMatch{ inFrame: newMetaFrame([]classic.EvalMatch{
{Metric: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(1)}, {Metric: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(1)},
}, ptr.Float64(1)), }, ptr.Float64(1), withRefID("A")),
values: nil, values: map[string]NumberValueCapture{
"A0": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(1)},
},
}, {
desc: "Classic condition frame with multiple matches",
inFrame: newMetaFrame([]classic.EvalMatch{
{Metric: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(1)},
{Metric: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(3)},
}, ptr.Float64(1), withRefID("A")),
values: map[string]NumberValueCapture{
"A0": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(1)},
"A1": {Var: "A", Labels: data.Labels{"host": "foo"}, Value: ptr.Float64(3)},
},
}, { }, {
desc: "Nil value", desc: "Nil value",
inFrame: newMetaFrame([]NumberValueCapture{ inFrame: newMetaFrame([]NumberValueCapture{
@ -96,10 +108,24 @@ func TestExtractValues(t *testing.T) {
} }
} }
func newMetaFrame(custom interface{}, val *float64) *data.Frame { type frameCallback func(frame *data.Frame)
return data.NewFrame("",
func withRefID(refID string) frameCallback {
return func(frame *data.Frame) {
frame.RefID = refID
}
}
func newMetaFrame(custom interface{}, val *float64, callbacks ...frameCallback) *data.Frame {
f := data.NewFrame("",
data.NewField("", nil, []*float64{val})). data.NewField("", nil, []*float64{val})).
SetMeta(&data.FrameMeta{ SetMeta(&data.FrameMeta{
Custom: custom, Custom: custom,
}) })
for _, cb := range callbacks {
cb(f)
}
return f
} }

View File

@ -33,8 +33,8 @@ type Evaluation struct {
EvaluationTime time.Time EvaluationTime time.Time
EvaluationState eval.State EvaluationState eval.State
// Values contains the RefID and value of reduce and math expressions. // Values contains the RefID and value of reduce and math expressions.
// It does not contain values for classic conditions as the values // Classic conditions can have different values for the same RefID as they can include multiple conditions.
// in classic conditions do not have a RefID. // 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
} }