Skip to content

Commit 9a6c32a

Browse files
committed
Bootstrap Go project
1 parent 7982a69 commit 9a6c32a

11 files changed

+963
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
coverage/
44
dist/
55
node_modules/
6+
go/
67
*~
78
\#*
89
.\#*

.rc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,24 @@ function yarn() {
2727
else
2828
docker run ${DOCKER_RUN_OPTS} --entrypoint=yarn ${DOCKER_IMAGE} "$@"
2929
fi
30-
}
30+
}
31+
32+
function go() {
33+
DOCKER_IMAGE=go
34+
DOCKER_RUN_OPTS="--rm -v ${PWD}:${PWD} -w ${PWD} -e GOPATH=${PWD}/go"
35+
if [ -n "$ZSH_VERSION" ]; then
36+
docker run ${=DOCKER_RUN_OPTS} ${DOCKER_IMAGE} go "$@"
37+
else
38+
docker run ${DOCKER_RUN_OPTS} ${DOCKER_IMAGE} go "$@"
39+
fi
40+
}
41+
42+
function mage() {
43+
DOCKER_IMAGE=go
44+
DOCKER_RUN_OPTS="--rm -v ${PWD}:${PWD} -w ${PWD} -e GOPATH=${PWD}/go"
45+
if [ -n "$ZSH_VERSION" ]; then
46+
docker run ${=DOCKER_RUN_OPTS} ${DOCKER_IMAGE} mage "$@"
47+
else
48+
docker run ${DOCKER_RUN_OPTS} ${DOCKER_IMAGE} mage "$@"
49+
fi
50+
}

Dockerfile.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM golang:1.18.3
2+
3+
RUN git clone https://github.com/magefile/mage \
4+
&& cd mage \
5+
&& go run bootstrap.go
File renamed without changes.

Magefile.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//+build mage
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
// mage:import
8+
build "github.com/grafana/grafana-plugin-sdk-go/build"
9+
)
10+
11+
// Hello prints a message (shows that you can define custom Mage targets).
12+
func Hello() {
13+
fmt.Println("hello plugin developer!")
14+
}
15+
16+
// Default configures the default target.
17+
var Default = build.BuildAll

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ all: build up
77
build: | build-docker build-splunk-datasource
88

99
build-docker:
10-
docker build . -t node
10+
docker build . -t node -f Dockerfile.node
11+
docker build . -t go -f Dockerfile.go
1112

1213
build-splunk-datasource:
1314
yarn install
1415
yarn build
16+
go mod tidy
17+
mage -v
1518

1619
up:
1720
docker-compose up -d

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/grafana/grafana-starter-datasource-backend
2+
3+
go 1.16
4+
5+
require (
6+
github.com/grafana/grafana-plugin-sdk-go v0.136.0
7+
github.com/magefile/mage v1.13.0 // indirect
8+
)

go.sum

Lines changed: 659 additions & 0 deletions
Large diffs are not rendered by default.

pkg/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
7+
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
8+
"github.com/grafana/grafana-starter-datasource-backend/pkg/plugin"
9+
)
10+
11+
func main() {
12+
// Start listening to requests sent from Grafana. This call is blocking so
13+
// it won't finish until Grafana shuts down the process or the plugin choose
14+
// to exit by itself using os.Exit. Manage automatically manages life cycle
15+
// of datasource instances. It accepts datasource instance factory as first
16+
// argument. This factory will be automatically called on incoming request
17+
// from Grafana to create different instances of SampleDatasource (per datasource
18+
// ID). When datasource configuration changed Dispose method will be called and
19+
// new datasource instance created using NewSampleDatasource factory.
20+
if err := datasource.Manage("myorgid-simple-backend-datasource", plugin.NewSampleDatasource, datasource.ManageOpts{}); err != nil {
21+
log.DefaultLogger.Error(err.Error())
22+
os.Exit(1)
23+
}
24+
}

