diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index a530517c5c4..6b195b91688 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -79,13 +79,15 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { conditionModel := simplejson.NewFromAny(condition) - switch conditionModel.Get("type").MustString() { - case "query": - queryCondition, err := NewQueryCondition(conditionModel, index) - if err != nil { + conditionType := conditionModel.Get("type").MustString() + if factory, exist := conditionFactories[conditionType]; !exist { + return nil, AlertValidationError{Reason: "Unknown alert condition: " + conditionType} + } else { + if queryCondition, err := factory(conditionModel, index); err != nil { return nil, err + } else { + model.Conditions = append(model.Conditions, queryCondition) } - model.Conditions = append(model.Conditions, queryCondition) } } @@ -95,3 +97,11 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { return model, nil } + +type ConditionFactory func(model *simplejson.Json, index int) (AlertCondition, error) + +var conditionFactories map[string]ConditionFactory = make(map[string]ConditionFactory) + +func RegisterCondition(typeName string, factory ConditionFactory) { + conditionFactories[typeName] = factory +} diff --git a/pkg/services/alerting/alert_rule_test.go b/pkg/services/alerting/alert_rule_test.go index 7a007946207..461920f3601 100644 --- a/pkg/services/alerting/alert_rule_test.go +++ b/pkg/services/alerting/alert_rule_test.go @@ -8,9 +8,17 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +type FakeCondition struct{} + +func (f *FakeCondition) Eval(context *AlertResultContext) {} + func TestAlertRuleModel(t *testing.T) { Convey("Testing alert rule", t, func() { + RegisterCondition("test", func(model *simplejson.Json, index int) (AlertCondition, error) { + return &FakeCondition{}, nil + }) + Convey("Can parse seconds", func() { seconds := getTimeDurationStringToSeconds("10s") So(seconds, ShouldEqual, 10) @@ -41,14 +49,8 @@ func TestAlertRuleModel(t *testing.T) { "frequency": "60s", "conditions": [ { - "type": "query", - "query": { - "params": ["A", "5m", "now"], - "datasourceId": 1, - "model": {"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} - }, - "reducer": {"type": "avg", "params": []}, - "evaluator": {"type": ">", "params": [100]} + "type": "test", + "prop": 123 } ], "notifications": [ @@ -75,27 +77,6 @@ func TestAlertRuleModel(t *testing.T) { So(alertRule.Conditions, ShouldHaveLength, 1) - Convey("Can read query condition from json model", func() { - queryCondition, ok := alertRule.Conditions[0].(*QueryCondition) - So(ok, ShouldBeTrue) - - So(queryCondition.Query.From, ShouldEqual, "5m") - So(queryCondition.Query.To, ShouldEqual, "now") - So(queryCondition.Query.DatasourceId, ShouldEqual, 1) - - Convey("Can read query reducer", func() { - reducer, ok := queryCondition.Reducer.(*SimpleReducer) - So(ok, ShouldBeTrue) - So(reducer.Type, ShouldEqual, "avg") - }) - - Convey("Can read evaluator", func() { - evaluator, ok := queryCondition.Evaluator.(*DefaultAlertEvaluator) - So(ok, ShouldBeTrue) - So(evaluator.Type, ShouldEqual, ">") - }) - }) - Convey("Can read notifications", func() { So(len(alertRule.Notifications), ShouldEqual, 2) }) diff --git a/pkg/services/alerting/conditions/common.go b/pkg/services/alerting/conditions/common.go new file mode 100644 index 00000000000..06702fd1e08 --- /dev/null +++ b/pkg/services/alerting/conditions/common.go @@ -0,0 +1 @@ +package conditions diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go new file mode 100644 index 00000000000..457e1726cd8 --- /dev/null +++ b/pkg/services/alerting/conditions/evaluator.go @@ -0,0 +1,51 @@ +package conditions + +import ( + "encoding/json" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/tsdb" +) + +type AlertEvaluator interface { + Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool +} + +type DefaultAlertEvaluator struct { + Type string + Threshold float64 +} + +func (e *DefaultAlertEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { + switch e.Type { + case ">": + return reducedValue > e.Threshold + case "<": + return reducedValue < e.Threshold + } + + return false +} + +func NewDefaultAlertEvaluator(model *simplejson.Json) (*DefaultAlertEvaluator, error) { + evaluator := &DefaultAlertEvaluator{} + + evaluator.Type = model.Get("type").MustString() + if evaluator.Type == "" { + return nil, alerting.AlertValidationError{Reason: "Evaluator missing type property"} + } + + params := model.Get("params").MustArray() + if len(params) == 0 { + return nil, alerting.AlertValidationError{Reason: "Evaluator missing threshold parameter"} + } + + threshold, ok := params[0].(json.Number) + if !ok { + return nil, alerting.AlertValidationError{Reason: "Evaluator has invalid threshold parameter"} + } + + evaluator.Threshold, _ = threshold.Float64() + return evaluator, nil +} diff --git a/pkg/services/alerting/conditions.go b/pkg/services/alerting/conditions/query.go similarity index 63% rename from pkg/services/alerting/conditions.go rename to pkg/services/alerting/conditions/query.go index 42affee9d57..5956ae87e41 100644 --- a/pkg/services/alerting/conditions.go +++ b/pkg/services/alerting/conditions/query.go @@ -1,15 +1,21 @@ -package alerting +package conditions import ( - "encoding/json" "fmt" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/tsdb" ) +func init() { + alerting.RegisterCondition("query", func(model *simplejson.Json, index int) (alerting.AlertCondition, error) { + return NewQueryCondition(model, index) + }) +} + type QueryCondition struct { Index int Query AlertQuery @@ -18,7 +24,14 @@ type QueryCondition struct { HandleRequest tsdb.HandleRequestFunc } -func (c *QueryCondition) Eval(context *AlertResultContext) { +type AlertQuery struct { + Model *simplejson.Json + DatasourceId int64 + From string + To string +} + +func (c *QueryCondition) Eval(context *alerting.AlertResultContext) { seriesList, err := c.executeQuery(context) if err != nil { context.Error = err @@ -30,13 +43,13 @@ func (c *QueryCondition) Eval(context *AlertResultContext) { pass := c.Evaluator.Eval(series, reducedValue) if context.IsTestRun { - context.Logs = append(context.Logs, &AlertResultLogEntry{ + context.Logs = append(context.Logs, &alerting.AlertResultLogEntry{ Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, pass, series.Name, reducedValue), }) } if pass { - context.Events = append(context.Events, &AlertEvent{ + context.Events = append(context.Events, &alerting.AlertEvent{ Metric: series.Name, Value: reducedValue, }) @@ -46,7 +59,7 @@ func (c *QueryCondition) Eval(context *AlertResultContext) { } } -func (c *QueryCondition) executeQuery(context *AlertResultContext) (tsdb.TimeSeriesSlice, error) { +func (c *QueryCondition) executeQuery(context *alerting.AlertResultContext) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ Id: c.Query.DatasourceId, OrgId: context.Rule.OrgId, @@ -72,7 +85,7 @@ func (c *QueryCondition) executeQuery(context *AlertResultContext) (tsdb.TimeSer result = append(result, v.Series...) if context.IsTestRun { - context.Logs = append(context.Logs, &AlertResultLogEntry{ + context.Logs = append(context.Logs, &alerting.AlertResultLogEntry{ Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index), Data: v.Series, }) @@ -129,63 +142,3 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro condition.Evaluator = evaluator return &condition, nil } - -type SimpleReducer struct { - Type string -} - -func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 { - var value float64 = 0 - - switch s.Type { - case "avg": - for _, point := range series.Points { - value += point[0] - } - value = value / float64(len(series.Points)) - } - - return value -} - -func NewSimpleReducer(typ string) *SimpleReducer { - return &SimpleReducer{Type: typ} -} - -type DefaultAlertEvaluator struct { - Type string - Threshold float64 -} - -func (e *DefaultAlertEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { - switch e.Type { - case ">": - return reducedValue > e.Threshold - case "<": - return reducedValue < e.Threshold - } - - return false -} - -func NewDefaultAlertEvaluator(model *simplejson.Json) (*DefaultAlertEvaluator, error) { - evaluator := &DefaultAlertEvaluator{} - - evaluator.Type = model.Get("type").MustString() - if evaluator.Type == "" { - return nil, AlertValidationError{Reason: "Evaluator missing type property"} - } - - params := model.Get("params").MustArray() - if len(params) == 0 { - return nil, AlertValidationError{Reason: "Evaluator missing threshold parameter"} - } - - threshold, ok := params[0].(json.Number) - if !ok { - return nil, AlertValidationError{Reason: "Evaluator has invalid threshold parameter"} - } - - evaluator.Threshold, _ = threshold.Float64() - return evaluator, nil -} diff --git a/pkg/services/alerting/conditions_test.go b/pkg/services/alerting/conditions/query_test.go similarity index 72% rename from pkg/services/alerting/conditions_test.go rename to pkg/services/alerting/conditions/query_test.go index 6fbe2ebe93b..0e893e3201e 100644 --- a/pkg/services/alerting/conditions_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -1,4 +1,4 @@ -package alerting +package conditions import ( "testing" @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) @@ -19,6 +20,26 @@ func TestQueryCondition(t *testing.T) { ctx.reducer = `{"type": "avg"}` ctx.evaluator = `{"type": ">", "params": [100]}` + Convey("Can read query condition from json model", func() { + ctx.exec() + + So(ctx.condition.Query.From, ShouldEqual, "5m") + So(ctx.condition.Query.To, ShouldEqual, "now") + So(ctx.condition.Query.DatasourceId, ShouldEqual, 1) + + Convey("Can read query reducer", func() { + reducer, ok := ctx.condition.Reducer.(*SimpleReducer) + So(ok, ShouldBeTrue) + So(reducer.Type, ShouldEqual, "avg") + }) + + Convey("Can read evaluator", func() { + evaluator, ok := ctx.condition.Evaluator.(*DefaultAlertEvaluator) + So(ok, ShouldBeTrue) + So(evaluator.Type, ShouldEqual, ">") + }) + }) + Convey("should fire when avg is above 100", func() { ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}})} ctx.exec() @@ -42,7 +63,8 @@ type queryConditionTestContext struct { reducer string evaluator string series tsdb.TimeSeriesSlice - result *AlertResultContext + result *alerting.AlertResultContext + condition *QueryCondition } type queryConditionScenarioFunc func(c *queryConditionTestContext) @@ -63,6 +85,8 @@ func (ctx *queryConditionTestContext) exec() { condition, err := NewQueryCondition(jsonModel, 0) So(err, ShouldBeNil) + ctx.condition = condition + condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { return &tsdb.Response{ Results: map[string]*tsdb.QueryResult{ @@ -83,8 +107,8 @@ func queryConditionScenario(desc string, fn queryConditionScenarioFunc) { }) ctx := &queryConditionTestContext{} - ctx.result = &AlertResultContext{ - Rule: &AlertRule{}, + ctx.result = &alerting.AlertResultContext{ + Rule: &alerting.AlertRule{}, } fn(ctx) diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go new file mode 100644 index 00000000000..d75d1ff9167 --- /dev/null +++ b/pkg/services/alerting/conditions/reducer.go @@ -0,0 +1,29 @@ +package conditions + +import "github.com/grafana/grafana/pkg/tsdb" + +type QueryReducer interface { + Reduce(timeSeries *tsdb.TimeSeries) float64 +} + +type SimpleReducer struct { + Type string +} + +func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 { + var value float64 = 0 + + switch s.Type { + case "avg": + for _, point := range series.Points { + value += point[0] + } + value = value / float64(len(series.Points)) + } + + return value +} + +func NewSimpleReducer(typ string) *SimpleReducer { + return &SimpleReducer{Type: typ} +} diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index cd88c8697a5..dda1d74674e 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -12,6 +12,11 @@ import ( func TestAlertRuleExtraction(t *testing.T) { Convey("Parsing alert rules from dashboard json", t, func() { + + RegisterCondition("query", func(model *simplejson.Json, index int) (AlertCondition, error) { + return &FakeCondition{}, nil + }) + Convey("Parsing and validating alerts from dashboards", func() { json := `{ "id": 57, diff --git a/pkg/services/alerting/init/init.go b/pkg/services/alerting/init/init.go index ef54cad07c7..b6627a359e6 100644 --- a/pkg/services/alerting/init/init.go +++ b/pkg/services/alerting/init/init.go @@ -2,6 +2,7 @@ package init import ( "github.com/grafana/grafana/pkg/services/alerting" + _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" "github.com/grafana/grafana/pkg/setting" _ "github.com/grafana/grafana/pkg/tsdb/graphite" diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 773e02b7fbd..2a72bc04607 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,10 +1,6 @@ package alerting -import ( - "time" - - "github.com/grafana/grafana/pkg/tsdb" -) +import "time" type AlertHandler interface { Execute(context *AlertResultContext) @@ -23,11 +19,3 @@ type Notifier interface { type AlertCondition interface { Eval(result *AlertResultContext) } - -type QueryReducer interface { - Reduce(timeSeries *tsdb.TimeSeries) float64 -} - -type AlertEvaluator interface { - Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool -} diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 4e47ee868c4..6459f7df3da 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -3,7 +3,6 @@ package alerting import ( "time" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" ) @@ -61,10 +60,3 @@ type Level struct { Operator string Value float64 } - -type AlertQuery struct { - Model *simplejson.Json - DatasourceId int64 - From string - To string -}