Skip to content

Commit 655348a

Browse files
authored
Add a multi timeseries return format (#106)
1 parent 41d1be9 commit 655348a

File tree

4 files changed

+121
-25
lines changed

4 files changed

+121
-25
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.21.1
77
require (
88
github.com/go-sql-driver/mysql v1.4.0
99
github.com/google/go-cmp v0.5.9
10+
github.com/grafana/dataplane/sdata v0.0.7
1011
github.com/grafana/grafana-plugin-sdk-go v0.188.3
1112
github.com/mithrandie/csvq-driver v1.6.8
1213
github.com/stretchr/testify v1.8.4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1
9898
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
9999
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
100100
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
101+
github.com/grafana/dataplane/sdata v0.0.7 h1:CImITypIyS1jxijCR6xqKx71JnYAxcwpH9ChK0gH164=
102+
github.com/grafana/dataplane/sdata v0.0.7/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
101103
github.com/grafana/grafana-plugin-sdk-go v0.188.3 h1:91wrmnS6zXs4FhriVesZujkmjrwY1xhsusL30CtLkEE=
102104
github.com/grafana/grafana-plugin-sdk-go v0.188.3/go.mod h1:nctofuR6fyhx3Stbnh5ha6setroeqqBgO0Rj9s4t86o=
103105
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=

query.go

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"net/http"
1010
"time"
1111

12+
"github.com/grafana/dataplane/sdata/timeseries"
13+
1214
"github.com/grafana/grafana-plugin-sdk-go/backend"
1315
"github.com/grafana/grafana-plugin-sdk-go/data"
1416
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
@@ -18,14 +20,16 @@ import (
1820
type FormatQueryOption uint32
1921

2022
const (
21-
// FormatOptionTimeSeries formats the query results as a timeseries using "WideToLong"
23+
// FormatOptionTimeSeries formats the query results as a timeseries using "LongToWide"
2224
FormatOptionTimeSeries FormatQueryOption = iota
23-
// FormatOptionTable formats the query results as a table using "LongToWide"
25+
// FormatOptionTable sets the preferred visualization to table
2426
FormatOptionTable
2527
// FormatOptionLogs sets the preferred visualization to logs
2628
FormatOptionLogs
2729
// FormatOptionsTrace sets the preferred visualization to trace
2830
FormatOptionTrace
31+
// FormatOptionMulti formats the query results as a timeseries using "LongToMulti"
32+
FormatOptionMulti
2933
)
3034

3135
// Query is the model that represents the query that users submit from the panel / queryeditor.
@@ -155,43 +159,84 @@ func getFrames(rows *sql.Rows, limit int64, converters []sqlutil.Converter, fill
155159
frame.Meta = &data.FrameMeta{}
156160
}
157161

162+
count, err := frame.RowLen()
163+
if err != nil {
164+
return nil, err
165+
}
166+
if count == 0 {
167+
return nil, ErrorNoResults
168+
}
169+
158170
frame.Meta.ExecutedQueryString = query.RawSQL
159171
frame.Meta.PreferredVisualization = data.VisTypeGraph
160172

161-
if query.Format == FormatOptionTable {
162-
frame.Meta.PreferredVisualization = data.VisTypeTable
163-
return data.Frames{frame}, nil
164-
}
173+
switch query.Format {
174+
case FormatOptionMulti:
175+
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeLong {
165176

166-
if query.Format == FormatOptionLogs {
167-
frame.Meta.PreferredVisualization = data.VisTypeLogs
168-
return data.Frames{frame}, nil
169-
}
177+
err = fixFrameForLongToMulti(frame)
178+
if err != nil {
179+
return nil, err
180+
}
170181

171-
if query.Format == FormatOptionTrace {
182+
frames, err := timeseries.LongToMulti(&timeseries.LongFrame{frame})
183+
if err != nil {
184+
return nil, err
185+
}
186+
return frames.Frames(), nil
187+
}
188+
case FormatOptionTable:
189+
frame.Meta.PreferredVisualization = data.VisTypeTable
190+
case FormatOptionLogs:
191+
frame.Meta.PreferredVisualization = data.VisTypeLogs
192+
case FormatOptionTrace:
172193
frame.Meta.PreferredVisualization = data.VisTypeTrace
173-
return data.Frames{frame}, nil
194+
// Format as timeSeries
195+
default:
196+
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeLong {
197+
frame, err = data.LongToWide(frame, fillMode)
198+
if err != nil {
199+
return nil, err
200+
}
201+
}
174202
}
203+
return data.Frames{frame}, nil
204+
}
175205

176-
count, err := frame.RowLen()
177-
178-
if err != nil {
179-
return nil, err
206+
// fixFrameForLongToMulti edits the passed in frame so that it's first time field isn't nullable and has the correct meta
207+
func fixFrameForLongToMulti(frame *data.Frame) error {
208+
if frame == nil {
209+
return fmt.Errorf("can not convert to wide series, input is nil")
180210
}
181211

182-
if count == 0 {
183-
return nil, ErrorNoResults
212+
timeFields := frame.TypeIndices(data.FieldTypeTime, data.FieldTypeNullableTime)
213+
if len(timeFields) == 0 {
214+
return fmt.Errorf("can not convert to wide series, input is missing a time field")
184215
}
185216

186-
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeLong {
187-
frame, err := data.LongToWide(frame, fillMode)
188-
if err != nil {
189-
return nil, err
217+
// the timeseries package expects the first time field in the frame to be non-nullable and ignores the rest
218+
timeField := frame.Fields[timeFields[0]]
219+
if timeField.Type() == data.FieldTypeNullableTime {
220+
newValues := []time.Time{}
221+
for i := 0; i < timeField.Len(); i++ {
222+
val, ok := timeField.ConcreteAt(i)
223+
if !ok {
224+
return fmt.Errorf("can not convert to wide series, input has null time values")
225+
}
226+
newValues = append(newValues, val.(time.Time))
190227
}
191-
return data.Frames{frame}, nil
192-
}
228+
newField := data.NewField(timeField.Name, timeField.Labels, newValues)
229+
newField.Config = timeField.Config
230+
frame.Fields[timeFields[0]] = newField
193231

194-
return data.Frames{frame}, nil
232+
// LongToMulti requires the meta to be set for the frame
233+
if frame.Meta == nil {
234+
frame.Meta = &data.FrameMeta{}
235+
}
236+
frame.Meta.Type = data.FrameTypeTimeSeriesLong
237+
frame.Meta.TypeVersion = data.FrameTypeVersion{0, 1}
238+
}
239+
return nil
195240
}
196241

197242
func applyHeaders(query *Query, headers http.Header) *Query {

query_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
"fmt"
78
"testing"
89
"time"
910

11+
"github.com/grafana/grafana-plugin-sdk-go/data"
1012
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
13+
"github.com/stretchr/testify/require"
1114
)
1215

1316
var (
@@ -104,3 +107,48 @@ func TestQuery_Timeout(t *testing.T) {
104107
}
105108
})
106109
}
110+
111+
func TestFixFrameForLongToMulti(t *testing.T) {
112+
t.Run("fix time", func(t *testing.T) {
113+
time1 := time.UnixMilli(1)
114+
time2 := time.UnixMilli(2)
115+
frame := data.NewFrame("",
116+
data.NewField("time", nil, []*time.Time{&time1, &time2}),
117+
data.NewField("host", nil, []string{"a", "b"}),
118+
data.NewField("iface", nil, []string{"eth0", "eth0"}),
119+
data.NewField("in_bytes", nil, []float64{1, 2}),
120+
data.NewField("out_bytes", nil, []int64{3, 4}),
121+
)
122+
123+
err := fixFrameForLongToMulti(frame)
124+
require.NoError(t, err)
125+
126+
require.Equal(t, frame.Fields[0].Type(), data.FieldTypeTime)
127+
require.Equal(t, frame.Fields[0].Len(), 2)
128+
require.Equal(t, frame.Fields[0].At(0).(time.Time), time1)
129+
require.Equal(t, frame.Fields[0].At(1).(time.Time), time2)
130+
131+
require.Equal(t, frame.Meta.Type, data.FrameTypeTimeSeriesLong)
132+
require.Equal(t, frame.Meta.TypeVersion, data.FrameTypeVersion{0, 1})
133+
})
134+
t.Run("errors for null time", func(t *testing.T) {
135+
time1 := time.UnixMilli(1)
136+
frame := data.NewFrame("",
137+
data.NewField("time", nil, []*time.Time{&time1, nil}),
138+
data.NewField("host", nil, []string{"a", "b"}),
139+
data.NewField("in_bytes", nil, []float64{1, 2}),
140+
)
141+
142+
err := fixFrameForLongToMulti(frame)
143+
require.Equal(t, err, fmt.Errorf("can not convert to wide series, input has null time values"))
144+
})
145+
t.Run("error for no time", func(t *testing.T) {
146+
frame := data.NewFrame("",
147+
data.NewField("host", nil, []string{"a", "b"}),
148+
data.NewField("in_bytes", nil, []float64{1, 2}),
149+
)
150+
151+
err := fixFrameForLongToMulti(frame)
152+
require.Equal(t, err, fmt.Errorf("can not convert to wide series, input is missing a time field"))
153+
})
154+
}

0 commit comments

Comments
 (0)