Commit 009d58c4 by Kyle Brandt Committed by GitHub

Plugins: Transform plugin support (#20036)

currently temporary separate http api
parent 69691fbd
......@@ -32,7 +32,7 @@ require (
github.com/gorilla/websocket v1.4.0
github.com/gosimple/slug v1.4.2
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191024130641-6756418f682c
github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191029155514-4d93894a3f7a
github.com/hashicorp/go-hclog v0.8.0
github.com/hashicorp/go-plugin v1.0.1
github.com/hashicorp/go-version v1.1.0
......
......@@ -108,8 +108,8 @@ github.com/gosimple/slug v1.4.2 h1:jDmprx3q/9Lfk4FkGZtvzDQ9Cj9eAmsjzeQGp24PeiQ=
github.com/gosimple/slug v1.4.2/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 h1:SPdxCL9BChFTlyi0Khv64vdCW4TMna8+sxL7+Chx+Ag=
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4/go.mod h1:nc0XxBzjeGcrMltCDw269LoWF9S8ibhgxolCdA1R8To=
github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191024130641-6756418f682c h1:VqEGTH0daEXSpqoLNv87ohtflLi0nuvZsSsrmw3uNnY=
github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191024130641-6756418f682c/go.mod h1:nH8fL+JRTcD8u34ZZw8mYq5mVNtJlVl7AfvFUZ4fzUc=
github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191029155514-4d93894a3f7a h1:4xgIDiu73n83pb5z835OL/33+DGPGs4M94UBdm7lbJc=
github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191029155514-4d93894a3f7a/go.mod h1:cq0Q7VKEJhlo81CrNexlOZ3VuqmaP/z2rq6lQAydtBo=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0 h1:z3ollgGRg8RjfJH6UVBaG54R70GFd++QOkvnJH3VSBY=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
......
......@@ -328,6 +328,7 @@ func (hs *HTTPServer) registerRoutes() {
// metrics
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(hs.QueryMetrics))
apiRoute.Post("/tsdb/query/v2", bind(dtos.MetricRequest{}), Wrap(hs.QueryMetricsV2))
apiRoute.Post("/tsdb/transform", bind(dtos.MetricRequest{}), Wrap(hs.Transform))
apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios))
apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData))
apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk))
......
package api
import (
"net/http"
"github.com/grafana/grafana/pkg/api/dtos"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
)
// POST /api/tsdb/transform
// This enpoint is tempory, will be part of v2 query endpoint.
func (hs *HTTPServer) Transform(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
if !setting.IsExpressionsEnabled() {
return Error(404, "Expressions feature toggle is not enabled", nil)
}
if plugins.Transform == nil {
return Error(http.StatusServiceUnavailable, "transform plugin is not loaded", nil)
}
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
if len(reqDto.Queries) == 0 {
return Error(400, "No queries found in query", nil)
}
var datasourceID int64
for _, query := range reqDto.Queries {
name, err := query.Get("datasource").String()
if err != nil {
return Error(500, "datasource missing name", err)
}
datasourceID, err = query.Get("datasourceId").Int64()
if err != nil {
return Error(400, "GEL datasource missing ID", nil)
}
if name == "-- GEL --" {
break
}
}
ds, err := hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
if err != nil {
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Unable to load datasource meta data", err)
}
request := &tsdb.TsdbQuery{TimeRange: timeRange, Debug: reqDto.Debug}
for _, query := range reqDto.Queries {
request.Queries = append(request.Queries, &tsdb.Query{
RefId: query.Get("refId").MustString("A"),
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000),
Model: query,
DataSource: ds,
})
}
resp, err := plugins.Transform.Transform(c.Req.Context(), ds, request)
if err != nil {
return Error(500, "Transform request error", err)
}
statusCode := 200
for _, res := range resp.Results {
if res.Error != nil {
res.ErrorString = res.Error.Error()
resp.Message = res.ErrorString
statusCode = 400
}
}
return JSON(statusCode, &resp)
}
package backendplugin
import (
"os/exec"
"github.com/grafana/grafana/pkg/infra/log"
datasourceV1 "github.com/grafana/grafana-plugin-model/go/datasource"
rendererV1 "github.com/grafana/grafana-plugin-model/go/renderer"
sdk "github.com/grafana/grafana-plugin-sdk-go/common"
datasourceV2 "github.com/grafana/grafana-plugin-sdk-go/datasource"
transformV2 "github.com/grafana/grafana-plugin-sdk-go/transform"
"github.com/hashicorp/go-plugin"
)
const (
// DefaultProtocolVersion is the protocol version assumed for legacy clients that don't specify
// a particular version or version 1 during their handshake. This is currently the version used
// since Grafana launched support for backend plugins.
DefaultProtocolVersion = 1
)
// Handshake is the HandshakeConfig used to configure clients and servers.
var handshake = plugin.HandshakeConfig{
// The ProtocolVersion is the version that must match between Grafana core
// and Grafana plugins. This should be bumped whenever a (breaking) change
// happens in one or the other that makes it so that they can't safely communicate.
ProtocolVersion: DefaultProtocolVersion,
// The magic cookie values should NEVER be changed.
MagicCookieKey: sdk.MagicCookieKey,
MagicCookieValue: sdk.MagicCookieValue,
}
// NewClientConfig returns a configuration object that can be used to instantiate
// a client for the plugin described by the given metadata.
func NewClientConfig(executablePath string, logger log.Logger, versionedPlugins map[int]plugin.PluginSet) *plugin.ClientConfig {
return &plugin.ClientConfig{
Cmd: exec.Command(executablePath),
HandshakeConfig: handshake,
VersionedPlugins: versionedPlugins,
Logger: logWrapper{Logger: logger},
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
}
}
// NewDatasourceClient returns a datasource plugin client.
func NewDatasourceClient(pluginID, executablePath string, logger log.Logger) *plugin.Client {
versionedPlugins := map[int]plugin.PluginSet{
1: {
pluginID: &datasourceV1.DatasourcePluginImpl{},
},
2: {
pluginID: &datasourceV2.DatasourcePluginImpl{},
},
}
return plugin.NewClient(NewClientConfig(executablePath, logger, versionedPlugins))
}
// NewRendererClient returns a renderer plugin client.
func NewRendererClient(pluginID, executablePath string, logger log.Logger) *plugin.Client {
versionedPlugins := map[int]plugin.PluginSet{
1: {
pluginID: &rendererV1.RendererPluginImpl{},
},
}
return plugin.NewClient(NewClientConfig(executablePath, logger, versionedPlugins))
}
// NewTransformClient returns a transform plugin client.
func NewTransformClient(pluginID, executablePath string, logger log.Logger) *plugin.Client {
versionedPlugins := map[int]plugin.PluginSet{
2: {
pluginID: &transformV2.TransformPluginImpl{},
},
}
return plugin.NewClient(NewClientConfig(executablePath, logger, versionedPlugins))
}
package backendplugin
import (
"io"
"io/ioutil"
"log"
glog "github.com/grafana/grafana/pkg/infra/log"
hclog "github.com/hashicorp/go-hclog"
)
type logWrapper struct {
Logger glog.Logger
}
func (lw logWrapper) Trace(msg string, args ...interface{}) {
lw.Logger.Debug(msg, args...)
}
func (lw logWrapper) Debug(msg string, args ...interface{}) {
lw.Logger.Debug(msg, args...)
}
func (lw logWrapper) Info(msg string, args ...interface{}) {
lw.Logger.Info(msg, args...)
}
func (lw logWrapper) Warn(msg string, args ...interface{}) {
lw.Logger.Warn(msg, args...)
}
func (lw logWrapper) Error(msg string, args ...interface{}) {
lw.Logger.Error(msg, args...)
}
func (lw logWrapper) IsTrace() bool { return true }
func (lw logWrapper) IsDebug() bool { return true }
func (lw logWrapper) IsInfo() bool { return true }
func (lw logWrapper) IsWarn() bool { return true }
func (lw logWrapper) IsError() bool { return true }
func (lw logWrapper) With(args ...interface{}) hclog.Logger {
return logWrapper{Logger: lw.Logger.New(args...)}
}
func (lw logWrapper) Named(name string) hclog.Logger {
return logWrapper{Logger: lw.Logger.New()}
}
func (lw logWrapper) ResetNamed(name string) hclog.Logger {
return logWrapper{Logger: lw.Logger.New()}
}
func (lw logWrapper) StandardLogger(ops *hclog.StandardLoggerOptions) *log.Logger {
return nil
}
func (lw logWrapper) SetLevel(level hclog.Level) {}
// Return a value that conforms to io.Writer, which can be passed into log.SetOutput()
func (lw logWrapper) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return ioutil.Discard
}
......@@ -3,96 +3,15 @@ package wrapper
import (
"context"
"errors"
"fmt"
sdk "github.com/grafana/grafana-plugin-sdk-go"
"github.com/grafana/grafana-plugin-sdk-go/dataframe"
"github.com/grafana/grafana-plugin-sdk-go/genproto/datasource"
"github.com/grafana/grafana/pkg/bus"
sdk "github.com/grafana/grafana-plugin-sdk-go/datasource"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
type grafanaAPI struct {
logger log.Logger
}
func (s *grafanaAPI) QueryDatasource(ctx context.Context, req *datasource.QueryDatasourceRequest) (*datasource.QueryDatasourceResponse, error) {
if len(req.Queries) == 0 {
return nil, fmt.Errorf("zero queries found in datasource request")
}
getDsInfo := &models.GetDataSourceByIdQuery{
Id: req.DatasourceId,
OrgId: req.OrgId,
}
if err := bus.Dispatch(getDsInfo); err != nil {
return nil, fmt.Errorf("Could not find datasource %v", err)
}
// Convert plugin-model (datasource) queries to tsdb queries
queries := make([]*tsdb.Query, len(req.Queries))
for i, query := range req.Queries {
sj, err := simplejson.NewJson([]byte(query.ModelJson))
if err != nil {
return nil, err
}
queries[i] = &tsdb.Query{
RefId: query.RefId,
IntervalMs: query.IntervalMs,
MaxDataPoints: query.MaxDataPoints,
DataSource: getDsInfo.Result,
Model: sj,
}
}
timeRange := tsdb.NewTimeRange(req.TimeRange.FromRaw, req.TimeRange.ToRaw)
tQ := &tsdb.TsdbQuery{
TimeRange: timeRange,
Queries: queries,
}
// Execute the converted queries
tsdbRes, err := tsdb.HandleRequest(ctx, getDsInfo.Result, tQ)
if err != nil {
return nil, err
}
// Convert tsdb results (map) to plugin-model/datasource (slice) results
// Only error and Series responses mapped.
results := make([]*datasource.QueryResult, len(tsdbRes.Results))
resIdx := 0
for refID, res := range tsdbRes.Results {
qr := &datasource.QueryResult{
RefId: refID,
}
if res.Error != nil {
qr.Error = res.ErrorString
results[resIdx] = qr
resIdx++
continue
}
encodedFrames := make([][]byte, len(res.Series))
for sIdx, series := range res.Series {
frame, err := tsdb.SeriesToFrame(series)
if err != nil {
return nil, err
}
encodedFrames[sIdx], err = dataframe.MarshalArrow(frame)
if err != nil {
return nil, err
}
}
qr.Dataframes = encodedFrames
results[resIdx] = qr
resIdx++
}
return &datasource.QueryDatasourceResponse{Results: results}, nil
}
func NewDatasourcePluginWrapperV2(log log.Logger, plugin sdk.DatasourcePlugin) *DatasourcePluginWrapperV2 {
return &DatasourcePluginWrapperV2{DatasourcePlugin: plugin, logger: log}
}
......@@ -108,8 +27,8 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
return nil, err
}
pbQuery := &datasource.DatasourceRequest{
Datasource: &datasource.DatasourceInfo{
pbQuery := &pluginv2.DatasourceRequest{
Datasource: &pluginv2.DatasourceInfo{
Name: ds.Name,
Type: ds.Type,
Url: ds.Url,
......@@ -118,13 +37,13 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
JsonData: string(jsonData),
DecryptedSecureJsonData: ds.SecureJsonData.Decrypt(),
},
TimeRange: &datasource.TimeRange{
TimeRange: &pluginv2.TimeRange{
FromRaw: query.TimeRange.From,
ToRaw: query.TimeRange.To,
ToEpochMs: query.TimeRange.GetToAsMsEpoch(),
FromEpochMs: query.TimeRange.GetFromAsMsEpoch(),
},
Queries: []*datasource.Query{},
Queries: []*pluginv2.DatasourceQuery{},
}
for _, q := range query.Queries {
......@@ -132,7 +51,7 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
if err != nil {
return nil, err
}
pbQuery.Queries = append(pbQuery.Queries, &datasource.Query{
pbQuery.Queries = append(pbQuery.Queries, &pluginv2.DatasourceQuery{
ModelJson: string(modelJSON),
IntervalMs: q.IntervalMs,
RefId: q.RefId,
......@@ -140,7 +59,7 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
})
}
pbres, err := tw.DatasourcePlugin.Query(ctx, pbQuery, &grafanaAPI{logger: tw.logger})
pbres, err := tw.DatasourcePlugin.Query(ctx, pbQuery)
if err != nil {
return nil, err
......
......@@ -5,15 +5,16 @@ import (
"encoding/json"
"errors"
"fmt"
"os/exec"
"path"
"time"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
datasourceV1 "github.com/grafana/grafana-plugin-model/go/datasource"
sdk "github.com/grafana/grafana-plugin-sdk-go"
sdk "github.com/grafana/grafana-plugin-sdk-go/datasource"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
......@@ -63,12 +64,6 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
return nil
}
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "grafana_plugin_type",
MagicCookieValue: "datasource",
}
func (p *DataSourcePlugin) startBackendPlugin(ctx context.Context, log log.Logger) error {
p.log = log.New("plugin-id", p.Id)
......@@ -84,6 +79,7 @@ func (p *DataSourcePlugin) startBackendPlugin(ctx context.Context, log log.Logge
return nil
}
func (p *DataSourcePlugin) isVersionOne() bool {
return !p.SDK
}
......@@ -92,27 +88,7 @@ func (p *DataSourcePlugin) spawnSubProcess() error {
cmd := ComposePluginStartCommmand(p.Executable)
fullpath := path.Join(p.PluginDir, cmd)
var newClient *plugin.Client
if p.isVersionOne() {
newClient = plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: map[string]plugin.Plugin{p.Id: &datasourceV1.DatasourcePluginImpl{}},
Cmd: exec.Command(fullpath),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: LogWrapper{Logger: p.log},
})
} else {
newClient = plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: map[string]plugin.Plugin{p.Id: &sdk.DatasourcePluginImpl{}},
Cmd: exec.Command(fullpath),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: LogWrapper{Logger: p.log},
})
}
p.client = newClient
p.client = backendplugin.NewDatasourceClient(p.Id, fullpath, p.log)
rpcClient, err := p.client.Client()
if err != nil {
......@@ -124,7 +100,7 @@ func (p *DataSourcePlugin) spawnSubProcess() error {
return err
}
if p.isVersionOne() {
if p.client.NegotiatedVersion() == 1 {
plugin := raw.(datasourceV1.DatasourcePlugin)
tsdb.RegisterTsdbQueryEndpoint(p.Id, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
......
......@@ -29,6 +29,7 @@ var (
Plugins map[string]*PluginBase
PluginTypes map[string]interface{}
Renderer *RendererPlugin
Transform *TransformPlugin
GrafanaLatestVersion string
GrafanaHasUpdate bool
......@@ -62,6 +63,7 @@ func (pm *PluginManager) Init() error {
"datasource": DataSourcePlugin{},
"app": AppPlugin{},
"renderer": RendererPlugin{},
"transform": TransformPlugin{},
}
pm.log.Info("Starting plugin search")
......@@ -118,6 +120,11 @@ func (pm *PluginManager) startBackendPlugins(ctx context.Context) {
pm.log.Error("Failed to init plugin.", "error", err, "plugin", ds.Id)
}
}
if Transform != nil {
if err := Transform.startBackendPlugin(ctx, plog); err != nil {
pm.log.Error("Failed to init plugin.", "error", err, "plugin", Transform.Id)
}
}
}
func (pm *PluginManager) Run(ctx context.Context) error {
......@@ -263,7 +270,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
}
func (scanner *PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
return pluginType == "renderer"
return pluginType == "renderer" || pluginType == "transform"
}
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
......
package plugins
import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
"time"
"github.com/grafana/grafana-plugin-sdk-go/dataframe"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/grafana/grafana-plugin-sdk-go/transform"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/tsdb"
plugin "github.com/hashicorp/go-plugin"
"golang.org/x/xerrors"
)
type TransformPlugin struct {
PluginBase
// TODO we probably want a Backend Plugin Base? Or some way to dedup proc management code
Executable string `json:"executable,omitempty"`
//transform.TransformPlugin
*TransformWrapper
client *plugin.Client
log log.Logger
}
func (tp *TransformPlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&tp); err != nil {
return err
}
if err := tp.registerPlugin(pluginDir); err != nil {
return err
}
Transform = tp
return nil
}
func (p *TransformPlugin) startBackendPlugin(ctx context.Context, log log.Logger) error {
p.log = log.New("plugin-id", p.Id)
if err := p.spawnSubProcess(); err != nil {
return err
}
go func() {
if err := p.restartKilledProcess(ctx); err != nil {
p.log.Error("Attempting to restart killed process failed", "err", err)
}
}()
return nil
}
func (p *TransformPlugin) spawnSubProcess() error {
cmd := ComposePluginStartCommmand(p.Executable)
fullpath := path.Join(p.PluginDir, cmd)
p.client = backendplugin.NewTransformClient(p.Id, fullpath, p.log)
rpcClient, err := p.client.Client()
if err != nil {
return err
}
raw, err := rpcClient.Dispense(p.Id)
if err != nil {
return err
}
plugin, ok := raw.(transform.TransformPlugin)
if !ok {
return fmt.Errorf("unexpected type %T, expected *transform.GRPCClient", raw)
}
p.TransformWrapper = NewTransformWrapper(p.log, plugin)
return nil
}
func (p *TransformPlugin) restartKilledProcess(ctx context.Context) error {
ticker := time.NewTicker(time.Second * 1)
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil && !xerrors.Is(err, context.Canceled) {
return err
}
return nil
case <-ticker.C:
if !p.client.Exited() {
continue
}
if err := p.spawnSubProcess(); err != nil {
p.log.Error("Failed to restart plugin", "err", err)
continue
}
p.log.Debug("Plugin process restarted")
}
}
}
func (p *TransformPlugin) Kill() {
if p.client != nil {
p.log.Debug("Killing subprocess ", "name", p.Name)
p.client.Kill()
}
}
// ...
// Wrapper Code
// ...
func NewTransformWrapper(log log.Logger, plugin transform.TransformPlugin) *TransformWrapper {
return &TransformWrapper{plugin, log, &grafanaAPI{log}}
}
type TransformWrapper struct {
transform.TransformPlugin
logger log.Logger
api *grafanaAPI
}
func (tw *TransformWrapper) Transform(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
jsonData, err := ds.JsonData.MarshalJSON()
if err != nil {
return nil, err
}
pbQuery := &pluginv2.TransformRequest{
Datasource: &pluginv2.DatasourceInfo{
Name: ds.Name,
Type: ds.Type,
Url: ds.Url,
Id: ds.Id,
OrgId: ds.OrgId,
JsonData: string(jsonData),
DecryptedSecureJsonData: ds.SecureJsonData.Decrypt(),
},
TimeRange: &pluginv2.TimeRange{
FromRaw: query.TimeRange.From,
ToRaw: query.TimeRange.To,
ToEpochMs: query.TimeRange.GetToAsMsEpoch(),
FromEpochMs: query.TimeRange.GetFromAsMsEpoch(),
},
Queries: []*pluginv2.TransformQuery{},
}
for _, q := range query.Queries {
modelJSON, err := q.Model.MarshalJSON()
if err != nil {
return nil, err
}
pbQuery.Queries = append(pbQuery.Queries, &pluginv2.TransformQuery{
ModelJson: string(modelJSON),
IntervalMs: q.IntervalMs,
RefId: q.RefId,
MaxDataPoints: q.MaxDataPoints,
})
}
pbres, err := tw.TransformPlugin.Transform(ctx, pbQuery, tw.api)
if err != nil {
return nil, err
}
res := &tsdb.Response{
Results: map[string]*tsdb.QueryResult{},
}
for _, r := range pbres.Results {
qr := &tsdb.QueryResult{
RefId: r.RefId,
}
if r.Error != "" {
qr.Error = errors.New(r.Error)
qr.ErrorString = r.Error
}
if r.MetaJson != "" {
metaJSON, err := simplejson.NewJson([]byte(r.MetaJson))
if err != nil {
tw.logger.Error("Error parsing JSON Meta field: " + err.Error())
}
qr.Meta = metaJSON
}
qr.Dataframes = r.Dataframes
res.Results[r.RefId] = qr
}
return res, nil
}
type grafanaAPI struct {
logger log.Logger
}
func (s *grafanaAPI) QueryDatasource(ctx context.Context, req *pluginv2.QueryDatasourceRequest) (*pluginv2.QueryDatasourceResponse, error) {
if len(req.Queries) == 0 {
return nil, fmt.Errorf("zero queries found in datasource request")
}
getDsInfo := &models.GetDataSourceByIdQuery{
Id: req.DatasourceId,
OrgId: req.OrgId,
}
if err := bus.Dispatch(getDsInfo); err != nil {
return nil, fmt.Errorf("Could not find datasource %v", err)
}
// Convert plugin-model (datasource) queries to tsdb queries
queries := make([]*tsdb.Query, len(req.Queries))
for i, query := range req.Queries {
sj, err := simplejson.NewJson([]byte(query.ModelJson))
if err != nil {
return nil, err
}
queries[i] = &tsdb.Query{
RefId: query.RefId,
IntervalMs: query.IntervalMs,
MaxDataPoints: query.MaxDataPoints,
DataSource: getDsInfo.Result,
Model: sj,
}
}
timeRange := tsdb.NewTimeRange(req.TimeRange.FromRaw, req.TimeRange.ToRaw)
tQ := &tsdb.TsdbQuery{
TimeRange: timeRange,
Queries: queries,
}
// Execute the converted queries
tsdbRes, err := tsdb.HandleRequest(ctx, getDsInfo.Result, tQ)
if err != nil {
return nil, err
}
// Convert tsdb results (map) to plugin-model/datasource (slice) results
// Only error and Series responses mapped.
results := make([]*pluginv2.DatasourceQueryResult, len(tsdbRes.Results))
resIdx := 0
for refID, res := range tsdbRes.Results {
qr := &pluginv2.DatasourceQueryResult{
RefId: refID,
}
if res.Error != nil {
qr.Error = res.ErrorString
results[resIdx] = qr
resIdx++
continue
}
encodedFrames := make([][]byte, len(res.Series))
for sIdx, series := range res.Series {
frame, err := tsdb.SeriesToFrame(series)
if err != nil {
return nil, err
}
encodedFrames[sIdx], err = dataframe.MarshalArrow(frame)
if err != nil {
return nil, err
}
}
qr.Dataframes = encodedFrames
results[resIdx] = qr
resIdx++
}
return &pluginv2.QueryDatasourceResponse{Results: results}, nil
}
......@@ -3,37 +3,21 @@ package rendering
import (
"context"
"fmt"
"os/exec"
"path"
"time"
pluginModel "github.com/grafana/grafana-plugin-model/go/renderer"
"github.com/grafana/grafana/pkg/plugins"
plugin "github.com/hashicorp/go-plugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
)
func (rs *RenderingService) startPlugin(ctx context.Context) error {
cmd := plugins.ComposePluginStartCommmand("plugin_start")
fullpath := path.Join(rs.pluginInfo.PluginDir, cmd)
var handshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "grafana_plugin_type",
MagicCookieValue: "renderer",
}
rs.log.Info("Renderer plugin found, starting", "cmd", cmd)
rs.pluginClient = plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshakeConfig,
Plugins: map[string]plugin.Plugin{
plugins.Renderer.Id: &pluginModel.RendererPluginImpl{},
},
Cmd: exec.Command(fullpath),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: plugins.LogWrapper{Logger: rs.log},
})
rs.pluginClient = backendplugin.NewRendererClient(plugins.Renderer.Id, fullpath, rs.log)
rpcClient, err := rs.pluginClient.Client()
if err != nil {
return err
......
SRC_DIR=./proto
DST_DIR=./genproto
all: build
${DST_DIR}/datasource/datasource.pb.go: ${SRC_DIR}/datasource.proto
protoc -I=${SRC_DIR} --go_out=plugins=grpc:${DST_DIR}/datasource/ ${SRC_DIR}/datasource.proto
build-proto: ${DST_DIR}/datasource/datasource.pb.go
build: build-proto
go build ./...
.PHONY: all build build-proto
# Grafana Plugin SDK for Go
Develop Grafana backend plugins with this Go SDK.
**Warning**: This SDK is currently in alpha and will likely have major breaking changes during early development. Please do not consider this SDK published until this warning has been removed.
## Usage
```go
package main
import (
"context"
"log"
"os"
gf "github.com/grafana/grafana-plugin-sdk-go"
)
const pluginID = "myorg-custom-datasource"
type MyDataSource struct {
logger *log.Logger
}
func (d *MyDataSource) Query(ctx context.Context, tr gf.TimeRange, ds gf.DataSourceInfo, queries []gf.Query) ([]gf.QueryResult, error) {
return []gf.QueryResult{}, nil
}
func main() {
logger := log.New(os.Stderr, "", 0)
srv := gf.NewServer()
srv.HandleDataSource(pluginID, &MyDataSource{
logger: logger,
})
if err := srv.Serve(); err != nil {
logger.Fatal(err)
}
}
```
## Developing
Generate Go code for Protobuf definitions:
```
make build-proto
```
package common
import plugin "github.com/hashicorp/go-plugin"
const (
MagicCookieKey = "grafana_plugin_type"
MagicCookieValue = "datasource"
ProtocolVersion = 2
)
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: ProtocolVersion,
MagicCookieKey: MagicCookieKey,
MagicCookieValue: MagicCookieValue,
}
package grafana
package datasource
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/dataframe"
"github.com/grafana/grafana-plugin-sdk-go/genproto/datasource"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
plugin "github.com/hashicorp/go-plugin"
)
......@@ -45,7 +44,7 @@ type QueryResult struct {
// DataSourceHandler handles data source queries.
type DataSourceHandler interface {
Query(ctx context.Context, tr TimeRange, ds DataSourceInfo, queries []Query, api GrafanaAPIHandler) ([]QueryResult, error)
Query(ctx context.Context, tr TimeRange, ds DataSourceInfo, queries []Query) ([]QueryResult, error)
}
// datasourcePluginWrapper converts to and from protobuf types.
......@@ -55,7 +54,7 @@ type datasourcePluginWrapper struct {
handler DataSourceHandler
}
func (p *datasourcePluginWrapper) Query(ctx context.Context, req *datasource.DatasourceRequest, api GrafanaAPI) (*datasource.DatasourceResponse, error) {
func (p *datasourcePluginWrapper) Query(ctx context.Context, req *pluginv2.DatasourceRequest) (*pluginv2.DatasourceResponse, error) {
tr := TimeRange{
From: time.Unix(0, req.TimeRange.FromEpochMs*int64(time.Millisecond)),
To: time.Unix(0, req.TimeRange.ToEpochMs*int64(time.Millisecond)),
......@@ -80,18 +79,18 @@ func (p *datasourcePluginWrapper) Query(ctx context.Context, req *datasource.Dat
})
}
results, err := p.handler.Query(ctx, tr, dsi, queries, &grafanaAPIWrapper{api: api})
results, err := p.handler.Query(ctx, tr, dsi, queries)
if err != nil {
return nil, err
}
if len(results) == 0 {
return &datasource.DatasourceResponse{
Results: []*datasource.QueryResult{},
return &pluginv2.DatasourceResponse{
Results: []*pluginv2.DatasourceQueryResult{},
}, nil
}
var respResults []*datasource.QueryResult
var respResults []*pluginv2.DatasourceQueryResult
for _, res := range results {
encodedFrames := make([][]byte, len(res.DataFrames))
......@@ -105,7 +104,7 @@ func (p *datasourcePluginWrapper) Query(ctx context.Context, req *datasource.Dat
}
}
queryResult := &datasource.QueryResult{
queryResult := &pluginv2.DatasourceQueryResult{
Error: res.Error,
RefId: res.RefID,
MetaJson: res.MetaJSON,
......@@ -115,7 +114,7 @@ func (p *datasourcePluginWrapper) Query(ctx context.Context, req *datasource.Dat
respResults = append(respResults, queryResult)
}
return &datasource.DatasourceResponse{
return &pluginv2.DatasourceResponse{
Results: respResults,
}, nil
}
......@@ -127,59 +126,3 @@ type DatasourceQueryResult struct {
MetaJSON string
DataFrames []*dataframe.Frame
}
// GrafanaAPIHandler handles data source queries.
type GrafanaAPIHandler interface {
QueryDatasource(ctx context.Context, orgID int64, datasourceID int64, tr TimeRange, queries []Query) ([]DatasourceQueryResult, error)
}
// grafanaAPIWrapper converts to and from Grafana types for calls from a datasource.
type grafanaAPIWrapper struct {
api GrafanaAPI
}
func (w *grafanaAPIWrapper) QueryDatasource(ctx context.Context, orgID int64, datasourceID int64, tr TimeRange, queries []Query) ([]DatasourceQueryResult, error) {
rawQueries := make([]*datasource.Query, 0, len(queries))
for _, q := range queries {
rawQueries = append(rawQueries, &datasource.Query{
RefId: q.RefID,
MaxDataPoints: q.MaxDataPoints,
IntervalMs: q.Interval.Milliseconds(),
ModelJson: string(q.ModelJSON),
})
}
rawResp, err := w.api.QueryDatasource(ctx, &datasource.QueryDatasourceRequest{
OrgId: orgID,
DatasourceId: datasourceID,
TimeRange: &datasource.TimeRange{
FromEpochMs: tr.From.UnixNano() / 1e6,
ToEpochMs: tr.To.UnixNano() / 1e6,
FromRaw: fmt.Sprintf("%v", tr.From.UnixNano()/1e6),
ToRaw: fmt.Sprintf("%v", tr.To.UnixNano()/1e6),
},
Queries: rawQueries,
})
if err != nil {
return nil, err
}
results := make([]DatasourceQueryResult, len(rawResp.GetResults()))
for resIdx, rawRes := range rawResp.GetResults() {
// TODO Error property etc
dfs := make([]*dataframe.Frame, len(rawRes.Dataframes))
for dfIdx, b := range rawRes.Dataframes {
dfs[dfIdx], err = dataframe.UnMarshalArrow(b)
if err != nil {
return nil, err
}
}
results[resIdx] = DatasourceQueryResult{
DataFrames: dfs,
}
}
return results, nil
}
package datasource
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
type GRPCClient struct {
client pluginv2.DatasourcePluginClient
}
// DatasourcePlugin is the Grafana datasource plugin interface.
type DatasourcePlugin interface {
Query(ctx context.Context, req *pluginv2.DatasourceRequest) (*pluginv2.DatasourceResponse, error)
}
type grpcServer struct {
Impl datasourcePluginWrapper
}
func (m *GRPCClient) Query(ctx context.Context, req *pluginv2.DatasourceRequest) (*pluginv2.DatasourceResponse, error) {
return m.client.Query(ctx, req)
}
func (m *grpcServer) Query(ctx context.Context, req *pluginv2.DatasourceRequest) (*pluginv2.DatasourceResponse, error) {
return m.Impl.Query(ctx, req)
}
package grafana
package datasource
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/datasource"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
......@@ -16,13 +16,12 @@ type DatasourcePluginImpl struct {
}
func (p *DatasourcePluginImpl) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
datasource.RegisterDatasourcePluginServer(s, &grpcServer{
Impl: p.Impl,
broker: broker,
pluginv2.RegisterDatasourcePluginServer(s, &grpcServer{
Impl: p.Impl,
})
return nil
}
func (p *DatasourcePluginImpl) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCClient{client: datasource.NewDatasourcePluginClient(c), broker: broker}, nil
return &GRPCClient{client: pluginv2.NewDatasourcePluginClient(c)}, nil
}
package datasource
import (
"github.com/grafana/grafana-plugin-sdk-go/common"
plugin "github.com/hashicorp/go-plugin"
)
// Serve starts serving the datasource plugin over gRPC.
//
// The plugin ID should be in the format <org>-<name>-datasource.
func Serve(pluginID string, handler DataSourceHandler) error {
versionedPlugins := map[int]plugin.PluginSet{
common.ProtocolVersion: {
pluginID: &DatasourcePluginImpl{
Impl: datasourcePluginWrapper{
handler: handler,
},
},
},
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: common.Handshake,
VersionedPlugins: versionedPlugins,
GRPCServer: plugin.DefaultGRPCServer,
})
return nil
}
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: common.proto
package pluginv2
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type TimeRange struct {
FromRaw string `protobuf:"bytes,1,opt,name=fromRaw,proto3" json:"fromRaw,omitempty"`
ToRaw string `protobuf:"bytes,2,opt,name=toRaw,proto3" json:"toRaw,omitempty"`
FromEpochMs int64 `protobuf:"varint,3,opt,name=fromEpochMs,proto3" json:"fromEpochMs,omitempty"`
ToEpochMs int64 `protobuf:"varint,4,opt,name=toEpochMs,proto3" json:"toEpochMs,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *TimeRange) Reset() { *m = TimeRange{} }
func (m *TimeRange) String() string { return proto.CompactTextString(m) }
func (*TimeRange) ProtoMessage() {}
func (*TimeRange) Descriptor() ([]byte, []int) {
return fileDescriptor_common_efe63640b8634961, []int{0}
}
func (m *TimeRange) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TimeRange.Unmarshal(m, b)
}
func (m *TimeRange) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_TimeRange.Marshal(b, m, deterministic)
}
func (dst *TimeRange) XXX_Merge(src proto.Message) {
xxx_messageInfo_TimeRange.Merge(dst, src)
}
func (m *TimeRange) XXX_Size() int {
return xxx_messageInfo_TimeRange.Size(m)
}
func (m *TimeRange) XXX_DiscardUnknown() {
xxx_messageInfo_TimeRange.DiscardUnknown(m)
}
var xxx_messageInfo_TimeRange proto.InternalMessageInfo
func (m *TimeRange) GetFromRaw() string {
if m != nil {
return m.FromRaw
}
return ""
}
func (m *TimeRange) GetToRaw() string {
if m != nil {
return m.ToRaw
}
return ""
}
func (m *TimeRange) GetFromEpochMs() int64 {
if m != nil {
return m.FromEpochMs
}
return 0
}
func (m *TimeRange) GetToEpochMs() int64 {
if m != nil {
return m.ToEpochMs
}
return 0
}
type DatasourceInfo struct {
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
OrgId int64 `protobuf:"varint,2,opt,name=orgId,proto3" json:"orgId,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"`
JsonData string `protobuf:"bytes,6,opt,name=jsonData,proto3" json:"jsonData,omitempty"`
DecryptedSecureJsonData map[string]string `protobuf:"bytes,7,rep,name=decryptedSecureJsonData,proto3" json:"decryptedSecureJsonData,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *DatasourceInfo) Reset() { *m = DatasourceInfo{} }
func (m *DatasourceInfo) String() string { return proto.CompactTextString(m) }
func (*DatasourceInfo) ProtoMessage() {}
func (*DatasourceInfo) Descriptor() ([]byte, []int) {
return fileDescriptor_common_efe63640b8634961, []int{1}
}
func (m *DatasourceInfo) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_DatasourceInfo.Unmarshal(m, b)
}
func (m *DatasourceInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_DatasourceInfo.Marshal(b, m, deterministic)
}
func (dst *DatasourceInfo) XXX_Merge(src proto.Message) {
xxx_messageInfo_DatasourceInfo.Merge(dst, src)
}
func (m *DatasourceInfo) XXX_Size() int {
return xxx_messageInfo_DatasourceInfo.Size(m)
}
func (m *DatasourceInfo) XXX_DiscardUnknown() {
xxx_messageInfo_DatasourceInfo.DiscardUnknown(m)
}
var xxx_messageInfo_DatasourceInfo proto.InternalMessageInfo
func (m *DatasourceInfo) GetId() int64 {
if m != nil {
return m.Id
}
return 0
}
func (m *DatasourceInfo) GetOrgId() int64 {
if m != nil {
return m.OrgId
}
return 0
}
func (m *DatasourceInfo) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *DatasourceInfo) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *DatasourceInfo) GetUrl() string {
if m != nil {
return m.Url
}
return ""
}
func (m *DatasourceInfo) GetJsonData() string {
if m != nil {
return m.JsonData
}
return ""
}
func (m *DatasourceInfo) GetDecryptedSecureJsonData() map[string]string {
if m != nil {
return m.DecryptedSecureJsonData
}
return nil
}
func init() {
proto.RegisterType((*TimeRange)(nil), "pluginv2.TimeRange")
proto.RegisterType((*DatasourceInfo)(nil), "pluginv2.DatasourceInfo")
proto.RegisterMapType((map[string]string)(nil), "pluginv2.DatasourceInfo.DecryptedSecureJsonDataEntry")
}
func init() { proto.RegisterFile("common.proto", fileDescriptor_common_efe63640b8634961) }
var fileDescriptor_common_efe63640b8634961 = []byte{
// 296 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x91, 0xcf, 0x4a, 0xc3, 0x40,
0x10, 0xc6, 0x49, 0xb6, 0xff, 0x32, 0x95, 0x22, 0x8b, 0x60, 0x28, 0x3d, 0x84, 0x9e, 0x72, 0xca,
0xa1, 0x22, 0x88, 0xe7, 0xf6, 0xd0, 0x82, 0x97, 0xd5, 0x17, 0x58, 0x93, 0x6d, 0x8c, 0x26, 0x3b,
0x61, 0xb3, 0xa9, 0x04, 0x9f, 0xd0, 0xb7, 0x92, 0x9d, 0x1a, 0xff, 0x80, 0xf6, 0xf6, 0x7d, 0xbf,
0x9d, 0xd9, 0x6f, 0x98, 0x81, 0xb3, 0x14, 0xab, 0x0a, 0x75, 0x52, 0x1b, 0xb4, 0xc8, 0x27, 0x75,
0xd9, 0xe6, 0x85, 0x3e, 0xac, 0x96, 0x6f, 0x10, 0x3c, 0x14, 0x95, 0x12, 0x52, 0xe7, 0x8a, 0x87,
0x30, 0xde, 0x1b, 0xac, 0x84, 0x7c, 0x0d, 0xbd, 0xc8, 0x8b, 0x03, 0xd1, 0x5b, 0x7e, 0x01, 0x43,
0x8b, 0x8e, 0xfb, 0xc4, 0x8f, 0x86, 0x47, 0x30, 0x75, 0x05, 0x9b, 0x1a, 0xd3, 0xa7, 0xbb, 0x26,
0x64, 0x91, 0x17, 0x33, 0xf1, 0x13, 0xf1, 0x05, 0x04, 0x16, 0xfb, 0xf7, 0x01, 0xbd, 0x7f, 0x83,
0xe5, 0xbb, 0x0f, 0xb3, 0xb5, 0xb4, 0xb2, 0xc1, 0xd6, 0xa4, 0x6a, 0xab, 0xf7, 0xc8, 0x67, 0xe0,
0x17, 0x19, 0xa5, 0x33, 0xe1, 0x17, 0x99, 0x0b, 0x46, 0x93, 0x6f, 0x33, 0x0a, 0x66, 0xe2, 0x68,
0x38, 0x87, 0x81, 0x96, 0x95, 0xa2, 0xc4, 0x40, 0x90, 0x76, 0xcc, 0x76, 0xb5, 0xa2, 0x94, 0x40,
0x90, 0xe6, 0xe7, 0xc0, 0x5a, 0x53, 0x86, 0x43, 0x42, 0x4e, 0xf2, 0x39, 0x4c, 0x9e, 0x1b, 0xd4,
0x2e, 0x35, 0x1c, 0x11, 0xfe, 0xf2, 0x1c, 0xe1, 0x32, 0x53, 0xa9, 0xe9, 0x6a, 0xab, 0xb2, 0x7b,
0x95, 0xb6, 0x46, 0xed, 0xfa, 0xd2, 0x71, 0xc4, 0xe2, 0xe9, 0xea, 0x3a, 0xe9, 0xf7, 0x96, 0xfc,
0x1e, 0x3b, 0x59, 0xff, 0xdd, 0xb7, 0xd1, 0xd6, 0x74, 0xe2, 0xbf, 0x5f, 0xe7, 0x3b, 0x58, 0x9c,
0x6a, 0x74, 0xe3, 0xbf, 0xa8, 0xee, 0xf3, 0x16, 0x4e, 0xba, 0x75, 0x1c, 0x64, 0xd9, 0xaa, 0xfe,
0x0e, 0x64, 0x6e, 0xfd, 0x1b, 0xef, 0x71, 0x44, 0x97, 0xbd, 0xfa, 0x08, 0x00, 0x00, 0xff, 0xff,
0x82, 0xb7, 0xa6, 0x7c, 0xe9, 0x01, 0x00, 0x00,
}
module github.com/grafana/grafana-plugin-sdk-go
go 1.12
require (
github.com/apache/arrow/go/arrow v0.0.0-20190716210558-5f564424c71c
github.com/golang/protobuf v1.3.2
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd
github.com/hashicorp/go-plugin v1.0.1
github.com/mattetti/filebuffer v1.0.0
github.com/stretchr/testify v1.3.0
golang.org/x/net v0.0.0-20190311183353-d8887717615a
google.golang.org/grpc v1.23.1
)
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/apache/arrow/go/arrow v0.0.0-20190716210558-5f564424c71c h1:iHUHzx3S1TU5xt+D7vLb0PAk3e+RfayF9IhR6+hyO/k=
github.com/apache/arrow/go/arrow v0.0.0-20190716210558-5f564424c71c/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
github.com/apache/arrow/go/arrow v0.0.0-20190926121000-5b4a08f3d2cf h1:yvUZ/QVWcn9Et/v+9lnKHqKySB3D4G4MkIr5UN2MvTk=
github.com/apache/arrow/go/arrow v0.0.0-20190926121000-5b4a08f3d2cf/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/flatbuffers v1.11.0 h1:O7CEyB8Cb3/DmtxODGtLHcEvpr81Jm5qLg/hsHnxA2A=
github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd h1:rNuUHR+CvK1IS89MMtcF0EpcVMZtjKfPRp4MEmt/aTs=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/mattetti/filebuffer v1.0.0 h1:ixTvQ0JjBTwWbdpDZ98lLrydo7KRi8xNRIi5RFszsbY=
github.com/mattetti/filebuffer v1.0.0/go.mod h1:X6nyAIge2JGVmuJt2MFCqmHrb/5IHiphfHtot0s5cnI=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
package grafana
import (
plugin "github.com/hashicorp/go-plugin"
)
const (
magicCookieKey = "grafana_plugin_type"
magicCookieValue = "datasource"
)
// Server serves all registered data source handlers.
type Server struct {
datasources map[string]DataSourceHandler
}
// NewServer returns a new instance of Server.
func NewServer() *Server {
return &Server{
datasources: make(map[string]DataSourceHandler),
}
}
// HandleDataSource registers a new data source.
//
// The plugin ID should be in the format <org>-<name>-datasource.
func (g *Server) HandleDataSource(pluginID string, p DataSourceHandler) {
g.datasources[pluginID] = p
}
// Serve starts serving the registered handlers over gRPC.
func (g *Server) Serve() error {
plugins := make(map[string]plugin.Plugin)
for id, h := range g.datasources {
plugins[id] = &DatasourcePluginImpl{
Impl: datasourcePluginWrapper{
handler: h,
},
}
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: magicCookieKey,
MagicCookieValue: magicCookieValue,
},
Plugins: plugins,
GRPCServer: plugin.DefaultGRPCServer,
})
return nil
}
package grafana
package transform
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/datasource"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// GRPCClient is an implementation of TransformPluginClient that talks over RPC.
type GRPCClient struct {
broker *plugin.GRPCBroker
client datasource.DatasourcePluginClient
client pluginv2.TransformPluginClient
}
// DatasourcePlugin is the Grafana datasource interface.
type DatasourcePlugin interface {
Query(ctx context.Context, req *datasource.DatasourceRequest, api GrafanaAPI) (*datasource.DatasourceResponse, error)
}
// GrafanaAPI is the Grafana API interface that allows a datasource plugin to callback and request additional information from Grafana.
type GrafanaAPI interface {
QueryDatasource(ctx context.Context, req *datasource.QueryDatasourceRequest) (*datasource.QueryDatasourceResponse, error)
}
func (m *GRPCClient) Query(ctx context.Context, req *datasource.DatasourceRequest, api GrafanaAPI) (*datasource.DatasourceResponse, error) {
func (m *GRPCClient) Transform(ctx context.Context, req *pluginv2.TransformRequest, api GrafanaAPI) (*pluginv2.TransformResponse, error) {
apiServer := &GRPCGrafanaAPIServer{Impl: api}
var s *grpc.Server
serverFunc := func(opts []grpc.ServerOption) *grpc.Server {
s = grpc.NewServer(opts...)
datasource.RegisterGrafanaAPIServer(s, apiServer)
pluginv2.RegisterGrafanaAPIServer(s, apiServer)
return s
}
brokeId := m.broker.NextId()
go m.broker.AcceptAndServe(brokeId, serverFunc)
brokeID := m.broker.NextId()
go m.broker.AcceptAndServe(brokeID, serverFunc)
req.RequestId = brokeId
res, err := m.client.Query(ctx, req)
req.RequestId = brokeID
res, err := m.client.Transform(ctx, req)
s.Stop()
return res, err
}
type grpcServer struct {
// GRPCServer is the gRPC server that GRPCClient talks to.
type GRPCServer struct {
broker *plugin.GRPCBroker
Impl datasourcePluginWrapper
Impl transformPluginWrapper
}
func (m *grpcServer) Query(ctx context.Context, req *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) {
func (m *GRPCServer) Transform(ctx context.Context, req *pluginv2.TransformRequest) (*pluginv2.TransformResponse, error) {
conn, err := m.broker.Dial(req.RequestId)
if err != nil {
return nil, err
}
defer conn.Close()
api := &GRPCGrafanaAPIClient{datasource.NewGrafanaAPIClient(conn)}
return m.Impl.Query(ctx, req, api)
api := &GRPCGrafanaAPIClient{pluginv2.NewGrafanaAPIClient(conn)}
return m.Impl.Transform(ctx, req, api)
}
// GRPCGrafanaAPIClient is an implementation of GrafanaAPIClient that talks over RPC.
type GRPCGrafanaAPIClient struct{ client datasource.GrafanaAPIClient }
type GRPCGrafanaAPIClient struct{ client pluginv2.GrafanaAPIClient }
func (m *GRPCGrafanaAPIClient) QueryDatasource(ctx context.Context, req *datasource.QueryDatasourceRequest) (*datasource.QueryDatasourceResponse, error) {
func (m *GRPCGrafanaAPIClient) QueryDatasource(ctx context.Context, req *pluginv2.QueryDatasourceRequest) (*pluginv2.QueryDatasourceResponse, error) {
resp, err := m.client.QueryDatasource(ctx, req)
if err != nil {
hclog.Default().Info("grafana.QueryDatasource", "client", "start", "err", err)
......@@ -77,7 +69,7 @@ type GRPCGrafanaAPIServer struct {
Impl GrafanaAPI
}
func (m *GRPCGrafanaAPIServer) QueryDatasource(ctx context.Context, req *datasource.QueryDatasourceRequest) (*datasource.QueryDatasourceResponse, error) {
func (m *GRPCGrafanaAPIServer) QueryDatasource(ctx context.Context, req *pluginv2.QueryDatasourceRequest) (*pluginv2.QueryDatasourceResponse, error) {
resp, err := m.Impl.QueryDatasource(ctx, req)
if err != nil {
return nil, err
......
package transform
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
plugin "github.com/hashicorp/go-plugin"
"google.golang.org/grpc"
)
// GrafanaAPI is the Grafana API interface that allows a datasource plugin to callback and request additional information from Grafana.
type GrafanaAPI interface {
QueryDatasource(ctx context.Context, req *pluginv2.QueryDatasourceRequest) (*pluginv2.QueryDatasourceResponse, error)
}
// TransformPlugin is the Grafana transform plugin interface.
type TransformPlugin interface {
Transform(ctx context.Context, req *pluginv2.TransformRequest, api GrafanaAPI) (*pluginv2.TransformResponse, error)
}
// TransformPluginImpl implements the plugin interface from github.com/hashicorp/go-plugin.
type TransformPluginImpl struct {
plugin.NetRPCUnsupportedPlugin
Impl transformPluginWrapper
}
// GRPCServer implements the server for a TransformPlugin
func (p *TransformPluginImpl) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
pluginv2.RegisterTransformPluginServer(s, &GRPCServer{
Impl: p.Impl,
broker: broker,
})
return nil
}
// GRPCClient implements the client for a TransformPlugin
func (p *TransformPluginImpl) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCClient{client: pluginv2.NewTransformPluginClient(c), broker: broker}, nil
}
var _ plugin.GRPCPlugin = &TransformPluginImpl{}
package transform
import (
"github.com/grafana/grafana-plugin-sdk-go/common"
plugin "github.com/hashicorp/go-plugin"
)
// Serve starts serving the datasource plugin over gRPC.
//
// The plugin ID should be in the format <org>-<name>-datasource.
func Serve(pluginID string, handler TransformHandler) error {
versionedPlugins := map[int]plugin.PluginSet{
common.ProtocolVersion: {
pluginID: &TransformPluginImpl{
Impl: transformPluginWrapper{
handler: handler,
},
},
},
}
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: common.Handshake,
VersionedPlugins: versionedPlugins,
GRPCServer: plugin.DefaultGRPCServer,
})
return nil
}
package transform
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/dataframe"
"github.com/grafana/grafana-plugin-sdk-go/datasource"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/hashicorp/go-plugin"
)
// Query represents the query as sent from the frontend.
type Query struct {
RefID string
MaxDataPoints int64
Interval time.Duration
ModelJSON json.RawMessage
}
// QueryResult holds the results for a given query.
type QueryResult struct {
Error string
RefID string
MetaJSON string
DataFrames []*dataframe.Frame
}
// TransformHandler handles data source queries.
// Note: Arguments are sdk.Datasource objects
type TransformHandler interface {
Transform(ctx context.Context, tr datasource.TimeRange, ds datasource.DataSourceInfo, queries []Query, api GrafanaAPIHandler) ([]QueryResult, error)
}
// transformPluginWrapper converts protobuf types to sdk go types.
// This allows consumers to use the TransformHandler interface which uses sdk types instead of
// the generated protobuf types. Protobuf requests are coverted to SDK requests, and the SDK response
// are converted to protobuf response.
type transformPluginWrapper struct {
plugin.NetRPCUnsupportedPlugin
handler TransformHandler
}
// Transform ....
func (p *transformPluginWrapper) Transform(ctx context.Context, req *pluginv2.TransformRequest, api GrafanaAPI) (*pluginv2.TransformResponse, error) {
// Create an SDK request from the protobuf request
tr := datasource.TimeRange{
From: time.Unix(0, req.TimeRange.FromEpochMs*int64(time.Millisecond)),
To: time.Unix(0, req.TimeRange.ToEpochMs*int64(time.Millisecond)),
}
dsi := datasource.DataSourceInfo{
ID: req.Datasource.Id,
OrgID: req.Datasource.OrgId,
Name: req.Datasource.Name,
Type: req.Datasource.Type,
URL: req.Datasource.Url,
JSONData: json.RawMessage(req.Datasource.JsonData),
}
var queries []Query
for _, q := range req.Queries {
queries = append(queries, Query{
RefID: q.RefId,
MaxDataPoints: q.MaxDataPoints,
Interval: time.Duration(q.IntervalMs) * time.Millisecond,
ModelJSON: []byte(q.ModelJson),
})
}
// Makes SDK request, get SDK response
results, err := p.handler.Transform(ctx, tr, dsi, queries, &grafanaAPIWrapper{api: api})
if err != nil {
return nil, err
}
if len(results) == 0 {
return &pluginv2.TransformResponse{
Results: []*pluginv2.TransformResult{},
}, nil
}
// Convert SDK response to protobuf response
var respResults []*pluginv2.TransformResult
for _, res := range results {
encodedFrames := make([][]byte, len(res.DataFrames))
for dfIdx, df := range res.DataFrames {
if len(df.Fields) == 0 {
continue
}
encodedFrames[dfIdx], err = dataframe.MarshalArrow(df)
if err != nil {
return nil, err
}
}
transResult := &pluginv2.TransformResult{
Error: res.Error,
RefId: res.RefID,
MetaJson: res.MetaJSON,
Dataframes: encodedFrames,
}
respResults = append(respResults, transResult)
}
return &pluginv2.TransformResponse{
Results: respResults,
}, nil
}
// GrafanaAPIHandler handles querying other data sources from the transform plugin.
type GrafanaAPIHandler interface {
QueryDatasource(ctx context.Context, orgID int64, datasourceID int64, tr datasource.TimeRange, queries []datasource.Query) ([]datasource.DatasourceQueryResult, error)
}
// grafanaAPIWrapper converts protobuf types to sdk go types - allowing consumers to use the GrafanaAPIHandler interface.
// This allows consumers to use the GrafanaAPIHandler interface which uses sdk types instead of
// the generated protobuf types. SDK requests are turned into protobuf requests, and the protobuf responses are turned
// into SDK responses. Note: (This is a mirror of the converion that happens on the TransformHandler).
type grafanaAPIWrapper struct {
api GrafanaAPI
}
func (w *grafanaAPIWrapper) QueryDatasource(ctx context.Context, orgID int64, datasourceID int64, tr datasource.TimeRange, queries []datasource.Query) ([]datasource.DatasourceQueryResult, error) {
// Create protobuf requests from SDK requests
rawQueries := make([]*pluginv2.DatasourceQuery, 0, len(queries))
for _, q := range queries {
rawQueries = append(rawQueries, &pluginv2.DatasourceQuery{
RefId: q.RefID,
MaxDataPoints: q.MaxDataPoints,
IntervalMs: q.Interval.Milliseconds(),
ModelJson: string(q.ModelJSON),
})
}
rawResp, err := w.api.QueryDatasource(ctx, &pluginv2.QueryDatasourceRequest{
OrgId: orgID,
DatasourceId: datasourceID,
TimeRange: &pluginv2.TimeRange{
FromEpochMs: tr.From.UnixNano() / 1e6,
ToEpochMs: tr.To.UnixNano() / 1e6,
FromRaw: fmt.Sprintf("%v", tr.From.UnixNano()/1e6),
ToRaw: fmt.Sprintf("%v", tr.To.UnixNano()/1e6),
},
Queries: rawQueries,
})
if err != nil {
return nil, err
}
// Convert protobuf responses to SDK responses
results := make([]datasource.DatasourceQueryResult, len(rawResp.GetResults()))
for resIdx, rawRes := range rawResp.GetResults() {
// TODO Error property etc
dfs := make([]*dataframe.Frame, len(rawRes.Dataframes))
for dfIdx, b := range rawRes.Dataframes {
dfs[dfIdx], err = dataframe.UnMarshalArrow(b)
if err != nil {
return nil, err
}
}
results[resIdx] = datasource.DatasourceQueryResult{
DataFrames: dfs,
}
}
return results, nil
}
......@@ -131,10 +131,12 @@ github.com/gosimple/slug
# github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
github.com/grafana/grafana-plugin-model/go/datasource
github.com/grafana/grafana-plugin-model/go/renderer
# github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191024130641-6756418f682c
github.com/grafana/grafana-plugin-sdk-go
# github.com/grafana/grafana-plugin-sdk-go v0.0.0-20191029155514-4d93894a3f7a
github.com/grafana/grafana-plugin-sdk-go/common
github.com/grafana/grafana-plugin-sdk-go/dataframe
github.com/grafana/grafana-plugin-sdk-go/genproto/datasource
github.com/grafana/grafana-plugin-sdk-go/datasource
github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2
github.com/grafana/grafana-plugin-sdk-go/transform
# github.com/hashicorp/go-hclog v0.8.0
github.com/hashicorp/go-hclog
# github.com/hashicorp/go-plugin v1.0.1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment