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" });
7 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

Excellent tutorial! Thank you!

Do you have a tutorial that shows how to do this on the front-end(React Panel Plugin).

Thank you!

Could you share an example of what part you’d like to do on the frontend?

To POST to a backend resource, you’d write something like this in your panel plugin:

props.datasource.postResource('device', { state: "on" });

Is there anyway to define a resource handler using JavaScript in my frontend plugin. Thanks

I’m curious to know more about your use case. The main purpose of resource handlers is to let your frontend plugin perform operations on the server.

For example, since you can only decrypt secrets on the server, you would add a resource handler to define a server-side function that can access those secrets. Your frontend plugin could then call the resource handler whenever it need to operate on sensitive data.

Because a resource handler in the frontend would have the same access as the code calling it, there would be no need for it.

I am looking for a way to extend the Grafana API with a custom path, which should be accessible from the client. I want to use this API to implement dynamic data links. For instance, I have a table panel which contains an object id coming from a data source and I want the user to be able to access the deep link that points to the source url of that object (a site external to Grafana). I would like to create a data link that passes that id as a query param to the API. The API calculates the redirect url and returns a http/301 with a Location header. The API will prevent me to replicate a lot of variables and logic to every dashboard that needs this functionality, and it will allow you to maintain the business logic in a centralized location (the API).
I have tried to accomplish this using your JSON API datasource plugin and the datasource proxy API, but since the client needs to know the datasource id (which is now deprecated and replaced by uid), you would have to store that id / uid in a variable on every dashboard that uses this functionality. That is all doable, but then I ran into a second issue when using this technique on Grafana instances with multiple organizations. When a user opens a dashboard from another organization in a new tab, the data links in the other tab break, because the data source id / uid does not exist in the last opened organization. Since the data source link is a GET request, you cannot switch organization dynamically by passing the orgId query param, while this does work when linking to another dashboard. The HTTP API needs a PUT to switch organization: Organization HTTP API | Grafana documentation

I was wondering if this resource handler might be able to solve my problem, but since it’s also depending on the data source id / uid, I don’t think it will. Do you have any suggestions?
Btw, we are using your JSON API plugin extensively to query Elasticsearch without time windows (which is applied always under the hood in the official Elasticsearch plugin), so many thanks for your work on that! :heart::wink: