How to add a resource handler for your data source

The primary way for a data source to retrieve data is through the query method. But sometimes your data source needs to request auxiliary data on demand, for example to offer auto-completion inside the data source’s query editor.

In this post, I want to show you how to add a resource handler to your data source. By adding a resource handler to your backend plugin, you can extend the Grafana HTTP API with your own data source-specific routes.

Resource handler are great, not only for auto-completion, but for building control panels where the user should be able to write back to the data source. For example, to update the state of an IoT device.

To add a resource handler to your backend plugin, you need to implement the backend.CallResourceHandler interface for your data source struct.

func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
    return sender.Send(&backend.CallResourceResponse{
        Status: http.StatusOK,
        Body: []byte("Hello, world!"),
    })
}

You can then access your resources through the following endpoint:

https://localhost:3000/api/datasources/<DATASOURCE_ID>/resources/
  • DATASOURCE_ID is an integer that uniquely identifies your data source.

Pro tip: To verify the data source ID, you can enter window.grafanaBootData.settings.datasources in the Developer Console, to list the data source definitions in your Grafana instance.

To support multiple routes, you can use a switch with the req.Path:

func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
	switch req.Path {
	case "namespaces":
		return sender.Send(&backend.CallResourceResponse{
			Status: http.StatusOK,
			Body:   []byte(`{ namespaces: ["ns-1", "ns-2"] }`),
		})
	case "projects":
		return sender.Send(&backend.CallResourceResponse{
			Status: http.StatusOK,
			Body:   []byte(`{ projects: ["project-1", "project-2"] }`),
		})
	default:
		return sender.Send(&backend.CallResourceResponse{
			Status: http.StatusNotFound,
		})
	}
}

With this, your plugin now has its own REST API that you can query from your query editor, using BackendSrv.fetch():

const observable = getBackendSrv()
  .fetch({
    url: `/api/datasources/${props.datasource.id}/resources/namespaces`,
  });

const response = await lastValueFrom(observable);
  • props.datasource.id gives you the data source ID of the query that’s being edited.

For more information on how to use BackendSrv.fetch(), check out my previous post.

Adding resource handlers to your backend plugins opens up more opportunities to make your plugin more dynamic.

Are you using resource handlers already, let me know what you’re using them for!

Update (2022-01-17):

You can also query your resources using the getResource and postResource helpers from the DataSourceWithBackend class.

For example, in your query editor component, you can access the data source instance from the props object:

const namespaces = await props.datasource.getResource('namespaces');
props.datasource.postResource('device', { state: "on" });
5 Likes

this is a really cool tip for power users.

loving these how to’s, @marcusolsson! :grafana:

1 Like

If you have some more advanced needs/use case and/or want to use a more Go-agnostic approach for handling resources using regular http.Handler you can use a package provided by the grafana-plugin-sdk-go named httpadapter. This package provides support for handling resource calls using an http.Handler.

What’s interesting with using http.Handler is that you can use that with Go’s builtin router functionality called ServeMux or use your preferred HTTP router library, for example gorilla/mux to name one of the more popular ones.

Given above example of CallResource method we can do the same thing using httpadapter and ServeMux.

package mydatasource

import (
	"context"
	"net/http"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
)

type MyDatasource struct {
	resourceHandler backend.CallResourceHandler
}

func New() *MyDatasource {
	ds := &MyDatasource{}
	mux := http.NewServeMux()
	mux.HandleFunc("/namespaces", ds.handleNamespaces)
	mux.HandleFunc("/projects", ds.handleProjects)
	ds.resourceHandler := httpadapter.New(mux)
	return ds
}

func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
	return d.resourceHandler.CallResource(ctx, req)
}

func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) {
	_, err := rw.Write([]byte(`{ namespaces: ["ns-1", "ns-2"] }`))
	if err != nil {
		return
	}
	rw.WriteHeader(http.StatusOK)
}

func (d *MyDatasource) handleProjects(rw http.ResponseWriter, req *http.Request) {
	_, err := rw.Write([]byte(`{ projects: ["project-1", "project-2"] }`))
	if err != nil {
		return
	}
	rw.WriteHeader(http.StatusOK)
}

Using some other HTTP router library with above example should be straightforward replacing the use of ServeMux.

Some other examples of using the httpadapter package can be found for some of the builtin Grafana datasources:

What if you need access to backend.PluginContext?
Use the PluginConfigFromContext function.

func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) {
	pCtx := httpadapter.PluginConfigFromContext(req.Context())

	bytes, err := json.Marshal(pCtx.User)
	if err != nil {
		rw.WriteHeader(http.StatusInternalServerError)
	}
	
	_, err := rw.Write(bytes)
	if err != nil {
		return
	}
	rw.WriteHeader(http.StatusOK)
}
3 Likes