pkg/plugin/plugin.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"math/rand"
7+
"time"
8+
9+
"github.com/grafana/grafana-plugin-sdk-go/backend"
10+
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
11+
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
12+
"github.com/grafana/grafana-plugin-sdk-go/data"
13+
"github.com/grafana/grafana-plugin-sdk-go/live"
14+
)
15+
16+
// Make sure SampleDatasource implements required interfaces. This is important to do
17+
// since otherwise we will only get a not implemented error response from plugin in
18+
// runtime. In this example datasource instance implements backend.QueryDataHandler,
19+
// backend.CheckHealthHandler, backend.StreamHandler interfaces. Plugin should not
20+
// implement all these interfaces - only those which are required for a particular task.
21+
// For example if plugin does not need streaming functionality then you are free to remove
22+
// methods that implement backend.StreamHandler. Implementing instancemgmt.InstanceDisposer
23+
// is useful to clean up resources used by previous datasource instance when a new datasource
24+
// instance created upon datasource settings changed.
25+
var (
26+
_ backend.QueryDataHandler = (*SampleDatasource)(nil)
27+
_ backend.CheckHealthHandler = (*SampleDatasource)(nil)
28+
_ backend.StreamHandler = (*SampleDatasource)(nil)
29+
_ instancemgmt.InstanceDisposer = (*SampleDatasource)(nil)
30+
)
31+
32+
// NewSampleDatasource creates a new datasource instance.
33+
func NewSampleDatasource(_ backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
34+
return &SampleDatasource{}, nil
35+
}
36+
37+
// SampleDatasource is an example datasource which can respond to data queries, reports
38+
// its health and has streaming skills.
39+
type SampleDatasource struct{}
40+
41+
// Dispose here tells plugin SDK that plugin wants to clean up resources when a new instance
42+
// created. As soon as datasource settings change detected by SDK old datasource instance will
43+
// be disposed and a new one will be created using NewSampleDatasource factory function.
44+
func (d *SampleDatasource) Dispose() {
45+
// Clean up datasource instance resources.
46+
}
47+
48+
// QueryData handles multiple queries and returns multiple responses.
49+
// req contains the queries []DataQuery (where each query contains RefID as a unique identifier).
50+
// The QueryDataResponse contains a map of RefID to the response for each query, and each response
51+
// contains Frames ([]*Frame).
52+
func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
53+
log.DefaultLogger.Info("QueryData called", "request", req)
54+
55+
// create response struct
56+
response := backend.NewQueryDataResponse()
57+
58+
// loop over queries and execute them individually.
59+
for _, q := range req.Queries {
60+
res := d.query(ctx, req.PluginContext, q)
61+
62+
// save the response in a hashmap
63+
// based on with RefID as identifier
64+
response.Responses[q.RefID] = res
65+
}
66+
67+
return response, nil
68+
}
69+
70+
type queryModel struct {
71+
WithStreaming bool `json:"withStreaming"`
72+
}
73+
74+
func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
75+
response := backend.DataResponse{}
76+
77+
// Unmarshal the JSON into our queryModel.
78+
var qm queryModel
79+
80+
response.Error = json.Unmarshal(query.JSON, &qm)
81+
if response.Error != nil {
82+
return response
83+
}
84+
85+
// create data frame response.
86+
frame := data.NewFrame("response")
87+
88+
// add fields.
89+
frame.Fields = append(frame.Fields,
90+
data.NewField("time", nil, []time.Time{query.TimeRange.From, query.TimeRange.To}),
91+
data.NewField("values", nil, []int64{10, 20}),
92+
)
93+
94+
// If query called with streaming on then return a channel
95+
// to subscribe on a client-side and consume updates from a plugin.
96+
// Feel free to remove this if you don't need streaming for your datasource.
97+
if qm.WithStreaming {
98+
channel := live.Channel{
99+
Scope: live.ScopeDatasource,
100+
Namespace: pCtx.DataSourceInstanceSettings.UID,
101+
Path: "stream",
102+
}
103+
frame.SetMeta(&data.FrameMeta{Channel: channel.String()})
104+
}
105+
106+
// add the frames to the response.
107+
response.Frames = append(response.Frames, frame)
108+
109+
return response
110+
}
111+
112+
// CheckHealth handles health checks sent from Grafana to the plugin.
113+
// The main use case for these health checks is the test button on the
114+
// datasource configuration page which allows users to verify that
115+
// a datasource is working as expected.
116+
func (d *SampleDatasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
117+
log.DefaultLogger.Info("CheckHealth called", "request", req)
118+
119+
var status = backend.HealthStatusOk
120+
var message = "Data source is working"
121+
122+
if rand.Int()%2 == 0 {
123+
status = backend.HealthStatusError
124+
message = "randomized error"
125+
}
126+
127+
return &backend.CheckHealthResult{
128+
Status: status,
129+
Message: message,
130+
}, nil
131+
}
132+
133+
// SubscribeStream is called when a client wants to connect to a stream. This callback
134+
// allows sending the first message.
135+
func (d *SampleDatasource) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
136+
log.DefaultLogger.Info("SubscribeStream called", "request", req)
137+
138+
status := backend.SubscribeStreamStatusPermissionDenied
139+
if req.Path == "stream" {
140+
// Allow subscribing only on expected path.
141+
status = backend.SubscribeStreamStatusOK
142+
}
143+
return &backend.SubscribeStreamResponse{
144+
Status: status,
145+
}, nil
146+
}
147+
148+
// RunStream is called once for any open channel. Results are shared with everyone
149+
// subscribed to the same channel.
150+
func (d *SampleDatasource) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
151+
log.DefaultLogger.Info("RunStream called", "request", req)
152+
153+
// Create the same data frame as for query data.
154+
frame := data.NewFrame("response")
155+
156+
// Add fields (matching the same schema used in QueryData).
157+
frame.Fields = append(frame.Fields,
158+
data.NewField("time", nil, make([]time.Time, 1)),
159+
data.NewField("values", nil, make([]int64, 1)),
160+
)
161+
162+
counter := 0
163+
164+
// Stream data frames periodically till stream closed by Grafana.
165+
for {
166+
select {
167+
case <-ctx.Done():
168+
log.DefaultLogger.Info("Context done, finish streaming", "path", req.Path)
169+
return nil
170+
case <-time.After(time.Second):
171+
// Send new data periodically.
172+
frame.Fields[0].Set(0, time.Now())
173+
frame.Fields[1].Set(0, int64(10*(counter%2+1)))
174+
175+
counter++
176+
177+
err := sender.SendFrame(frame, data.IncludeAll)
178+
if err != nil {
179+
log.DefaultLogger.Error("Error sending frame", "error", err)
180+
continue
181+
}
182+
}
183+
}
184+
}
185+
186+
// PublishStream is called when a client sends a message to the stream.
187+
func (d *SampleDatasource) PublishStream(_ context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
188+
log.DefaultLogger.Info("PublishStream called", "request", req)
189+
190+
// Do not allow publishing at all.
191+
return &backend.PublishStreamResponse{
192+
Status: backend.PublishStreamStatusPermissionDenied,
193+
}, nil
194+
}

pkg/plugin/plugin_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package plugin_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/grafana/grafana-plugin-sdk-go/backend"
8+
"github.com/grafana/grafana-starter-datasource-backend/pkg/plugin"
9+
)
10+
11+
// This is where the tests for the datasource backend live.
12+
func TestQueryData(t *testing.T) {
13+
ds := plugin.SampleDatasource{}
14+
15+
resp, err := ds.QueryData(
16+
context.Background(),
17+
&backend.QueryDataRequest{
18+
Queries: []backend.DataQuery{
19+
{RefID: "A"},
20+
},
21+
},
22+
)
23+
if err != nil {
24+
t.Error(err)
25+
}
26+
27+
if len(resp.Responses) != 1 {
28+
t.Fatal("QueryData must return a response")
29+
}
30+
}

0 commit comments

Comments
 (0)