mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 18:52:37 +08:00
Merge branch 'or_alerting' of https://github.com/utkarshcmu/grafana into utkarshcmu-or_alerting
This commit is contained in:
@ -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 {
|
|||||||
res := backendCmd.Result
|
res := backendCmd.Result
|
||||||
|
|
||||||
dtoRes := &dtos.AlertTestResult{
|
dtoRes := &dtos.AlertTestResult{
|
||||||
Firing: res.Firing,
|
Firing: res.Firing,
|
||||||
|
FiringEval: res.FiringEval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
|
@ -36,6 +36,7 @@ type AlertTestCommand struct {
|
|||||||
|
|
||||||
type AlertTestResult struct {
|
type AlertTestResult struct {
|
||||||
Firing bool `json:"firing"`
|
Firing bool `json:"firing"`
|
||||||
|
FiringEval string `json:"firingEvaluation"`
|
||||||
TimeMs string `json:"timeMs"`
|
TimeMs string `json:"timeMs"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
EvalMatches []*EvalMatch `json:"matches,omitempty"`
|
EvalMatches []*EvalMatch `json:"matches,omitempty"`
|
||||||
|
@ -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
|
|||||||
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
|
|||||||
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()
|
||||||
|
condition.Operator = operator
|
||||||
|
|
||||||
return &condition, nil
|
return &condition, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ type EvalContext struct {
|
|||||||
Logs []*ResultLogEntry
|
Logs []*ResultLogEntry
|
||||||
Error error
|
Error error
|
||||||
Description string
|
Description string
|
||||||
|
FiringEval string
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
Rule *Rule
|
Rule *Rule
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
@ -21,7 +22,9 @@ 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 {
|
firingEval := ""
|
||||||
|
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 +35,25 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// break if result has not triggered yet
|
// calculating Firing based on operator
|
||||||
if cr.Firing == false {
|
operator := "AND"
|
||||||
firing = false
|
if cr.Operator == "or" {
|
||||||
break
|
firing = firing || cr.Firing
|
||||||
|
operator = "OR"
|
||||||
|
} else {
|
||||||
|
firing = firing && cr.Firing
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
firingEval = "[" + firingEval + " " + operator + " " + strconv.FormatBool(cr.Firing) + "]"
|
||||||
|
} else {
|
||||||
|
firingEval = strconv.FormatBool(firing)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
|
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.FiringEval = firingEval + " = " + 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 (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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) {
|
|||||||
|
|
||||||
handler.Eval(context)
|
handler.Eval(context)
|
||||||
So(context.Firing, ShouldEqual, true)
|
So(context.Firing, ShouldEqual, true)
|
||||||
|
So(context.FiringEval, 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) {
|
|||||||
|
|
||||||
handler.Eval(context)
|
handler.Eval(context)
|
||||||
So(context.Firing, ShouldEqual, false)
|
So(context.Firing, ShouldEqual, false)
|
||||||
|
So(context.FiringEval, 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.FiringEval, 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.FiringEval, 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.FiringEval, 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.FiringEval, 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.FiringEval, 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.FiringEval, ShouldEqual, "[[true OR false] OR true] = true")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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 = [
|
|||||||
{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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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,7 +38,7 @@
|
|||||||
<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" 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">
|
||||||
|
Reference in New Issue
Block a user