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() {
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
}

View File

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

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 (
"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
}

View File

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

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) {
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,

View File

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

View File

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

View File

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