feat(alerting): refactoring conditions out to seperate package

This commit is contained in:
Torkel Ödegaard 2016-07-27 16:18:10 +02:00
parent ae5f8a76d9
commit 6aaf4c97a2
11 changed files with 161 additions and 126 deletions

View File

@ -79,13 +79,15 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) {
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
conditionModel := simplejson.NewFromAny(condition) conditionModel := simplejson.NewFromAny(condition)
switch conditionModel.Get("type").MustString() { conditionType := conditionModel.Get("type").MustString()
case "query": if factory, exist := conditionFactories[conditionType]; !exist {
queryCondition, err := NewQueryCondition(conditionModel, index) return nil, AlertValidationError{Reason: "Unknown alert condition: " + conditionType}
if err != nil { } else {
if queryCondition, err := factory(conditionModel, index); err != nil {
return nil, err 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 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
}

View File

@ -8,9 +8,17 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
type FakeCondition struct{}
func (f *FakeCondition) Eval(context *AlertResultContext) {}
func TestAlertRuleModel(t *testing.T) { func TestAlertRuleModel(t *testing.T) {
Convey("Testing alert rule", t, func() { Convey("Testing alert rule", t, func() {
RegisterCondition("test", func(model *simplejson.Json, index int) (AlertCondition, error) {
return &FakeCondition{}, nil
})
Convey("Can parse seconds", func() { Convey("Can parse seconds", func() {
seconds := getTimeDurationStringToSeconds("10s") seconds := getTimeDurationStringToSeconds("10s")
So(seconds, ShouldEqual, 10) So(seconds, ShouldEqual, 10)
@ -41,14 +49,8 @@ func TestAlertRuleModel(t *testing.T) {
"frequency": "60s", "frequency": "60s",
"conditions": [ "conditions": [
{ {
"type": "query", "type": "test",
"query": { "prop": 123
"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]}
} }
], ],
"notifications": [ "notifications": [
@ -75,27 +77,6 @@ func TestAlertRuleModel(t *testing.T) {
So(alertRule.Conditions, ShouldHaveLength, 1) 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() { Convey("Can read notifications", func() {
So(len(alertRule.Notifications), ShouldEqual, 2) So(len(alertRule.Notifications), ShouldEqual, 2)
}) })

View File

@ -0,0 +1 @@
package conditions

View File

@ -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
}

View File

@ -1,15 +1,21 @@
package alerting package conditions
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/tsdb" "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 { type QueryCondition struct {
Index int Index int
Query AlertQuery Query AlertQuery
@ -18,7 +24,14 @@ type QueryCondition struct {
HandleRequest tsdb.HandleRequestFunc 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) seriesList, err := c.executeQuery(context)
if err != nil { if err != nil {
context.Error = err context.Error = err
@ -30,13 +43,13 @@ func (c *QueryCondition) Eval(context *AlertResultContext) {
pass := c.Evaluator.Eval(series, reducedValue) pass := c.Evaluator.Eval(series, reducedValue)
if context.IsTestRun { 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), Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, pass, series.Name, reducedValue),
}) })
} }
if pass { if pass {
context.Events = append(context.Events, &AlertEvent{ context.Events = append(context.Events, &alerting.AlertEvent{
Metric: series.Name, Metric: series.Name,
Value: reducedValue, 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{ getDsInfo := &m.GetDataSourceByIdQuery{
Id: c.Query.DatasourceId, Id: c.Query.DatasourceId,
OrgId: context.Rule.OrgId, OrgId: context.Rule.OrgId,
@ -72,7 +85,7 @@ func (c *QueryCondition) executeQuery(context *AlertResultContext) (tsdb.TimeSer
result = append(result, v.Series...) result = append(result, v.Series...)
if context.IsTestRun { 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), Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index),
Data: v.Series, Data: v.Series,
}) })
@ -129,63 +142,3 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
condition.Evaluator = evaluator condition.Evaluator = evaluator
return &condition, nil 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
}

View File

@ -1,4 +1,4 @@
package alerting package conditions
import ( import (
"testing" "testing"
@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
@ -19,6 +20,26 @@ func TestQueryCondition(t *testing.T) {
ctx.reducer = `{"type": "avg"}` ctx.reducer = `{"type": "avg"}`
ctx.evaluator = `{"type": ">", "params": [100]}` 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() { Convey("should fire when avg is above 100", func() {
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}})} ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}})}
ctx.exec() ctx.exec()
@ -42,7 +63,8 @@ type queryConditionTestContext struct {
reducer string reducer string
evaluator string evaluator string
series tsdb.TimeSeriesSlice series tsdb.TimeSeriesSlice
result *AlertResultContext result *alerting.AlertResultContext
condition *QueryCondition
} }
type queryConditionScenarioFunc func(c *queryConditionTestContext) type queryConditionScenarioFunc func(c *queryConditionTestContext)
@ -63,6 +85,8 @@ func (ctx *queryConditionTestContext) exec() {
condition, err := NewQueryCondition(jsonModel, 0) condition, err := NewQueryCondition(jsonModel, 0)
So(err, ShouldBeNil) So(err, ShouldBeNil)
ctx.condition = condition
condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) {
return &tsdb.Response{ return &tsdb.Response{
Results: map[string]*tsdb.QueryResult{ Results: map[string]*tsdb.QueryResult{
@ -83,8 +107,8 @@ func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
}) })
ctx := &queryConditionTestContext{} ctx := &queryConditionTestContext{}
ctx.result = &AlertResultContext{ ctx.result = &alerting.AlertResultContext{
Rule: &AlertRule{}, Rule: &alerting.AlertRule{},
} }
fn(ctx) fn(ctx)

View File

@ -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}
}

View File

@ -12,6 +12,11 @@ import (
func TestAlertRuleExtraction(t *testing.T) { func TestAlertRuleExtraction(t *testing.T) {
Convey("Parsing alert rules from dashboard json", t, func() { 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() { Convey("Parsing and validating alerts from dashboards", func() {
json := `{ json := `{
"id": 57, "id": 57,

View File

@ -2,6 +2,7 @@ package init
import ( import (
"github.com/grafana/grafana/pkg/services/alerting" "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/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
_ "github.com/grafana/grafana/pkg/tsdb/graphite" _ "github.com/grafana/grafana/pkg/tsdb/graphite"

View File

@ -1,10 +1,6 @@
package alerting package alerting
import ( import "time"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
type AlertHandler interface { type AlertHandler interface {
Execute(context *AlertResultContext) Execute(context *AlertResultContext)
@ -23,11 +19,3 @@ type Notifier interface {
type AlertCondition interface { type AlertCondition interface {
Eval(result *AlertResultContext) Eval(result *AlertResultContext)
} }
type QueryReducer interface {
Reduce(timeSeries *tsdb.TimeSeries) float64
}
type AlertEvaluator interface {
Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool
}

View File

@ -3,7 +3,6 @@ package alerting
import ( import (
"time" "time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
) )
@ -61,10 +60,3 @@ type Level struct {
Operator string Operator string
Value float64 Value float64
} }
type AlertQuery struct {
Model *simplejson.Json
DatasourceId int64
From string
To string
}