From e16637793d1c116b86e975a4793356b2f83c8fd5 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 9 Oct 2020 08:21:16 -0400 Subject: [PATCH] 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 --- pkg/services/alerting/conditions/query.go | 67 +++++++++++++- .../alerting/conditions/query_test.go | 81 +++++++++++++++++ pkg/tsdb/frame_util.go | 57 ------------ pkg/tsdb/frame_util_test.go | 90 ------------------- 4 files changed, 147 insertions(+), 148 deletions(-) delete mode 100644 pkg/tsdb/frame_util_test.go diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 155d4eb6d93..9a24246f5ef 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -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 +} diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 9decc1d4e05..54765e7328d 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -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) + } + }) + } +} diff --git a/pkg/tsdb/frame_util.go b/pkg/tsdb/frame_util.go index 6455e2ff218..f88ad39e583 100644 --- a/pkg/tsdb/frame_util.go +++ b/pkg/tsdb/frame_util.go @@ -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 -} diff --git a/pkg/tsdb/frame_util_test.go b/pkg/tsdb/frame_util_test.go deleted file mode 100644 index d737194e62b..00000000000 --- a/pkg/tsdb/frame_util_test.go +++ /dev/null @@ -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) - } - }) - } -}