Alerting: Add labels to name when converting data frame to series (#28085)

* also move conversion func to alerting package since it is not used in other places

Fixes #28068
This commit is contained in:
Kyle Brandt
2020-10-09 08:21:16 -04:00
committed by GitHub
parent c9a5d1ad4b
commit e16637793d
4 changed files with 147 additions and 148 deletions

View File

@@ -7,6 +7,7 @@ import (
gocontext "context"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -179,7 +180,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
}
for _, frame := range frames {
ss, err := tsdb.FrameToSeriesSlice(frame)
ss, err := FrameToSeriesSlice(frame)
if err != nil {
return nil, errutil.Wrapf(err, `tsdb.HandleRequest() failed to convert dataframe "%v" to tsdb.TimeSeriesSlice`, frame.Name)
}
@@ -294,3 +295,67 @@ func validateToValue(to string) error {
_, err := time.ParseDuration(to)
return err
}
// FrameToSeriesSlice converts a frame that is a valid time series as per data.TimeSeriesSchema()
// to a TimeSeriesSlice.
func FrameToSeriesSlice(frame *data.Frame) (tsdb.TimeSeriesSlice, error) {
tsSchema := frame.TimeSeriesSchema()
if tsSchema.Type == data.TimeSeriesTypeNot {
// If no fields, or only a time field, create an empty tsdb.TimeSeriesSlice with a single
// time series in order to trigger "no data" in alerting.
if len(frame.Fields) == 0 || (len(frame.Fields) == 1 && frame.Fields[0].Type().Time()) {
return tsdb.TimeSeriesSlice{{
Name: frame.Name,
Points: make(tsdb.TimeSeriesPoints, 0),
}}, nil
}
return nil, fmt.Errorf("input frame is not recognized as a time series")
}
seriesCount := len(tsSchema.ValueIndices)
seriesSlice := make(tsdb.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 := &tsdb.TimeSeries{
Points: make(tsdb.TimeSeriesPoints, field.Len()),
}
switch {
case field.Config != nil && field.Config.DisplayName != "":
ts.Name = field.Config.DisplayName
case field.Labels != nil:
ts.Tags = field.Labels.Copy()
// Tags are appended to the name so they are eventually included in EvalMatch's Metric property
// for display in notifications.
ts.Name = fmt.Sprintf("%v {%v}", field.Name, field.Labels.String())
default:
ts.Name = field.Name
}
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] = tsdb.TimePoint{
null.FloatFrom(val),
timeNullFloatSlice[rowIdx],
}
}
seriesSlice = append(seriesSlice, ts)
}
return seriesSlice, nil
}

View File

@@ -2,9 +2,12 @@ package conditions
import (
"context"
"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/bus"
"github.com/grafana/grafana/pkg/components/null"
@@ -13,6 +16,8 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
"github.com/xorcare/pointer"
)
func TestQueryCondition(t *testing.T) {
@@ -226,3 +231,79 @@ func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
fn(ctx)
})
}
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)
}
})
}
}

View File

@@ -1,12 +1,9 @@
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
@@ -38,57 +35,3 @@ 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 {
// If no fields, or only a time field, create an empty TimeSeriesSlice with a single
// time series in order to trigger "no data" in alerting.
if len(frame.Fields) == 0 || (len(frame.Fields) == 1 && frame.Fields[0].Type().Time()) {
return TimeSeriesSlice{{
Name: frame.Name,
Points: make(TimeSeriesPoints, 0),
}}, nil
}
return nil, fmt.Errorf("input frame is not recognized as a time series")
}
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
}

View File

@@ -1,90 +0,0 @@
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: "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: TimeSeriesSlice{
&TimeSeries{
Name: "Values Int64s",
Tags: map[string]string{"Animal Factor": "cat"},
Points: TimeSeriesPoints{},
},
&TimeSeries{
Name: "Values Floats",
Tags: map[string]string{"Animal Factor": "sloth"},
Points: 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)
}
})
}
}