Skip to content

Commit 3f66a49

Browse files
marefrkylebrandtaknuds1
authored
Adds new query type field and query type multiplexer (mux) (#152)
Adds new field, QueryType, to QueryDataRequest. Adds new QueryTypeMux to be able to register a handler per query type. This should ease the handling when supporting queries of different types. Closes #75 Co-Authored-By: Kyle Brandt <[email protected]> Co-authored-by: Arve Knudsen <[email protected]>
1 parent b29f29b commit 3f66a49

11 files changed

+337
-78
lines changed

Diff for: backend/convert_from_protobuf.go

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func (f ConvertFromProtobuf) TimeRange(proto *pluginv2.TimeRange) TimeRange {
9191
func (f ConvertFromProtobuf) DataQuery(proto *pluginv2.DataQuery) *DataQuery {
9292
return &DataQuery{
9393
RefID: proto.RefId,
94+
QueryType: proto.QueryType,
9495
MaxDataPoints: proto.MaxDataPoints,
9596
TimeRange: f.TimeRange(proto.TimeRange),
9697
Interval: time.Duration(proto.IntervalMS) * time.Millisecond,

Diff for: backend/convert_from_protobuf_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"testing"
88
"time"
99

10-
"github.com/davecgh/go-spew/spew"
1110
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
1211
"github.com/mitchellh/reflectwalk"
1312
"github.com/stretchr/testify/require"
@@ -30,7 +29,6 @@ func (w *walker) StructField(f reflect.StructField, v reflect.Value) error {
3029
if f.PkgPath != "" {
3130
return nil
3231
}
33-
spew.Dump(f)
3432
w.FieldCount++
3533
if v.IsZero() {
3634
w.ZeroValueFieldCount++
@@ -304,6 +302,7 @@ var protoDataQuery = &pluginv2.DataQuery{
304302
TimeRange: protoTimeRange,
305303
IntervalMS: 60 * 1000,
306304
Json: []byte(`{ "query": "SELECT * from FUN"`),
305+
QueryType: "qt",
307306
}
308307

309308
func TestConvertFromProtobufDataQuery(t *testing.T) {
@@ -334,6 +333,8 @@ func TestConvertFromProtobufDataQuery(t *testing.T) {
334333

335334
requireCounter.Equal(t, protoDQ.RefId, sdkDQ.RefID)
336335
requireCounter.Equal(t, protoDQ.MaxDataPoints, sdkDQ.MaxDataPoints)
336+
requireCounter.Equal(t, protoDQ.QueryType, sdkDQ.QueryType)
337+
337338
requireCounter.Equal(t, time.Duration(time.Minute), sdkDQ.Interval)
338339
requireCounter.Equal(t, sdkTimeRange.From, sdkDQ.TimeRange.From)
339340
requireCounter.Equal(t, sdkTimeRange.To, sdkDQ.TimeRange.To)
@@ -406,6 +407,7 @@ func TestConvertFromProtobufQueryDataRequest(t *testing.T) {
406407
// Queries
407408
requireCounter.Equal(t, protoQDR.Queries[0].RefId, sdkQDR.Queries[0].RefID)
408409
requireCounter.Equal(t, protoQDR.Queries[0].MaxDataPoints, sdkQDR.Queries[0].MaxDataPoints)
410+
requireCounter.Equal(t, protoQDR.Queries[0].QueryType, sdkQDR.Queries[0].QueryType)
409411
requireCounter.Equal(t, time.Duration(time.Minute), sdkQDR.Queries[0].Interval)
410412
requireCounter.Equal(t, sdkTimeRange.From, sdkQDR.Queries[0].TimeRange.From)
411413
requireCounter.Equal(t, sdkTimeRange.To, sdkQDR.Queries[0].TimeRange.To)

Diff for: backend/convert_to_protobuf.go

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func (t ConvertToProtobuf) CheckHealthResponse(res *CheckHealthResult) *pluginv2
111111
func (t ConvertToProtobuf) DataQuery(q DataQuery) *pluginv2.DataQuery {
112112
return &pluginv2.DataQuery{
113113
RefId: q.RefID,
114+
QueryType: q.QueryType,
114115
MaxDataPoints: q.MaxDataPoints,
115116
IntervalMS: q.Interval.Milliseconds(),
116117
TimeRange: t.TimeRange(q.TimeRange),

Diff for: backend/data.go

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ type DataQuery struct {
3535
// RefID is the unique identifer of the query, set by the frontend call.
3636
RefID string
3737

38+
// QueryType is an optional identifier for the type of query.
39+
// It can be used to distinguish different types of queries.
40+
QueryType string
41+
3842
// MaxDataPoints is the maximum number of datapoints that should be returned from a time series query.
3943
MaxDataPoints int64
4044

Diff for: backend/datasource/doc.go

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package datasource provides utilities for data source plugins.
2+
package datasource

Diff for: backend/datasource/query_type_mux.go

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package datasource
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
8+
9+
"github.com/grafana/grafana-plugin-sdk-go/backend"
10+
)
11+
12+
// QueryTypeHandlerFunc is an adapter to allow the use of
13+
// ordinary functions as backend.QueryDataHandler. If f is a function
14+
// with the appropriate signature, QueryTypeHandlerFunc(f) is a
15+
// Handler that calls f.
16+
type QueryTypeHandlerFunc func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
17+
18+
// QueryData calls f(ctx, req).
19+
func (fn QueryTypeHandlerFunc) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
20+
return fn(ctx, req)
21+
}
22+
23+
// QueryTypeMux is a query type multiplexer.
24+
type QueryTypeMux struct {
25+
m map[string]backend.QueryDataHandler
26+
fallbackHandler backend.QueryDataHandler
27+
}
28+
29+
// NewQueryTypeMux allocates and returns a new QueryTypeMux.
30+
func NewQueryTypeMux() *QueryTypeMux {
31+
return new(QueryTypeMux)
32+
}
33+
34+
// Handle registers the handler for the given query type.
35+
//
36+
// Providing an empty queryType registers the handler as a fallback handler
37+
// that will be called when query type doesn't match any registered handlers.
38+
// If handler is nil, Handle panics.
39+
// If a handler already exists for queryType, Handle panics.
40+
func (mux *QueryTypeMux) Handle(queryType string, handler backend.QueryDataHandler) {
41+
if handler == nil {
42+
panic("datasource: nil handler")
43+
}
44+
45+
if mux.m == nil {
46+
mux.m = map[string]backend.QueryDataHandler{}
47+
mux.fallbackHandler = QueryTypeHandlerFunc(fallbackHandler)
48+
}
49+
50+
if _, exist := mux.m[queryType]; exist {
51+
panic("datasource: multiple registrations for " + queryType)
52+
}
53+
54+
if queryType == "" {
55+
log.DefaultLogger.Debug("datasource: registering query type fallback handler")
56+
mux.fallbackHandler = handler
57+
return
58+
}
59+
60+
log.DefaultLogger.Debug("datasource: registering query type handler", "queryType", queryType)
61+
mux.m[queryType] = handler
62+
}
63+
64+
// HandleFunc registers the handler function for the given query type.
65+
//
66+
// Providing an empty queryType registers the handler as a fallback handler
67+
// that will be called when query type doesn't match any registered handlers.
68+
// If handler is nil, Handle panics.
69+
// If a handler already exists for queryType, Handle panics.
70+
func (mux *QueryTypeMux) HandleFunc(queryType string, handler func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)) {
71+
mux.Handle(queryType, QueryTypeHandlerFunc(handler))
72+
}
73+
74+
// QueryData dispatches the request to the handler(s) whose
75+
// query type matches the request queries query type.
76+
func (mux *QueryTypeMux) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
77+
type requestHandler struct {
78+
handler backend.QueryDataHandler
79+
request *backend.QueryDataRequest
80+
}
81+
82+
requests := map[string]requestHandler{}
83+
for _, q := range req.Queries {
84+
qt, handler := mux.getHandler(q.QueryType)
85+
if _, exists := requests[qt]; !exists {
86+
requests[qt] = requestHandler{
87+
handler: handler,
88+
request: &backend.QueryDataRequest{
89+
PluginContext: req.PluginContext,
90+
Headers: req.Headers,
91+
Queries: []backend.DataQuery{},
92+
},
93+
}
94+
}
95+
requests[qt].request.Queries = append(requests[qt].request.Queries, q)
96+
}
97+
98+
responses := backend.Responses{}
99+
for _, rh := range requests {
100+
qtResponse, err := rh.handler.QueryData(ctx, rh.request)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
for k, v := range qtResponse.Responses {
106+
responses[k] = v
107+
}
108+
}
109+
110+
return &backend.QueryDataResponse{
111+
Responses: responses,
112+
}, nil
113+
}
114+
115+
func (mux *QueryTypeMux) getHandler(queryType string) (string, backend.QueryDataHandler) {
116+
handler, exists := mux.m[queryType]
117+
if !exists {
118+
return "", mux.fallbackHandler
119+
}
120+
121+
return queryType, handler
122+
}
123+
124+
func fallbackHandler(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
125+
responses := backend.Responses{}
126+
for _, q := range req.Queries {
127+
responses[q.RefID] = backend.DataResponse{
128+
Error: fmt.Errorf("no handler found for query type '%s'", q.QueryType),
129+
}
130+
}
131+
132+
return &backend.QueryDataResponse{
133+
Responses: responses,
134+
}, nil
135+
}

Diff for: backend/datasource/query_type_mux_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package datasource
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/grafana/grafana-plugin-sdk-go/backend"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func ExampleQueryTypeMux() {
13+
mux := NewQueryTypeMux()
14+
mux.HandleFunc("queryTypeA", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
15+
// handle queryTypeA
16+
return nil, nil
17+
})
18+
mux.HandleFunc("queryTypeB", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
19+
// handle queryTypeB
20+
return nil, nil
21+
})
22+
23+
_ = backend.ServeOpts{
24+
QueryDataHandler: mux,
25+
}
26+
}
27+
28+
func TestQueryTypeMux(t *testing.T) {
29+
mux := NewQueryTypeMux()
30+
aHandler := &testHandler{}
31+
mux.Handle("a", aHandler)
32+
bHandler := &testHandler{}
33+
mux.Handle("b", bHandler)
34+
35+
res, err := mux.QueryData(context.Background(), &backend.QueryDataRequest{
36+
Queries: []backend.DataQuery{
37+
backend.DataQuery{
38+
RefID: "A",
39+
QueryType: "a",
40+
},
41+
backend.DataQuery{
42+
RefID: "B",
43+
QueryType: "b",
44+
},
45+
backend.DataQuery{
46+
RefID: "C",
47+
QueryType: "a",
48+
},
49+
backend.DataQuery{
50+
RefID: "D",
51+
QueryType: "d",
52+
},
53+
},
54+
})
55+
56+
require.NoError(t, err)
57+
require.Equal(t, 1, aHandler.callCount)
58+
require.Len(t, aHandler.request.Queries, 2)
59+
require.Equal(t, "A", aHandler.request.Queries[0].RefID)
60+
require.Equal(t, "C", aHandler.request.Queries[1].RefID)
61+
62+
require.Equal(t, 1, bHandler.callCount)
63+
require.Len(t, bHandler.request.Queries, 1)
64+
require.Equal(t, "B", bHandler.request.Queries[0].RefID)
65+
require.Len(t, res.Responses, 4)
66+
require.Equal(t, "no handler found for query type 'd'", res.Responses["D"].Error.Error())
67+
68+
t.Run("When overriding fallback handler should call fallback handler", func(t *testing.T) {
69+
errBoom := errors.New("BOOM")
70+
mux.HandleFunc("", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
71+
return nil, errBoom
72+
})
73+
res, err = mux.QueryData(context.Background(), &backend.QueryDataRequest{
74+
Queries: []backend.DataQuery{
75+
backend.DataQuery{
76+
RefID: "A",
77+
QueryType: "unhandled",
78+
},
79+
},
80+
})
81+
82+
require.Nil(t, res)
83+
require.Error(t, err)
84+
require.Equal(t, errBoom, err)
85+
})
86+
}
87+
88+
type testHandler struct {
89+
callCount int
90+
request *backend.QueryDataRequest
91+
}
92+
93+
func (th *testHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
94+
th.callCount++
95+
th.request = req
96+
responses := backend.Responses{}
97+
for _, q := range req.Queries {
98+
responses[q.RefID] = backend.DataResponse{}
99+
}
100+
101+
return &backend.QueryDataResponse{
102+
Responses: responses,
103+
}, nil
104+
}

0 commit comments

Comments
 (0)