Sfoglia il codice sorgente

Merge branch 'or_alerting' of https://github.com/utkarshcmu/grafana into utkarshcmu-or_alerting

Torkel Ödegaard 9 anni fa
parent
commit
457ae74343

+ 4 - 1
docs/sources/alerting/rules.md

@@ -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
 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
-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
 of another alert in your conditions, and `Time Of Day`.

+ 2 - 1
pkg/api/alerting.go

@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
 	res := backendCmd.Result
 
 	dtoRes := &dtos.AlertTestResult{
-		Firing: res.Firing,
+		Firing:     res.Firing,
+		FiringEval: res.FiringEval,
 	}
 
 	if res.Error != nil {

+ 1 - 0
pkg/api/dtos/alerting.go

@@ -36,6 +36,7 @@ type AlertTestCommand struct {
 
 type AlertTestResult struct {
 	Firing      bool                  `json:"firing"`
+	FiringEval  string                `json:"firingEvaluation"`
 	TimeMs      string                `json:"timeMs"`
 	Error       string                `json:"error,omitempty"`
 	EvalMatches []*EvalMatch          `json:"matches,omitempty"`

+ 7 - 1
pkg/services/alerting/conditions/query.go

@@ -23,6 +23,7 @@ type QueryCondition struct {
 	Query         AlertQuery
 	Reducer       QueryReducer
 	Evaluator     AlertEvaluator
+	Operator      string
 	HandleRequest tsdb.HandleRequestFunc
 }
 
@@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
 	return &alerting.ConditionResult{
 		Firing:      evalMatchCount > 0,
 		NoDataFound: emptySerieCount == len(seriesList),
+		Operator:    c.Operator,
 		EvalMatches: matches,
 	}, nil
 }
@@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
 	if err != nil {
 		return nil, err
 	}
-
 	condition.Evaluator = evaluator
+
+	operatorJson := model.Get("operator")
+	operator := operatorJson.Get("type").MustString()
+	condition.Operator = operator
+
 	return &condition, nil
 }
 

+ 1 - 0
pkg/services/alerting/eval_context.go

@@ -18,6 +18,7 @@ type EvalContext struct {
 	Logs            []*ResultLogEntry
 	Error           error
 	Description     string
+	FiringEval      string
 	StartTime       time.Time
 	EndTime         time.Time
 	Rule            *Rule

+ 18 - 5
pkg/services/alerting/eval_handler.go

@@ -1,6 +1,7 @@
 package alerting
 
 import (
+	"strconv"
 	"time"
 
 	"github.com/grafana/grafana/pkg/log"
@@ -21,7 +22,9 @@ func NewEvalHandler() *DefaultEvalHandler {
 
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 	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)
 		if err != nil {
 			context.Error = err
@@ -32,15 +35,25 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 			break
 		}
 
-		// break if result has not triggered yet
-		if cr.Firing == false {
-			firing = false
-			break
+		// calculating Firing based on operator
+		operator := "AND"
+		if cr.Operator == "or" {
+			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.FiringEval = firingEval + " = " + strconv.FormatBool(firing)
 	context.Firing = firing
 	context.EndTime = time.Now()
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond

+ 88 - 3
pkg/services/alerting/eval_handler_test.go

@@ -8,12 +8,13 @@ import (
 )
 
 type conditionStub struct {
-	firing  bool
-	matches []*EvalMatch
+	firing   bool
+	operator string
+	matches  []*EvalMatch
 }
 
 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) {
@@ -29,6 +30,7 @@ func TestAlertingExecutor(t *testing.T) {
 
 			handler.Eval(context)
 			So(context.Firing, ShouldEqual, true)
+			So(context.FiringEval, ShouldEqual, "true = true")
 		})
 
 		Convey("Show return false with not passing asdf", func() {
@@ -41,6 +43,89 @@ func TestAlertingExecutor(t *testing.T) {
 
 			handler.Eval(context)
 			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")
 		})
 	})
 }

+ 1 - 0
pkg/services/alerting/interfaces.go

@@ -24,6 +24,7 @@ type Notifier interface {
 type ConditionResult struct {
 	Firing      bool
 	NoDataFound bool
+	Operator    string
 	EvalMatches []*EvalMatch
 }
 

+ 6 - 0
public/app/features/alerting/alert_def.ts

@@ -28,6 +28,11 @@ var evalFunctions = [
   {text: 'HAS NO VALUE' , value: 'no_value'}
 ];
 
+var evalOperators = [
+  {text: 'OR', value: 'or'},
+  {text: 'AND', value: 'and'},
+];
+
 var reducerTypes = [
   {text: 'avg()', value: 'avg'},
   {text: 'min()', value: 'min'},
@@ -116,6 +121,7 @@ export default {
   getStateDisplayModel: getStateDisplayModel,
   conditionTypes: conditionTypes,
   evalFunctions: evalFunctions,
+  evalOperators: evalOperators,
   noDataModes: noDataModes,
   executionErrorModes: executionErrorModes,
   reducerTypes: reducerTypes,

+ 4 - 0
public/app/features/alerting/alert_tab_ctrl.ts

@@ -18,6 +18,7 @@ export class AlertTabCtrl {
   alert: any;
   conditionModels: any;
   evalFunctions: any;
+  evalOperators: any;
   noDataModes: any;
   executionErrorModes: any;
   addNotificationSegment;
@@ -41,6 +42,7 @@ export class AlertTabCtrl {
     this.$scope.ctrl = this;
     this.subTabIndex = 0;
     this.evalFunctions = alertDef.evalFunctions;
+    this.evalOperators = alertDef.evalOperators;
     this.conditionTypes = alertDef.conditionTypes;
     this.noDataModes = alertDef.noDataModes;
     this.executionErrorModes = alertDef.executionErrorModes;
@@ -194,6 +196,7 @@ export class AlertTabCtrl {
       query: {params: ['A', '5m', 'now']},
       reducer: {type: 'avg', params: []},
       evaluator: {type: 'gt', params: [null]},
+      operator: {type: 'and'},
     };
   }
 
@@ -250,6 +253,7 @@ export class AlertTabCtrl {
     cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
     cm.reducerPart = alertDef.createReducerPart(source.reducer);
     cm.evaluator = source.evaluator;
+    cm.operator = source.operator;
 
     return cm;
   }

+ 1 - 1
public/app/features/alerting/partials/alert_tab.html

@@ -38,7 +38,7 @@
 				<h5 class="section-heading">Conditions</h5>
 				<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
 					<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>
 					</div>
           <div class="gf-form">