The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/backtesting/eval_data_test.go

293 lines
9.5 KiB

package backtesting
import (
"context"
"errors"
"fmt"
"math/rand"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
func GenerateWideSeriesFrame(size int, resolution time.Duration) *data.Frame {
fields := make(data.Fields, 0, rand.Intn(4)+2)
fields = append(fields, data.NewField("time", nil, make([]time.Time, size)))
for i := 1; i < cap(fields); i++ {
name := fmt.Sprintf("values-%d", i)
fields = append(fields, data.NewField(name, models.GenerateAlertLabels(rand.Intn(4)+1, name), make([]int64, size)))
}
frame := data.NewFrame("test", fields...)
tmili := time.Now().UnixMilli()
tmili = tmili - tmili%resolution.Milliseconds()
current := time.UnixMilli(tmili).Add(-resolution * time.Duration(size))
for i := 0; i < size; i++ {
vals := make([]any, 0, len(frame.Fields))
vals = append(vals, current)
for i := 1; i < cap(vals); i++ {
vals = append(vals, rand.Int63n(2)-1) // random value [-1,1]
}
frame.SetRow(i, vals...)
current = current.Add(resolution)
}
return frame
}
func TestDataEvaluator_New(t *testing.T) {
t.Run("should fail if frame is not TimeSeriesTypeWide", func(t *testing.T) {
t.Run("but TimeSeriesTypeNot", func(t *testing.T) {
frameTimeSeriesTypeNot := data.NewFrame("test")
require.Equal(t, data.TimeSeriesTypeNot, frameTimeSeriesTypeNot.TimeSeriesSchema().Type)
_, err := newDataEvaluator(util.GenerateShortUID(), frameTimeSeriesTypeNot)
require.Error(t, err)
})
t.Run("but TimeSeriesTypeLong", func(t *testing.T) {
frameTimeSeriesTypeLong := data.NewFrame("test", data.NewField("time", nil, make([]time.Time, 0)), data.NewField("data", nil, make([]string, 0)), data.NewField("value", nil, make([]int64, 0)))
require.Equal(t, data.TimeSeriesTypeLong, frameTimeSeriesTypeLong.TimeSeriesSchema().Type)
_, err := newDataEvaluator(util.GenerateShortUID(), frameTimeSeriesTypeLong)
require.Error(t, err)
})
})
t.Run("should convert fame to series and sort it", func(t *testing.T) {
refID := util.GenerateShortUID()
frameSize := rand.Intn(100) + 100
frame := GenerateWideSeriesFrame(frameSize, time.Second)
rand.Shuffle(frameSize, func(i, j int) {
rowi := frame.RowCopy(i)
rowj := frame.RowCopy(j)
frame.SetRow(i, rowj...)
frame.SetRow(j, rowi...)
})
e, err := newDataEvaluator(refID, frame)
require.NoError(t, err)
require.Equal(t, refID, e.refID)
require.Len(t, e.data, len(frame.Fields)-1) // timestamp is not counting
for idx, series := range e.data {
assert.Equalf(t, series.Len(), frameSize, "Length of the series %d is %d but expected to be %d", idx, series.Len(), frameSize)
assert.Equalf(t, frame.Fields[idx+1].Labels, series.GetLabels(), "Labels of series %d does not match with original field labels", idx)
assert.Lessf(t, series.GetTime(0), series.GetTime(1), "Series %d is expected to be sorted in ascending order", idx)
}
})
}
func TestDataEvaluator_Eval(t *testing.T) {
type results struct {
time time.Time
results eval.Results
}
refID := util.GenerateShortUID()
frameSize := rand.Intn(100) + 100
frame := GenerateWideSeriesFrame(frameSize, time.Second)
from := frame.At(0, 0).(time.Time)
to := frame.At(0, frame.Rows()-1).(time.Time)
evaluator, err := newDataEvaluator(refID, frame)
require.NoErrorf(t, err, "Frame %v", frame)
t.Run("should use data points when frame resolution matches evaluation interval", func(t *testing.T) {
r := make([]results, 0, frame.Rows())
interval := time.Second
resultsCount := int(to.Sub(from).Seconds() / interval.Seconds())
err = evaluator.Eval(context.Background(), from, time.Second, resultsCount, func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
require.NoError(t, err)
require.Len(t, r, resultsCount)
t.Run("results should be in the same refID", func(t *testing.T) {
for _, res := range r {
for _, result := range res.results {
require.Contains(t, result.Values, refID)
}
}
})
t.Run("should be Alerting if value is not 0", func(t *testing.T) {
for _, res := range r {
for _, result := range res.results {
v := result.Values[refID].Value
require.NotNil(t, v)
if *v == 0 {
require.Equalf(t, eval.Normal, result.State, "Result value is %d", *v)
} else {
require.Equalf(t, eval.Alerting, result.State, "Result value is %d", *v)
}
}
}
})
t.Run("results should be in ascending order", func(t *testing.T) {
var prev = results{}
for i := 0; i < len(r); i++ {
current := r[i]
if i > 0 {
require.Less(t, prev.time, current.time)
} else {
require.Equal(t, from, current.time)
}
prev = current
}
})
t.Run("results should be in the same order as fields in frame", func(t *testing.T) {
for i := 0; i < len(r); i++ {
current := r[i]
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(i)
require.NoError(t, err)
require.EqualValues(t, expected, *result.Values[refID].Value)
}
}
})
})
t.Run("when frame resolution does not match evaluation interval", func(t *testing.T) {
t.Run("should closest timestamp if interval is smaller than frame resolution", func(t *testing.T) {
interval := 300 * time.Millisecond
size := to.Sub(from).Milliseconds() / interval.Milliseconds()
r := make([]results, 0, size)
err = evaluator.Eval(context.Background(), from, interval, int(size), func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
currentRowIdx := 0
nextTime := frame.At(0, currentRowIdx+1).(time.Time)
for id, current := range r {
if !current.time.Before(nextTime) {
currentRowIdx++
if frame.Rows() > currentRowIdx+1 {
nextTime = frame.At(0, currentRowIdx+1).(time.Time)
}
}
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(currentRowIdx)
require.NoError(t, err)
require.EqualValuesf(t, expected, *result.Values[refID].Value, "Time %d", id)
}
}
})
t.Run("should downscale series if interval is smaller using previous value", func(t *testing.T) {
interval := 5 * time.Second
size := int(to.Sub(from).Seconds() / interval.Seconds())
r := make([]results, 0, size)
err = evaluator.Eval(context.Background(), from, interval, size, func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
currentRowIdx := 0
var frameDate time.Time
for resultNum, current := range r {
for i := currentRowIdx; i < frame.Rows(); i++ {
d := frame.At(0, i).(time.Time)
if d.Equal(current.time) {
currentRowIdx = i
frameDate = d
break
}
if d.After(current.time) {
require.Fail(t, "Interval is not aligned")
}
}
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(currentRowIdx)
require.NoError(t, err)
require.EqualValuesf(t, expected, *result.Values[refID].Value, "Current time [%v] frame time [%v]. Result #%d", current.time, frameDate, resultNum)
}
}
})
})
t.Run("when eval interval is larger than data", func(t *testing.T) {
t.Run("should be noData until the frame interval", func(t *testing.T) {
newFrom := from.Add(-10 * time.Second)
r := make([]results, 0, int(to.Sub(newFrom).Seconds()))
err = evaluator.Eval(context.Background(), newFrom, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
rowIdx := 0
for _, current := range r {
if current.time.Before(from) {
require.Len(t, current.results, 1)
require.Equal(t, eval.NoData, current.results[0].State)
} else {
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(rowIdx)
require.NoError(t, err)
require.EqualValues(t, expected, *result.Values[refID].Value)
}
rowIdx++
}
}
})
t.Run("should be the last value after the frame interval", func(t *testing.T) {
newTo := to.Add(10 * time.Second)
r := make([]results, 0, int(newTo.Sub(from).Seconds()))
err = evaluator.Eval(context.Background(), from, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
rowIdx := 0
for _, current := range r {
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(rowIdx)
require.NoError(t, err)
require.EqualValues(t, expected, *result.Values[refID].Value)
}
if current.time.Before(to) {
rowIdx++
}
}
})
})
t.Run("should stop if callback error", func(t *testing.T) {
expectedError := errors.New("error")
err = evaluator.Eval(context.Background(), from, time.Second, 6, func(idx int, now time.Time, res eval.Results) error {
if idx == 5 {
return expectedError
}
return nil
})
require.ErrorIs(t, err, expectedError)
})
}