mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
feat(alerting): refactoring conditions out to seperate package
This commit is contained in:
parent
ae5f8a76d9
commit
6aaf4c97a2
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
1
pkg/services/alerting/conditions/common.go
Normal file
1
pkg/services/alerting/conditions/common.go
Normal file
@ -0,0 +1 @@
|
||||
package conditions
|
51
pkg/services/alerting/conditions/evaluator.go
Normal file
51
pkg/services/alerting/conditions/evaluator.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
29
pkg/services/alerting/conditions/reducer.go
Normal file
29
pkg/services/alerting/conditions/reducer.go
Normal 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}
|
||||
}
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user