2016-07-27 09:18:10 -05:00
|
|
|
package conditions
|
2016-07-20 02:30:31 -05:00
|
|
|
|
|
|
|
import (
|
2016-10-03 02:38:03 -05:00
|
|
|
"context"
|
2020-10-09 07:21:16 -05:00
|
|
|
"math"
|
2016-07-20 02:30:31 -05:00
|
|
|
"testing"
|
2020-03-18 09:30:07 -05:00
|
|
|
"time"
|
2016-07-20 02:30:31 -05:00
|
|
|
|
2020-10-09 07:21:16 -05:00
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
2020-03-18 09:30:07 -05:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
2016-07-20 07:28:02 -05:00
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
2017-01-13 05:32:30 -06:00
|
|
|
"github.com/grafana/grafana/pkg/components/null"
|
2016-07-20 02:30:31 -05:00
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
2019-05-14 01:15:05 -05:00
|
|
|
"github.com/grafana/grafana/pkg/models"
|
2016-07-27 09:18:10 -05:00
|
|
|
"github.com/grafana/grafana/pkg/services/alerting"
|
2016-07-20 07:28:02 -05:00
|
|
|
"github.com/grafana/grafana/pkg/tsdb"
|
2016-07-20 02:30:31 -05:00
|
|
|
. "github.com/smartystreets/goconvey/convey"
|
2020-10-09 07:21:16 -05:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/xorcare/pointer"
|
2016-07-20 02:30:31 -05:00
|
|
|
)
|
|
|
|
|
2020-11-05 04:00:00 -06:00
|
|
|
func newTimeSeriesPointsFromArgs(values ...float64) tsdb.TimeSeriesPoints {
|
|
|
|
points := make(tsdb.TimeSeriesPoints, 0)
|
|
|
|
|
|
|
|
for i := 0; i < len(values); i += 2 {
|
|
|
|
points = append(points, tsdb.NewTimePoint(null.FloatFrom(values[i]), values[i+1]))
|
|
|
|
}
|
|
|
|
|
|
|
|
return points
|
|
|
|
}
|
|
|
|
|
2016-07-20 02:30:31 -05:00
|
|
|
func TestQueryCondition(t *testing.T) {
|
|
|
|
Convey("when evaluating query condition", t, func() {
|
2016-07-21 09:19:28 -05:00
|
|
|
queryConditionScenario("Given avg() and > 100", func(ctx *queryConditionTestContext) {
|
|
|
|
ctx.reducer = `{"type": "avg"}`
|
2016-08-16 03:22:16 -05:00
|
|
|
ctx.evaluator = `{"type": "gt", "params": [100]}`
|
2016-07-21 09:19:28 -05:00
|
|
|
|
2016-07-27 09:18:10 -05:00
|
|
|
Convey("Can read query condition from json model", func() {
|
2019-10-22 07:08:18 -05:00
|
|
|
_, err := ctx.exec()
|
|
|
|
So(err, ShouldBeNil)
|
2016-07-27 09:18:10 -05:00
|
|
|
|
|
|
|
So(ctx.condition.Query.From, ShouldEqual, "5m")
|
|
|
|
So(ctx.condition.Query.To, ShouldEqual, "now")
|
2019-06-03 03:25:58 -05:00
|
|
|
So(ctx.condition.Query.DatasourceID, ShouldEqual, 1)
|
2016-07-27 09:18:10 -05:00
|
|
|
|
|
|
|
Convey("Can read query reducer", func() {
|
2019-06-03 03:25:58 -05:00
|
|
|
reducer := ctx.condition.Reducer
|
2016-07-27 09:18:10 -05:00
|
|
|
So(reducer.Type, ShouldEqual, "avg")
|
|
|
|
})
|
|
|
|
|
|
|
|
Convey("Can read evaluator", func() {
|
2019-06-03 03:25:58 -05:00
|
|
|
evaluator, ok := ctx.condition.Evaluator.(*thresholdEvaluator)
|
2016-07-27 09:18:10 -05:00
|
|
|
So(ok, ShouldBeTrue)
|
2016-08-16 01:12:55 -05:00
|
|
|
So(evaluator.Type, ShouldEqual, "gt")
|
2016-07-27 09:18:10 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2016-07-22 06:14:09 -05:00
|
|
|
Convey("should fire when avg is above 100", func() {
|
2020-11-05 04:00:00 -06:00
|
|
|
points := newTimeSeriesPointsFromArgs(120, 0)
|
2020-11-17 04:51:31 -06:00
|
|
|
ctx.series = tsdb.TimeSeriesSlice{&tsdb.TimeSeries{Name: "test1", Points: points}}
|
2016-11-03 09:26:17 -05:00
|
|
|
cr, err := ctx.exec()
|
2016-07-21 09:19:28 -05:00
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeTrue)
|
2016-07-21 09:19:28 -05:00
|
|
|
})
|
|
|
|
|
2020-03-18 09:30:07 -05:00
|
|
|
Convey("should fire when avg is above 100 on dataframe", func() {
|
|
|
|
ctx.frame = data.NewFrame("",
|
2020-06-09 06:13:06 -05:00
|
|
|
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
2020-03-18 09:30:07 -05:00
|
|
|
data.NewField("val", nil, []int64{120, 150}),
|
|
|
|
)
|
|
|
|
cr, err := ctx.exec()
|
|
|
|
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeTrue)
|
|
|
|
})
|
|
|
|
|
2016-07-22 06:14:09 -05:00
|
|
|
Convey("Should not fire when avg is below 100", func() {
|
2020-11-05 04:00:00 -06:00
|
|
|
points := newTimeSeriesPointsFromArgs(90, 0)
|
2020-11-17 04:51:31 -06:00
|
|
|
ctx.series = tsdb.TimeSeriesSlice{&tsdb.TimeSeries{Name: "test1", Points: points}}
|
2016-11-03 09:26:17 -05:00
|
|
|
cr, err := ctx.exec()
|
2016-07-21 09:19:28 -05:00
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeFalse)
|
2016-07-21 09:19:28 -05:00
|
|
|
})
|
2016-09-08 06:28:41 -05:00
|
|
|
|
2020-03-18 09:30:07 -05:00
|
|
|
Convey("Should not fire when avg is below 100 on dataframe", func() {
|
|
|
|
ctx.frame = data.NewFrame("",
|
2020-06-09 06:13:06 -05:00
|
|
|
data.NewField("time", nil, []time.Time{time.Now(), time.Now()}),
|
2020-03-18 09:30:07 -05:00
|
|
|
data.NewField("val", nil, []int64{12, 47}),
|
|
|
|
)
|
|
|
|
cr, err := ctx.exec()
|
|
|
|
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeFalse)
|
|
|
|
})
|
|
|
|
|
2020-06-01 10:11:25 -05:00
|
|
|
Convey("Should fire if only first series matches", func() {
|
2016-09-08 06:28:41 -05:00
|
|
|
ctx.series = tsdb.TimeSeriesSlice{
|
2020-11-17 04:51:31 -06:00
|
|
|
&tsdb.TimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs(120, 0)},
|
|
|
|
&tsdb.TimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs(0, 0)},
|
2016-09-08 06:28:41 -05:00
|
|
|
}
|
2016-11-03 09:26:17 -05:00
|
|
|
cr, err := ctx.exec()
|
2016-09-08 06:28:41 -05:00
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeTrue)
|
2016-09-08 06:28:41 -05:00
|
|
|
})
|
2016-09-08 07:33:10 -05:00
|
|
|
|
2017-01-13 05:32:30 -06:00
|
|
|
Convey("No series", func() {
|
|
|
|
Convey("Should set NoDataFound when condition is gt", func() {
|
|
|
|
ctx.series = tsdb.TimeSeriesSlice{}
|
|
|
|
cr, err := ctx.exec()
|
|
|
|
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeFalse)
|
|
|
|
So(cr.NoDataFound, ShouldBeTrue)
|
|
|
|
})
|
|
|
|
|
|
|
|
Convey("Should be firing when condition is no_value", func() {
|
|
|
|
ctx.evaluator = `{"type": "no_value", "params": []}`
|
|
|
|
ctx.series = tsdb.TimeSeriesSlice{}
|
|
|
|
cr, err := ctx.exec()
|
|
|
|
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeTrue)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2016-09-08 07:33:10 -05:00
|
|
|
Convey("Empty series", func() {
|
2017-01-13 05:32:30 -06:00
|
|
|
Convey("Should set Firing if eval match", func() {
|
|
|
|
ctx.evaluator = `{"type": "no_value", "params": []}`
|
|
|
|
ctx.series = tsdb.TimeSeriesSlice{
|
2020-11-17 04:51:31 -06:00
|
|
|
&tsdb.TimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
2017-01-13 05:32:30 -06:00
|
|
|
}
|
|
|
|
cr, err := ctx.exec()
|
|
|
|
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.Firing, ShouldBeTrue)
|
|
|
|
})
|
|
|
|
|
2016-09-08 07:33:10 -05:00
|
|
|
Convey("Should set NoDataFound both series are empty", func() {
|
|
|
|
ctx.series = tsdb.TimeSeriesSlice{
|
2020-11-17 04:51:31 -06:00
|
|
|
&tsdb.TimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
|
|
|
&tsdb.TimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs()},
|
2016-09-08 07:33:10 -05:00
|
|
|
}
|
2016-11-03 09:26:17 -05:00
|
|
|
cr, err := ctx.exec()
|
2016-09-08 07:33:10 -05:00
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.NoDataFound, ShouldBeTrue)
|
2016-09-08 07:33:10 -05:00
|
|
|
})
|
|
|
|
|
2016-09-16 07:58:10 -05:00
|
|
|
Convey("Should set NoDataFound both series contains null", func() {
|
|
|
|
ctx.series = tsdb.TimeSeriesSlice{
|
2020-11-17 04:51:31 -06:00
|
|
|
&tsdb.TimeSeries{Name: "test1", Points: tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}},
|
|
|
|
&tsdb.TimeSeries{Name: "test2", Points: tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}},
|
2016-09-16 07:58:10 -05:00
|
|
|
}
|
2016-11-03 09:26:17 -05:00
|
|
|
cr, err := ctx.exec()
|
2016-09-16 07:58:10 -05:00
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.NoDataFound, ShouldBeTrue)
|
2016-09-16 07:58:10 -05:00
|
|
|
})
|
|
|
|
|
2020-06-01 10:11:25 -05:00
|
|
|
Convey("Should not set NoDataFound if one series is empty", func() {
|
2016-09-08 07:33:10 -05:00
|
|
|
ctx.series = tsdb.TimeSeriesSlice{
|
2020-11-17 04:51:31 -06:00
|
|
|
&tsdb.TimeSeries{Name: "test1", Points: newTimeSeriesPointsFromArgs()},
|
|
|
|
&tsdb.TimeSeries{Name: "test2", Points: newTimeSeriesPointsFromArgs(120, 0)},
|
2016-09-08 07:33:10 -05:00
|
|
|
}
|
2016-11-03 09:26:17 -05:00
|
|
|
cr, err := ctx.exec()
|
2016-09-08 07:33:10 -05:00
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
So(cr.NoDataFound, ShouldBeFalse)
|
2016-09-08 07:33:10 -05:00
|
|
|
})
|
|
|
|
})
|
2016-07-20 07:28:02 -05:00
|
|
|
})
|
2016-07-21 09:19:28 -05:00
|
|
|
})
|
|
|
|
}
|
2016-07-20 07:28:02 -05:00
|
|
|
|
2016-07-21 09:19:28 -05:00
|
|
|
type queryConditionTestContext struct {
|
|
|
|
reducer string
|
|
|
|
evaluator string
|
|
|
|
series tsdb.TimeSeriesSlice
|
2020-03-18 09:30:07 -05:00
|
|
|
frame *data.Frame
|
2016-07-27 09:29:28 -05:00
|
|
|
result *alerting.EvalContext
|
2016-07-27 09:18:10 -05:00
|
|
|
condition *QueryCondition
|
2016-07-21 09:19:28 -05:00
|
|
|
}
|
2016-07-20 02:30:31 -05:00
|
|
|
|
2016-07-21 09:19:28 -05:00
|
|
|
type queryConditionScenarioFunc func(c *queryConditionTestContext)
|
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
|
2016-07-21 09:19:28 -05:00
|
|
|
jsonModel, err := simplejson.NewJson([]byte(`{
|
2016-07-20 02:30:31 -05:00
|
|
|
"type": "query",
|
|
|
|
"query": {
|
|
|
|
"params": ["A", "5m", "now"],
|
|
|
|
"datasourceId": 1,
|
|
|
|
"model": {"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
|
|
|
},
|
2016-07-21 09:19:28 -05:00
|
|
|
"reducer":` + ctx.reducer + `,
|
|
|
|
"evaluator":` + ctx.evaluator + `
|
2016-07-20 02:30:31 -05:00
|
|
|
}`))
|
2016-07-21 09:19:28 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
|
2019-06-03 03:25:58 -05:00
|
|
|
condition, err := newQueryCondition(jsonModel, 0)
|
2016-07-21 09:19:28 -05:00
|
|
|
So(err, ShouldBeNil)
|
|
|
|
|
2016-07-27 09:18:10 -05:00
|
|
|
ctx.condition = condition
|
|
|
|
|
2020-03-18 09:30:07 -05:00
|
|
|
qr := &tsdb.QueryResult{
|
|
|
|
Series: ctx.series,
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.frame != nil {
|
|
|
|
qr = &tsdb.QueryResult{
|
2020-06-09 06:13:06 -05:00
|
|
|
Dataframes: tsdb.NewDecodedDataFrames(data.Frames{ctx.frame}),
|
2020-03-18 09:30:07 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-14 01:15:05 -05:00
|
|
|
condition.HandleRequest = func(context context.Context, dsInfo *models.DataSource, req *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
2016-07-21 09:19:28 -05:00
|
|
|
return &tsdb.Response{
|
|
|
|
Results: map[string]*tsdb.QueryResult{
|
2020-03-18 09:30:07 -05:00
|
|
|
"A": qr,
|
2016-07-21 09:19:28 -05:00
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2016-11-03 09:26:17 -05:00
|
|
|
return condition.Eval(ctx.result)
|
2016-07-21 09:19:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
|
|
|
|
Convey(desc, func() {
|
2019-05-14 01:15:05 -05:00
|
|
|
bus.AddHandler("test", func(query *models.GetDataSourceByIdQuery) error {
|
|
|
|
query.Result = &models.DataSource{Id: 1, Type: "graphite"}
|
2016-07-21 09:19:28 -05:00
|
|
|
return nil
|
2016-07-20 02:30:31 -05:00
|
|
|
})
|
|
|
|
|
2016-07-21 09:19:28 -05:00
|
|
|
ctx := &queryConditionTestContext{}
|
2016-07-27 09:29:28 -05:00
|
|
|
ctx.result = &alerting.EvalContext{
|
|
|
|
Rule: &alerting.Rule{},
|
2016-07-21 09:19:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
fn(ctx)
|
2016-07-20 02:30:31 -05:00
|
|
|
})
|
|
|
|
}
|
2020-10-09 07:21:16 -05:00
|
|
|
|
|
|
|
func TestFrameToSeriesSlice(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
frame *data.Frame
|
|
|
|
seriesSlice tsdb.TimeSeriesSlice
|
|
|
|
Err require.ErrorAssertionFunc
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "a wide series",
|
|
|
|
frame: data.NewFrame("",
|
|
|
|
data.NewField("Time", nil, []time.Time{
|
|
|
|
time.Date(2020, 1, 2, 3, 4, 0, 0, time.UTC),
|
|
|
|
time.Date(2020, 1, 2, 3, 4, 30, 0, time.UTC),
|
|
|
|
}),
|
|
|
|
data.NewField(`Values Int64s`, data.Labels{"Animal Factor": "cat"}, []*int64{
|
|
|
|
nil,
|
|
|
|
pointer.Int64(3),
|
|
|
|
}),
|
|
|
|
data.NewField(`Values Floats`, data.Labels{"Animal Factor": "sloth"}, []float64{
|
|
|
|
2.0,
|
|
|
|
4.0,
|
|
|
|
})),
|
|
|
|
|
|
|
|
seriesSlice: tsdb.TimeSeriesSlice{
|
|
|
|
&tsdb.TimeSeries{
|
|
|
|
Name: "Values Int64s {Animal Factor=cat}",
|
|
|
|
Tags: map[string]string{"Animal Factor": "cat"},
|
|
|
|
Points: tsdb.TimeSeriesPoints{
|
|
|
|
tsdb.TimePoint{null.FloatFrom(math.NaN()), null.FloatFrom(1577934240000)},
|
|
|
|
tsdb.TimePoint{null.FloatFrom(3), null.FloatFrom(1577934270000)},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
&tsdb.TimeSeries{
|
|
|
|
Name: "Values Floats {Animal Factor=sloth}",
|
|
|
|
Tags: map[string]string{"Animal Factor": "sloth"},
|
|
|
|
Points: tsdb.TimeSeriesPoints{
|
|
|
|
tsdb.TimePoint{null.FloatFrom(2), null.FloatFrom(1577934240000)},
|
|
|
|
tsdb.TimePoint{null.FloatFrom(4), null.FloatFrom(1577934270000)},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Err: require.NoError,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "empty wide series",
|
|
|
|
frame: data.NewFrame("",
|
|
|
|
data.NewField("Time", nil, []time.Time{}),
|
|
|
|
data.NewField(`Values Int64s`, data.Labels{"Animal Factor": "cat"}, []*int64{}),
|
|
|
|
data.NewField(`Values Floats`, data.Labels{"Animal Factor": "sloth"}, []float64{})),
|
|
|
|
|
|
|
|
seriesSlice: tsdb.TimeSeriesSlice{
|
|
|
|
&tsdb.TimeSeries{
|
|
|
|
Name: "Values Int64s {Animal Factor=cat}",
|
|
|
|
Tags: map[string]string{"Animal Factor": "cat"},
|
|
|
|
Points: tsdb.TimeSeriesPoints{},
|
|
|
|
},
|
|
|
|
&tsdb.TimeSeries{
|
|
|
|
Name: "Values Floats {Animal Factor=sloth}",
|
|
|
|
Tags: map[string]string{"Animal Factor": "sloth"},
|
|
|
|
Points: tsdb.TimeSeriesPoints{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Err: require.NoError,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
seriesSlice, err := FrameToSeriesSlice(tt.frame)
|
|
|
|
tt.Err(t, err)
|
|
|
|
if diff := cmp.Diff(tt.seriesSlice, seriesSlice, cmpopts.EquateNaNs()); diff != "" {
|
|
|
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|