func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) { timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To) seriesList, err := c.executeQuery(context, timeRange) if err != nil { return nil, err } emptySerieCount := 0 evalMatchCount := 0 var matches []*alerting.EvalMatch for _, series := range seriesList { reducedValue := c.Reducer.Reduce(series) evalMatch := c.Evaluator.Eval(reducedValue) if reducedValue.Valid == false { emptySerieCount++ } if context.IsTestRun { context.Logs = append(context.Logs, &alerting.ResultLogEntry{ Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %s", c.Index, evalMatch, series.Name, reducedValue), }) } if evalMatch { evalMatchCount++ matches = append(matches, &alerting.EvalMatch{ Metric: series.Name, Value: reducedValue, }) } } // handle no series special case if len(seriesList) == 0 { // eval condition for null value evalMatch := c.Evaluator.Eval(null.FloatFromPtr(nil)) if context.IsTestRun { context.Logs = append(context.Logs, &alerting.ResultLogEntry{ Message: fmt.Sprintf("Condition[%d]: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch), }) } if evalMatch { evalMatchCount++ matches = append(matches, &alerting.EvalMatch{Metric: "NoData", Value: null.FloatFromPtr(nil)}) } } return &alerting.ConditionResult{ Firing: evalMatchCount > 0, NoDataFound: emptySerieCount == len(seriesList), Operator: c.Operator, EvalMatches: matches, }, nil }
func (rp *ResponseParser) parseValue(value interface{}) null.Float { number, ok := value.(json.Number) if !ok { return null.FloatFromPtr(nil) } fvalue, err := number.Float64() if err == nil { return null.FloatFrom(fvalue) } ivalue, err := number.Int64() if err == nil { return null.FloatFrom(float64(ivalue)) } return null.FloatFromPtr(nil) }
func TestEvalutors(t *testing.T) { Convey("greater then", t, func() { So(evalutorScenario(`{"type": "gt", "params": [1] }`, 3), ShouldBeTrue) So(evalutorScenario(`{"type": "gt", "params": [3] }`, 1), ShouldBeFalse) }) Convey("less then", t, func() { So(evalutorScenario(`{"type": "lt", "params": [1] }`, 3), ShouldBeFalse) So(evalutorScenario(`{"type": "lt", "params": [3] }`, 1), ShouldBeTrue) }) Convey("within_range", t, func() { So(evalutorScenario(`{"type": "within_range", "params": [1, 100] }`, 3), ShouldBeTrue) So(evalutorScenario(`{"type": "within_range", "params": [1, 100] }`, 300), ShouldBeFalse) So(evalutorScenario(`{"type": "within_range", "params": [100, 1] }`, 3), ShouldBeTrue) So(evalutorScenario(`{"type": "within_range", "params": [100, 1] }`, 300), ShouldBeFalse) }) Convey("outside_range", t, func() { So(evalutorScenario(`{"type": "outside_range", "params": [1, 100] }`, 1000), ShouldBeTrue) So(evalutorScenario(`{"type": "outside_range", "params": [1, 100] }`, 50), ShouldBeFalse) So(evalutorScenario(`{"type": "outside_range", "params": [100, 1] }`, 1000), ShouldBeTrue) So(evalutorScenario(`{"type": "outside_range", "params": [100, 1] }`, 50), ShouldBeFalse) }) Convey("no_value", t, func() { Convey("should be false if serie have values", func() { So(evalutorScenario(`{"type": "no_value", "params": [] }`, 50), ShouldBeFalse) }) Convey("should be true when the serie have no value", func() { jsonModel, err := simplejson.NewJson([]byte(`{"type": "no_value", "params": [] }`)) So(err, ShouldBeNil) evaluator, err := NewAlertEvaluator(jsonModel) So(err, ShouldBeNil) So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue) }) }) }
func TestQueryCondition(t *testing.T) { Convey("when evaluating query condition", t, func() { queryConditionScenario("Given avg() and > 100", func(ctx *queryConditionTestContext) { ctx.reducer = `{"type": "avg"}` ctx.evaluator = `{"type": "gt", "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.(*ThresholdEvaluator) So(ok, ShouldBeTrue) So(evaluator.Type, ShouldEqual, "gt") }) }) Convey("should fire when avg is above 100", func() { points := tsdb.NewTimeSeriesPointsFromArgs(120, 0) ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.Firing, ShouldBeTrue) }) Convey("Should not fire when avg is below 100", func() { points := tsdb.NewTimeSeriesPointsFromArgs(90, 0) ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)} cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.Firing, ShouldBeFalse) }) Convey("Should fire if only first serie matches", func() { ctx.series = tsdb.TimeSeriesSlice{ tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)), } cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.Firing, ShouldBeTrue) }) 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) }) }) Convey("Empty series", func() { Convey("Should set Firing if eval match", func() { ctx.evaluator = `{"type": "no_value", "params": []}` ctx.series = tsdb.TimeSeriesSlice{ tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), } cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.Firing, ShouldBeTrue) }) Convey("Should set NoDataFound both series are empty", func() { ctx.series = tsdb.TimeSeriesSlice{ tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()), } cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.NoDataFound, ShouldBeTrue) }) Convey("Should set NoDataFound both series contains null", func() { ctx.series = tsdb.TimeSeriesSlice{ tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}), } cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.NoDataFound, ShouldBeTrue) }) Convey("Should not set NoDataFound if one serie is empty", func() { ctx.series = tsdb.TimeSeriesSlice{ tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)), } cr, err := ctx.exec() So(err, ShouldBeNil) So(cr.NoDataFound, ShouldBeFalse) }) }) }) }) }
func TestSimpleReducer(t *testing.T) { Convey("Test simple reducer by calculating", t, func() { Convey("sum", func() { result := testReducer("sum", 1, 2, 3) So(result, ShouldEqual, float64(6)) }) Convey("min", func() { result := testReducer("min", 3, 2, 1) So(result, ShouldEqual, float64(1)) }) Convey("max", func() { result := testReducer("max", 1, 2, 3) So(result, ShouldEqual, float64(3)) }) Convey("count", func() { result := testReducer("count", 1, 2, 3000) So(result, ShouldEqual, float64(3)) }) Convey("last", func() { result := testReducer("last", 1, 2, 3000) So(result, ShouldEqual, float64(3000)) }) Convey("median odd amount of numbers", func() { result := testReducer("median", 1, 2, 3000) So(result, ShouldEqual, float64(2)) }) Convey("median even amount of numbers", func() { result := testReducer("median", 1, 2, 4, 3000) So(result, ShouldEqual, float64(3)) }) Convey("median with one values", func() { result := testReducer("median", 1) So(result, ShouldEqual, float64(1)) }) Convey("avg", func() { result := testReducer("avg", 1, 2, 3) So(result, ShouldEqual, float64(2)) }) Convey("avg with only nulls", func() { reducer := NewSimpleReducer("avg") series := &tsdb.TimeSeries{ Name: "test time serie", } series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) So(reducer.Reduce(series).Valid, ShouldEqual, false) }) Convey("avg of number values and null values should ignore nulls", func() { reducer := NewSimpleReducer("avg") series := &tsdb.TimeSeries{ Name: "test time serie", } series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1)) series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3)) series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4)) So(reducer.Reduce(series).Float64, ShouldEqual, float64(3)) }) }) }
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { if len(series.Points) == 0 { return null.FloatFromPtr(nil) } value := float64(0) allNull := true switch s.Type { case "avg": validPointsCount := 0 for _, point := range series.Points { if point[0].Valid { value += point[0].Float64 validPointsCount += 1 allNull = false } } if validPointsCount > 0 { value = value / float64(validPointsCount) } case "sum": for _, point := range series.Points { if point[0].Valid { value += point[0].Float64 allNull = false } } case "min": value = math.MaxFloat64 for _, point := range series.Points { if point[0].Valid { allNull = false if value > point[0].Float64 { value = point[0].Float64 } } } case "max": value = -math.MaxFloat64 for _, point := range series.Points { if point[0].Valid { allNull = false if value < point[0].Float64 { value = point[0].Float64 } } } case "count": value = float64(len(series.Points)) allNull = false case "last": points := series.Points for i := len(points) - 1; i >= 0; i-- { if points[i][0].Valid { value = points[i][0].Float64 allNull = false break } } case "median": var values []float64 for _, v := range series.Points { if v[0].Valid { allNull = false values = append(values, v[0].Float64) } } if len(values) >= 1 { sort.Float64s(values) length := len(values) if length%2 == 1 { value = values[(length-1)/2] } else { value = (values[(length/2)-1] + values[length/2]) / 2 } } } if allNull { return null.FloatFromPtr(nil) } return null.FloatFrom(value) }
func init() { ScenarioRegistry = make(map[string]*Scenario) logger := log.New("tsdb.testdata") logger.Debug("Initializing TestData Scenario") registerScenario(&Scenario{ Id: "random_walk", Name: "Random Walk", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { timeWalkerMs := context.TimeRange.GetFromAsMsEpoch() to := context.TimeRange.GetToAsMsEpoch() series := newSeriesForQuery(query) points := make(tsdb.TimeSeriesPoints, 0) walker := rand.Float64() * 100 for i := int64(0); i < 10000 && timeWalkerMs < to; i++ { points = append(points, tsdb.NewTimePoint(null.FloatFrom(walker), float64(timeWalkerMs))) walker += rand.Float64() - 0.5 timeWalkerMs += query.IntervalMs } series.Points = points queryRes := tsdb.NewQueryResult() queryRes.Series = append(queryRes.Series, series) return queryRes }, }) registerScenario(&Scenario{ Id: "no_data_points", Name: "No Data Points", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { return tsdb.NewQueryResult() }, }) registerScenario(&Scenario{ Id: "datapoints_outside_range", Name: "Datapoints Outside Range", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { queryRes := tsdb.NewQueryResult() series := newSeriesForQuery(query) outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000 series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(10), float64(outsideTime))) queryRes.Series = append(queryRes.Series, series) return queryRes }, }) registerScenario(&Scenario{ Id: "csv_metric_values", Name: "CSV Metric Values", StringInput: "1,20,90,30,5,0", Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { queryRes := tsdb.NewQueryResult() stringInput := query.Model.Get("stringInput").MustString() stringInput = strings.Replace(stringInput, " ", "", -1) values := []null.Float{} for _, strVal := range strings.Split(stringInput, ",") { if strVal == "null" { values = append(values, null.FloatFromPtr(nil)) } if val, err := strconv.ParseFloat(strVal, 64); err == nil { values = append(values, null.FloatFrom(val)) } } if len(values) == 0 { return queryRes } series := newSeriesForQuery(query) startTime := context.TimeRange.GetFromAsMsEpoch() endTime := context.TimeRange.GetToAsMsEpoch() step := (endTime - startTime) / int64(len(values)-1) for _, val := range values { series.Points = append(series.Points, tsdb.TimePoint{val, null.FloatFrom(float64(startTime))}) startTime += step } queryRes.Series = append(queryRes.Series, series) return queryRes }, }) }