Commit e34bf9a1 by Torkel Ödegaard

Merge branch 'utkarshcmu-or_alerting'

parents 98d1748e 62e8a039
...@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows ...@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
specify a query letter, time range and an aggregation function. The letter refers to specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
a single value that is then used in the threshold check. The query used in an alert rule cannot a single value that is then used in the threshold check. The query used in an alert rule cannot
contain any template variables. Currently we only support `AND` operator between conditions. contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
For example, we have 3 conditions in the following order:
`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`. of another alert in your conditions, and `Time Of Day`.
......
...@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { ...@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
res := backendCmd.Result res := backendCmd.Result
dtoRes := &dtos.AlertTestResult{ dtoRes := &dtos.AlertTestResult{
Firing: res.Firing, Firing: res.Firing,
ConditionEvals: res.ConditionEvals,
} }
if res.Error != nil { if res.Error != nil {
......
...@@ -35,11 +35,12 @@ type AlertTestCommand struct { ...@@ -35,11 +35,12 @@ type AlertTestCommand struct {
} }
type AlertTestResult struct { type AlertTestResult struct {
Firing bool `json:"firing"` Firing bool `json:"firing"`
TimeMs string `json:"timeMs"` ConditionEvals string `json:"conditionEvals"`
Error string `json:"error,omitempty"` TimeMs string `json:"timeMs"`
EvalMatches []*EvalMatch `json:"matches,omitempty"` Error string `json:"error,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"` EvalMatches []*EvalMatch `json:"matches,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"`
} }
type AlertTestResultLog struct { type AlertTestResultLog struct {
......
...@@ -23,6 +23,7 @@ type QueryCondition struct { ...@@ -23,6 +23,7 @@ type QueryCondition struct {
Query AlertQuery Query AlertQuery
Reducer QueryReducer Reducer QueryReducer
Evaluator AlertEvaluator Evaluator AlertEvaluator
Operator string
HandleRequest tsdb.HandleRequestFunc HandleRequest tsdb.HandleRequestFunc
} }
...@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio ...@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
return &alerting.ConditionResult{ return &alerting.ConditionResult{
Firing: evalMatchCount > 0, Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList), NoDataFound: emptySerieCount == len(seriesList),
Operator: c.Operator,
EvalMatches: matches, EvalMatches: matches,
}, nil }, nil
} }
...@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro ...@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
condition.Evaluator = evaluator condition.Evaluator = evaluator
operatorJson := model.Get("operator")
operator := operatorJson.Get("type").MustString("and")
condition.Operator = operator
return &condition, nil return &condition, nil
} }
......
...@@ -17,7 +17,7 @@ type EvalContext struct { ...@@ -17,7 +17,7 @@ type EvalContext struct {
EvalMatches []*EvalMatch EvalMatches []*EvalMatch
Logs []*ResultLogEntry Logs []*ResultLogEntry
Error error Error error
Description string ConditionEvals string
StartTime time.Time StartTime time.Time
EndTime time.Time EndTime time.Time
Rule *Rule Rule *Rule
......
package alerting package alerting
import ( import (
"strconv"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
...@@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler { ...@@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler {
func (e *DefaultEvalHandler) Eval(context *EvalContext) { func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true firing := true
for _, condition := range context.Rule.Conditions { conditionEvals := ""
for i := 0; i < len(context.Rule.Conditions); i++ {
condition := context.Rule.Conditions[i]
cr, err := condition.Eval(context) cr, err := condition.Eval(context)
if err != nil { if err != nil {
context.Error = err context.Error = err
...@@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { ...@@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
break break
} }
// break if result has not triggered yet // calculating Firing based on operator
if cr.Firing == false { if cr.Operator == "or" {
firing = false firing = firing || cr.Firing
break } else {
firing = firing && cr.Firing
}
if i > 0 {
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
} else {
conditionEvals = strconv.FormatBool(firing)
} }
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...) context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
} }
context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
context.Firing = firing context.Firing = firing
context.EndTime = time.Now() context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
......
...@@ -8,12 +8,13 @@ import ( ...@@ -8,12 +8,13 @@ import (
) )
type conditionStub struct { type conditionStub struct {
firing bool firing bool
matches []*EvalMatch operator string
matches []*EvalMatch
} }
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) { func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil
} }
func TestAlertingExecutor(t *testing.T) { func TestAlertingExecutor(t *testing.T) {
...@@ -29,6 +30,7 @@ func TestAlertingExecutor(t *testing.T) { ...@@ -29,6 +30,7 @@ func TestAlertingExecutor(t *testing.T) {
handler.Eval(context) handler.Eval(context)
So(context.Firing, ShouldEqual, true) So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "true = true")
}) })
Convey("Show return false with not passing asdf", func() { Convey("Show return false with not passing asdf", func() {
...@@ -41,6 +43,89 @@ func TestAlertingExecutor(t *testing.T) { ...@@ -41,6 +43,89 @@ func TestAlertingExecutor(t *testing.T) {
handler.Eval(context) handler.Eval(context)
So(context.Firing, ShouldEqual, false) So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if any of the condition is passing with OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
})
Convey("Show return false if any of the condition is failing with AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if one condition is failing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
})
Convey("Show return false if one condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
})
Convey("Show return false if a condition is failing with nested AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
})
Convey("Show return true if a condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
}) })
}) })
} }
...@@ -24,6 +24,7 @@ type Notifier interface { ...@@ -24,6 +24,7 @@ type Notifier interface {
type ConditionResult struct { type ConditionResult struct {
Firing bool Firing bool
NoDataFound bool NoDataFound bool
Operator string
EvalMatches []*EvalMatch EvalMatches []*EvalMatch
} }
......
...@@ -28,6 +28,11 @@ var evalFunctions = [ ...@@ -28,6 +28,11 @@ var evalFunctions = [
{text: 'HAS NO VALUE' , value: 'no_value'} {text: 'HAS NO VALUE' , value: 'no_value'}
]; ];
var evalOperators = [
{text: 'OR', value: 'or'},
{text: 'AND', value: 'and'},
];
var reducerTypes = [ var reducerTypes = [
{text: 'avg()', value: 'avg'}, {text: 'avg()', value: 'avg'},
{text: 'min()', value: 'min'}, {text: 'min()', value: 'min'},
...@@ -116,6 +121,7 @@ export default { ...@@ -116,6 +121,7 @@ export default {
getStateDisplayModel: getStateDisplayModel, getStateDisplayModel: getStateDisplayModel,
conditionTypes: conditionTypes, conditionTypes: conditionTypes,
evalFunctions: evalFunctions, evalFunctions: evalFunctions,
evalOperators: evalOperators,
noDataModes: noDataModes, noDataModes: noDataModes,
executionErrorModes: executionErrorModes, executionErrorModes: executionErrorModes,
reducerTypes: reducerTypes, reducerTypes: reducerTypes,
......
...@@ -18,6 +18,7 @@ export class AlertTabCtrl { ...@@ -18,6 +18,7 @@ export class AlertTabCtrl {
alert: any; alert: any;
conditionModels: any; conditionModels: any;
evalFunctions: any; evalFunctions: any;
evalOperators: any;
noDataModes: any; noDataModes: any;
executionErrorModes: any; executionErrorModes: any;
addNotificationSegment; addNotificationSegment;
...@@ -41,6 +42,7 @@ export class AlertTabCtrl { ...@@ -41,6 +42,7 @@ export class AlertTabCtrl {
this.$scope.ctrl = this; this.$scope.ctrl = this;
this.subTabIndex = 0; this.subTabIndex = 0;
this.evalFunctions = alertDef.evalFunctions; this.evalFunctions = alertDef.evalFunctions;
this.evalOperators = alertDef.evalOperators;
this.conditionTypes = alertDef.conditionTypes; this.conditionTypes = alertDef.conditionTypes;
this.noDataModes = alertDef.noDataModes; this.noDataModes = alertDef.noDataModes;
this.executionErrorModes = alertDef.executionErrorModes; this.executionErrorModes = alertDef.executionErrorModes;
...@@ -194,6 +196,7 @@ export class AlertTabCtrl { ...@@ -194,6 +196,7 @@ export class AlertTabCtrl {
query: {params: ['A', '5m', 'now']}, query: {params: ['A', '5m', 'now']},
reducer: {type: 'avg', params: []}, reducer: {type: 'avg', params: []},
evaluator: {type: 'gt', params: [null]}, evaluator: {type: 'gt', params: [null]},
operator: {type: 'and'},
}; };
} }
...@@ -250,6 +253,7 @@ export class AlertTabCtrl { ...@@ -250,6 +253,7 @@ export class AlertTabCtrl {
cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef); cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
cm.reducerPart = alertDef.createReducerPart(source.reducer); cm.reducerPart = alertDef.createReducerPart(source.reducer);
cm.evaluator = source.evaluator; cm.evaluator = source.evaluator;
cm.operator = source.operator;
return cm; return cm;
} }
......
...@@ -38,23 +38,23 @@ ...@@ -38,23 +38,23 @@
<h5 class="section-heading">Conditions</h5> <h5 class="section-heading">Conditions</h5>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels"> <div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span> <metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span> <span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)"> <query-part-editor class="gf-form-label query-part width-5" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor> </query-part-editor>
<span class="gf-form-label query-keyword">OF</span> <span class="gf-form-label query-keyword">OF</span>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)"> <query-part-editor class="gf-form-label query-part width-10" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
</query-part-editor> </query-part-editor>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model> <metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input> <input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label> <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input class="gf-form-input max-width-7" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input> <input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment