Skip to content

Commit e34bf9a

Browse files
committed
Merge branch 'utkarshcmu-or_alerting'
2 parents 98d1748 + 62e8a03 commit e34bf9a

File tree

11 files changed

+142
-22
lines changed

11 files changed

+142
-22
lines changed

docs/sources/alerting/rules.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
5555
specify a query letter, time range and an aggregation function. The letter refers to
5656
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
5757
a single value that is then used in the threshold check. The query used in an alert rule cannot
58-
contain any template variables. Currently we only support `AND` operator between conditions.
58+
contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
59+
For example, we have 3 conditions in the following order:
60+
`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
61+
so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
5962

6063
We plan to add other condition types in the future, like `Other Alert`, where you can include the state
6164
of another alert in your conditions, and `Time Of Day`.

pkg/api/alerting.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
119119
res := backendCmd.Result
120120

121121
dtoRes := &dtos.AlertTestResult{
122-
Firing: res.Firing,
122+
Firing: res.Firing,
123+
ConditionEvals: res.ConditionEvals,
123124
}
124125

125126
if res.Error != nil {

pkg/api/dtos/alerting.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ type AlertTestCommand struct {
3535
}
3636

3737
type AlertTestResult struct {
38-
Firing bool `json:"firing"`
39-
TimeMs string `json:"timeMs"`
40-
Error string `json:"error,omitempty"`
41-
EvalMatches []*EvalMatch `json:"matches,omitempty"`
42-
Logs []*AlertTestResultLog `json:"logs,omitempty"`
38+
Firing bool `json:"firing"`
39+
ConditionEvals string `json:"conditionEvals"`
40+
TimeMs string `json:"timeMs"`
41+
Error string `json:"error,omitempty"`
42+
EvalMatches []*EvalMatch `json:"matches,omitempty"`
43+
Logs []*AlertTestResultLog `json:"logs,omitempty"`
4344
}
4445

4546
type AlertTestResultLog struct {

pkg/services/alerting/conditions/query.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type QueryCondition struct {
2323
Query AlertQuery
2424
Reducer QueryReducer
2525
Evaluator AlertEvaluator
26+
Operator string
2627
HandleRequest tsdb.HandleRequestFunc
2728
}
2829

@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
7273
return &alerting.ConditionResult{
7374
Firing: evalMatchCount > 0,
7475
NoDataFound: emptySerieCount == len(seriesList),
76+
Operator: c.Operator,
7577
EvalMatches: matches,
7678
}, nil
7779
}
@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
168170
if err != nil {
169171
return nil, err
170172
}
171-
172173
condition.Evaluator = evaluator
174+
175+
operatorJson := model.Get("operator")
176+
operator := operatorJson.Get("type").MustString("and")
177+
condition.Operator = operator
178+
173179
return &condition, nil
174180
}
175181

pkg/services/alerting/eval_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type EvalContext struct {
1717
EvalMatches []*EvalMatch
1818
Logs []*ResultLogEntry
1919
Error error
20-
Description string
20+
ConditionEvals string
2121
StartTime time.Time
2222
EndTime time.Time
2323
Rule *Rule

pkg/services/alerting/eval_handler.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package alerting
22

33
import (
4+
"strconv"
5+
"strings"
46
"time"
57

68
"github.com/grafana/grafana/pkg/log"
@@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler {
2123

2224
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
2325
firing := true
24-
for _, condition := range context.Rule.Conditions {
26+
conditionEvals := ""
27+
28+
for i := 0; i < len(context.Rule.Conditions); i++ {
29+
condition := context.Rule.Conditions[i]
2530
cr, err := condition.Eval(context)
2631
if err != nil {
2732
context.Error = err
@@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
3237
break
3338
}
3439

35-
// break if result has not triggered yet
36-
if cr.Firing == false {
37-
firing = false
38-
break
40+
// calculating Firing based on operator
41+
if cr.Operator == "or" {
42+
firing = firing || cr.Firing
43+
} else {
44+
firing = firing && cr.Firing
45+
}
46+
47+
if i > 0 {
48+
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
49+
} else {
50+
conditionEvals = strconv.FormatBool(firing)
3951
}
4052

4153
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
4254
}
4355

56+
context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
4457
context.Firing = firing
4558
context.EndTime = time.Now()
4659
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond

pkg/services/alerting/eval_handler_test.go

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import (
88
)
99

1010
type conditionStub struct {
11-
firing bool
12-
matches []*EvalMatch
11+
firing bool
12+
operator string
13+
matches []*EvalMatch
1314
}
1415

1516
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
16-
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
17+
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil
1718
}
1819

1920
func TestAlertingExecutor(t *testing.T) {
@@ -29,6 +30,7 @@ func TestAlertingExecutor(t *testing.T) {
2930

3031
handler.Eval(context)
3132
So(context.Firing, ShouldEqual, true)
33+
So(context.ConditionEvals, ShouldEqual, "true = true")
3234
})
3335

3436
Convey("Show return false with not passing asdf", func() {
@@ -41,6 +43,89 @@ func TestAlertingExecutor(t *testing.T) {
4143

4244
handler.Eval(context)
4345
So(context.Firing, ShouldEqual, false)
46+
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
47+
})
48+
49+
Convey("Show return true if any of the condition is passing with OR operator", func() {
50+
context := NewEvalContext(context.TODO(), &Rule{
51+
Conditions: []Condition{
52+
&conditionStub{firing: true, operator: "and"},
53+
&conditionStub{firing: false, operator: "or"},
54+
},
55+
})
56+
57+
handler.Eval(context)
58+
So(context.Firing, ShouldEqual, true)
59+
So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
60+
})
61+
62+
Convey("Show return false if any of the condition is failing with AND operator", func() {
63+
context := NewEvalContext(context.TODO(), &Rule{
64+
Conditions: []Condition{
65+
&conditionStub{firing: true, operator: "and"},
66+
&conditionStub{firing: false, operator: "and"},
67+
},
68+
})
69+
70+
handler.Eval(context)
71+
So(context.Firing, ShouldEqual, false)
72+
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
73+
})
74+
75+
Convey("Show return true if one condition is failing with nested OR operator", func() {
76+
context := NewEvalContext(context.TODO(), &Rule{
77+
Conditions: []Condition{
78+
&conditionStub{firing: true, operator: "and"},
79+
&conditionStub{firing: true, operator: "and"},
80+
&conditionStub{firing: false, operator: "or"},
81+
},
82+
})
83+
84+
handler.Eval(context)
85+
So(context.Firing, ShouldEqual, true)
86+
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
87+
})
88+
89+
Convey("Show return false if one condition is passing with nested OR operator", func() {
90+
context := NewEvalContext(context.TODO(), &Rule{
91+
Conditions: []Condition{
92+
&conditionStub{firing: true, operator: "and"},
93+
&conditionStub{firing: false, operator: "and"},
94+
&conditionStub{firing: false, operator: "or"},
95+
},
96+
})
97+
98+
handler.Eval(context)
99+
So(context.Firing, ShouldEqual, false)
100+
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
101+
})
102+
103+
Convey("Show return false if a condition is failing with nested AND operator", func() {
104+
context := NewEvalContext(context.TODO(), &Rule{
105+
Conditions: []Condition{
106+
&conditionStub{firing: true, operator: "and"},
107+
&conditionStub{firing: false, operator: "and"},
108+
&conditionStub{firing: true, operator: "and"},
109+
},
110+
})
111+
112+
handler.Eval(context)
113+
So(context.Firing, ShouldEqual, false)
114+
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
115+
})
116+
117+
Convey("Show return true if a condition is passing with nested OR operator", func() {
118+
context := NewEvalContext(context.TODO(), &Rule{
119+
Conditions: []Condition{
120+
&conditionStub{firing: true, operator: "and"},
121+
&conditionStub{firing: false, operator: "or"},
122+
&conditionStub{firing: true, operator: "or"},
123+
},
124+
})
125+
126+
handler.Eval(context)
127+
So(context.Firing, ShouldEqual, true)
128+
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
44129
})
45130
})
46131
}

pkg/services/alerting/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Notifier interface {
2424
type ConditionResult struct {
2525
Firing bool
2626
NoDataFound bool
27+
Operator string
2728
EvalMatches []*EvalMatch
2829
}
2930

public/app/features/alerting/alert_def.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ var evalFunctions = [
2828
{text: 'HAS NO VALUE' , value: 'no_value'}
2929
];
3030

31+
var evalOperators = [
32+
{text: 'OR', value: 'or'},
33+
{text: 'AND', value: 'and'},
34+
];
35+
3136
var reducerTypes = [
3237
{text: 'avg()', value: 'avg'},
3338
{text: 'min()', value: 'min'},
@@ -116,6 +121,7 @@ export default {
116121
getStateDisplayModel: getStateDisplayModel,
117122
conditionTypes: conditionTypes,
118123
evalFunctions: evalFunctions,
124+
evalOperators: evalOperators,
119125
noDataModes: noDataModes,
120126
executionErrorModes: executionErrorModes,
121127
reducerTypes: reducerTypes,

public/app/features/alerting/alert_tab_ctrl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class AlertTabCtrl {
1818
alert: any;
1919
conditionModels: any;
2020
evalFunctions: any;
21+
evalOperators: any;
2122
noDataModes: any;
2223
executionErrorModes: any;
2324
addNotificationSegment;
@@ -41,6 +42,7 @@ export class AlertTabCtrl {
4142
this.$scope.ctrl = this;
4243
this.subTabIndex = 0;
4344
this.evalFunctions = alertDef.evalFunctions;
45+
this.evalOperators = alertDef.evalOperators;
4446
this.conditionTypes = alertDef.conditionTypes;
4547
this.noDataModes = alertDef.noDataModes;
4648
this.executionErrorModes = alertDef.executionErrorModes;
@@ -194,6 +196,7 @@ export class AlertTabCtrl {
194196
query: {params: ['A', '5m', 'now']},
195197
reducer: {type: 'avg', params: []},
196198
evaluator: {type: 'gt', params: [null]},
199+
operator: {type: 'and'},
197200
};
198201
}
199202

@@ -250,6 +253,7 @@ export class AlertTabCtrl {
250253
cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
251254
cm.reducerPart = alertDef.createReducerPart(source.reducer);
252255
cm.evaluator = source.evaluator;
256+
cm.operator = source.operator;
253257

254258
return cm;
255259
}

0 commit comments

Comments
 (0)