Alerting: support alerting on data.Frame (that can be time series) (#22812)
data.Frame (that can be series) are converted to as tsdb.TimeSeriesSlice - so new backend plugins can be shimmed into existing alerting use sdk v0.31.0
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
package tsdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
// SeriesToFrame converts a TimeSeries to a sdk Frame
|
||||
@@ -35,3 +38,72 @@ func convertTSDBTimePoint(point TimePoint) (t *time.Time, f *float64) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FrameToSeriesSlice converts a frame that is a valid time series as per data.TimeSeriesSchema()
|
||||
// to a TimeSeriesSlice.
|
||||
func FrameToSeriesSlice(frame *data.Frame) (TimeSeriesSlice, error) {
|
||||
tsSchema := frame.TimeSeriesSchema()
|
||||
if tsSchema.Type == data.TimeSeriesTypeNot {
|
||||
return nil, fmt.Errorf("input frame is not recognized as a time series")
|
||||
}
|
||||
// If Long, make wide
|
||||
if tsSchema.Type == data.TimeSeriesTypeLong {
|
||||
var err error
|
||||
frame, err = data.LongToWide(frame)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrap("failed to convert long to wide series when converting from dataframe", err)
|
||||
}
|
||||
tsSchema = frame.TimeSeriesSchema()
|
||||
}
|
||||
|
||||
seriesCount := len(tsSchema.ValueIndices)
|
||||
seriesSlice := make(TimeSeriesSlice, 0, seriesCount)
|
||||
timeField := frame.Fields[tsSchema.TimeIndex]
|
||||
timeNullFloatSlice := make([]null.Float, timeField.Len())
|
||||
|
||||
for i := 0; i < timeField.Len(); i++ { // built slice of time as epoch ms in null floats
|
||||
tStamp, err := timeField.FloatAt(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeNullFloatSlice[i] = null.FloatFrom(tStamp)
|
||||
}
|
||||
|
||||
for _, fieldIdx := range tsSchema.ValueIndices { // create a TimeSeries for each value Field
|
||||
field := frame.Fields[fieldIdx]
|
||||
ts := &TimeSeries{
|
||||
Name: field.Name,
|
||||
Tags: field.Labels.Copy(),
|
||||
Points: make(TimeSeriesPoints, field.Len()),
|
||||
}
|
||||
|
||||
for rowIdx := 0; rowIdx < field.Len(); rowIdx++ { // for each value in the field, make a TimePoint
|
||||
val, err := field.FloatAt(rowIdx)
|
||||
if err != nil {
|
||||
return nil, errutil.Wrapf(err, "failed to convert frame to tsdb.series, can not convert value %v to float", field.At(rowIdx))
|
||||
}
|
||||
ts.Points[rowIdx] = TimePoint{
|
||||
null.FloatFrom(val),
|
||||
timeNullFloatSlice[rowIdx],
|
||||
}
|
||||
}
|
||||
|
||||
seriesSlice = append(seriesSlice, ts)
|
||||
}
|
||||
|
||||
return seriesSlice, nil
|
||||
|
||||
}
|
||||
|
||||
// FramesFromBytes returns a data.Frame slice from marshalled arrow dataframes.
|
||||
func FramesFromBytes(bFrames [][]byte) ([]*data.Frame, error) {
|
||||
frames := make([]*data.Frame, len(bFrames))
|
||||
for i, bFrame := range bFrames {
|
||||
var err error
|
||||
frames[i], err = data.UnmarshalArrow(bFrame)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package tsdb
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/xorcare/pointer"
|
||||
)
|
||||
|
||||
func TestFrameToSeriesSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
frame *data.Frame
|
||||
seriesSlice 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: TimeSeriesSlice{
|
||||
&TimeSeries{
|
||||
Name: "Values Int64s",
|
||||
Tags: map[string]string{"Animal Factor": "cat"},
|
||||
Points: TimeSeriesPoints{
|
||||
TimePoint{null.FloatFrom(math.NaN()), null.FloatFrom(1577934240000)},
|
||||
TimePoint{null.FloatFrom(3), null.FloatFrom(1577934270000)},
|
||||
},
|
||||
},
|
||||
&TimeSeries{
|
||||
Name: "Values Floats",
|
||||
Tags: map[string]string{"Animal Factor": "sloth"},
|
||||
Points: TimeSeriesPoints{
|
||||
TimePoint{null.FloatFrom(2), null.FloatFrom(1577934240000)},
|
||||
TimePoint{null.FloatFrom(4), null.FloatFrom(1577934270000)},
|
||||
},
|
||||
},
|
||||
},
|
||||
Err: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "a long 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, 0, 0, time.UTC),
|
||||
time.Date(2020, 1, 2, 3, 4, 30, 0, time.UTC),
|
||||
time.Date(2020, 1, 2, 3, 4, 30, 0, time.UTC),
|
||||
}),
|
||||
data.NewField("Values Floats", nil, []float64{
|
||||
1.0,
|
||||
2.0,
|
||||
3.0,
|
||||
4.0,
|
||||
}),
|
||||
data.NewField("Values Int64", nil, []int64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
}),
|
||||
data.NewField("Animal Factor", nil, []string{
|
||||
"cat",
|
||||
"sloth",
|
||||
"cat",
|
||||
"sloth",
|
||||
}),
|
||||
data.NewField("Location", nil, []string{
|
||||
"Florida",
|
||||
"Central & South America",
|
||||
"Florida",
|
||||
"Central & South America",
|
||||
})),
|
||||
|
||||
seriesSlice: TimeSeriesSlice{
|
||||
&TimeSeries{
|
||||
Name: "Values Floats",
|
||||
Tags: map[string]string{"Animal Factor": "cat", "Location": "Florida"},
|
||||
Points: TimeSeriesPoints{
|
||||
TimePoint{null.FloatFrom(1), null.FloatFrom(1577934240000)},
|
||||
TimePoint{null.FloatFrom(3), null.FloatFrom(1577934270000)},
|
||||
},
|
||||
},
|
||||
&TimeSeries{
|
||||
Name: "Values Int64",
|
||||
Tags: map[string]string{"Animal Factor": "cat", "Location": "Florida"},
|
||||
Points: TimeSeriesPoints{
|
||||
TimePoint{null.FloatFrom(1), null.FloatFrom(1577934240000)},
|
||||
TimePoint{null.FloatFrom(3), null.FloatFrom(1577934270000)},
|
||||
},
|
||||
},
|
||||
&TimeSeries{
|
||||
Name: "Values Floats",
|
||||
Tags: map[string]string{"Animal Factor": "sloth", "Location": "Central & South America"},
|
||||
Points: TimeSeriesPoints{
|
||||
TimePoint{null.FloatFrom(2), null.FloatFrom(1577934240000)},
|
||||
TimePoint{null.FloatFrom(4), null.FloatFrom(1577934270000)},
|
||||
},
|
||||
},
|
||||
&TimeSeries{
|
||||
Name: "Values Int64",
|
||||
Tags: map[string]string{"Animal Factor": "sloth", "Location": "Central & South America"},
|
||||
Points: TimeSeriesPoints{
|
||||
TimePoint{null.FloatFrom(2), null.FloatFrom(1577934240000)},
|
||||
TimePoint{null.FloatFrom(4), null.FloatFrom(1577934270000)},
|
||||
},
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user