Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: Add/update resource handler topics for datasource and app plugins #1081

Merged
merged 7 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
id: add-resource-handler
title: Add resource handler for app plugins
description: Learn how to add a resource handler for app plugins.
keywords:
- grafana
- plugins
- plugin
- app
- resource
- resource handler
---

import ImplementResourceHandler from '@shared/implement-resource-handler.md';

# Add resource handler for app plugins

You can add a resource handler to your app backend to extend the Grafana HTTP API with your own app-specific routes. This guide explains why you may want to add [resource](../../key-concepts/backend-plugins/#resources) handlers and some common ways for doing so.

## Uses of resource handlers

An app often integrates with a HTTP service of some kind, e.g. a 3rd party service, to retrieve and send data. For example, this service might have specific authentication and authorization needs or a response format not suitable to return to Grafana and the plugin frontend.

In addition, you might want to [secure your resources](implement-rbac-in-app-plugins.md#secure-backend-resources) so that only users with a certain permission can access certain routes.

Resource handlers are also useful for building control panels that allow the user to write back to the app. For example, you could add a resource handler to update the state of an IoT device.

<ImplementResourceHandler />

## Accessing app resources

Once implemented you can access the resources using the Grafana HTTP API and from the frontend.

### Using the Grafana HTTP API

You can access the resources through the Grafana HTTP API by using the endpoint, `http://<GRAFANA_HOSTNAME>:<PORT>/api/plugins/<PLUGIN_ID>/resources{/<RESOURCE>}`. The `PLUGIN_ID` is the plugin identifier that uniquely identifies your app and the `RESOURCE` depends on how the resource handler is implemented and what resources (routes) are supported.

With the above example you can access the following resources:

- HTTP GET `http://<GRAFANA_HOSTNAME>:<PORT>/api/plugins/<PLUGIN_ID>/resources/namespaces`
- HTTP GET `http://<GRAFANA_HOSTNAME>:<PORT>/api/plugins/<PLUGIN_ID>/resources/projects`

### From the frontend

You can access your resources in a compontent using the `get` function of the `backendSrv` runtime service to send a HTTP GET request to `http://<GRAFANA_HOSTNAME>:<PORT>/api/plugins/<PLUGIN_ID>/resources/namespaces`

```typescript
import { getBackendSrv } from '@grafana/runtime';

const namespaces = await getBackendSrv().get(`/api/plugins/<PLUGIN_ID>/resources/namespaces`);
marefr marked this conversation as resolved.
Show resolved Hide resolved
```

## Additional examples

Some other examples of using resource handlers and the [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) package:

- The [app-with-backend](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-backend) example:
- [create resource handler](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/app-with-backend/pkg/plugin/app.go) and [register routes](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/app-with-backend/pkg/plugin/resources.go) in the backend.
- use [backendSrv](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/app-with-backend/src/pages/PageOne/PageOne.tsx) to call resources.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ keywords:
- resource handler
---

import ImplementResourceHandler from '@shared/implement-resource-handler.md';

# Add resource handler for data source plugins

You can add a resource handler to your data source backend to extend the Grafana HTTP API with your own data source-specific routes. This guide explains why you may want to add [resource](../../key-concepts/backend-plugins/#resources) handlers and some common ways for doing so.
Expand All @@ -22,141 +24,51 @@ The primary way for a data source to retrieve data from a backend is through the

Resource handlers are also useful for building control panels that allow the user to write back to the data source. For example, you could add a resource handler to update the state of an IoT device.

## Implement the resource handler interface
<ImplementResourceHandler />

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

```go
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!"),
})
}
```
Once implemented you can access the resources using the Grafana HTTP API and from the frontend.

### Using the Grafana HTTP API

You can then access your resources through the following endpoint: `http://<GRAFANA_HOSTNAME>:<PORT>/api/datasources/uid/<DATASOURCE_UID>/resources`
You can access the resources through the Grafana HTTP API by using the endpoint, `http://<GRAFANA_HOSTNAME>:<PORT>/api/datasources/uid/<DATASOURCE_UID>/resources{/<RESOURCE>}`. The `DATASOURCE_UID` is the data source unique identifier (UID) that uniquely identifies your data source and the `RESOURCE` depends on how the resource handler is implemented and what resources (routes) are supported.

In this example code, `DATASOURCE_UID` is the data source unique identifier (UID) that uniquely identifies your data source.
With the above example you can access the following resources:

- HTTP GET `http://<GRAFANA_HOSTNAME>:<PORT>/api/datasources/uid/<DATASOURCE_UID>/resources/namespaces`
- HTTP GET `http://<GRAFANA_HOSTNAME>:<PORT>/api/datasources/uid/<DATASOURCE_UID>/resources/projects`

:::tip

To verify the data source UID, you can enter `window.grafanaBootData.settings.datasources` in your browser's developer tools console, to list all the configured data sources in your Grafana instance.

:::

## Add support for multiple routes

To support multiple routes in your data source plugin, you can use a switch with the `req.Path`:

```go
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,
})
}
}
```

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

## Advanced use cases

If you have some more advanced use cases or want to use a more Go-agnostic approach for handling resources, you can use the regular [`http.Handler`](https://pkg.go.dev/net/http#Handler). You can do so by using a package provided by the [Grafana Plugin SDK for Go](../../key-concepts/backend-plugins/grafana-plugin-sdk-for-go) named [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter). This package provides support for handling resource calls using an [`http.Handler`](https://pkg.go.dev/net/http#Handler).

Using [`http.Handler`](https://pkg.go.dev/net/http#Handler) allows you to also use Go’s built-in router functionality called [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) or your preferred HTTP router library (for example, [`gorilla/mux`](https://github.com/gorilla/mux)).

An alternative to using the `CallResource` method shown in the [above example](#implement-the-resource-handler-interface) is to use [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) and [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) as shown below:
### From the frontend

```go
package mydatasource
You can query your resources using the `getResource` and `postResource` helpers from the `DataSourceWithBackend` class. To provide a nicer and more convenient API for your components it's recommended to extend your datasource class and instance with functions for each route as shown in the following example:

import (
"context"
"net/http"
```typescript
export class MyDataSource extends DataSourceWithBackend<MyQuery, MyDataSourceOptions> {
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
super(instanceSettings);
}

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
)
getNamespaces(): Promise<NamespacesResponse> {
return this.getResource('/namespaces');
}

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)
getProjects(): Promise<ProjectsResponse> {
return this.getResource('/projects');
}
}
```

:::note
For example, in your query editor component, you can access the data source instance from the `props` object and use `getNamespaces` to send a HTTP GET request to `http://<GRAFANA_HOSTNAME>:<PORT>/api/datasources/uid/<DATASOURCE_UID>/resources/namespaces`:

Using some other HTTP router library with above example should be straightforward. Just replace the use of [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) with another router.

:::

### What if you need access to the backend plugin context?

Use the `PluginConfigFromContext` function to access `backend.PluginContext`:

```go
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)
}
```typescript
const namespaces = await props.datasource.getNamespaces();
```

## Additional examples
Expand Down
112 changes: 112 additions & 0 deletions docusaurus/docs/shared/implement-resource-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
## Implement the resource handler interface

To add a resource handler to your backend plugin, you need to implement the `backend.CallResourceHandler` interface.

There are two ways you can implement this in your plugin, [using the `httpadapter` package](#using-the-httpadapter-package) or [manually implementing it](#manually-implementing-backendcallresourcehandler) in your plugin.

### Using the `httpadapter` package

The [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) package provided by the [Grafana Plugin SDK for Go](../../key-concepts/backend-plugins/grafana-plugin-sdk-for-go) is the recommended way for handling resources. This package provides support for handling resource calls using using the [`http.Handler`](https://pkg.go.dev/net/http#Handler) interface and allows responding to HTTP requests in a more Go-agnostic way and makes it easier to support multiple routes and methods (GET, POST etc).

Using [`http.Handler`](https://pkg.go.dev/net/http#Handler) allows you to also use Go’s built-in router functionality called [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) or your preferred HTTP router library (for example, [`gorilla/mux`](https://github.com/gorilla/mux)).

:::note

Go 1.22 [includes routing enhancement](https://go.dev/blog/routing-enhancements) that adds support for method matching and wildcards using the [`ServeMux`](https://pkg.go.dev/net/http#ServeMux).

:::

In the following example we demonstrate using the `httpadapter` package, `ServeMux` and `http.Handler` to add support for retrieving namespaces (`/namespaces`), projects (`/projects`) and updating the state of some device (`/device`) :

```go
package myplugin

import (
"context"
"net/http"

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

type MyPlugin struct {
resourceHandler backend.CallResourceHandler
}

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

func (p *MyPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return p.resourceHandler.CallResource(ctx, req)
marefr marked this conversation as resolved.
Show resolved Hide resolved
}

func (p *MyPlugin) handleNamespaces(rw http.ResponseWriter, req *http.Request) {
rw.Header().Add("Content-Type", "application/json")
_, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`))
if err != nil {
return
}
rw.WriteHeader(http.StatusOK)
}

func (p *MyPlugin) handleProjects(rw http.ResponseWriter, req *http.Request) {
marefr marked this conversation as resolved.
Show resolved Hide resolved
rw.Header().Add("Content-Type", "application/json")
_, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`))
if err != nil {
return
}
rw.WriteHeader(http.StatusOK)
}
```

#### Accessing the backend plugin context

You can use the [backend.PluginConfigFromContext](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend#PluginConfigFromContext) function to access [backend.PluginContext](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend#PluginContext). This holds contextual information about a plugin request, such as the user performing the request:

```go
func (p *MyPlugin) handleSomeRoute(rw http.ResponseWriter, req *http.Request) {
pCtx := backend.PluginConfigFromContext(req.Context())
bytes, err := json.Marshal(pCtx.User)
if err != nil {
return
}

rw.Header().Add("Content-Type", "application/json")
_, err := rw.Write(bytes)
if err != nil {
return
}
rw.WriteHeader(http.StatusOK)
}
```

### Manually implementing `backend.CallResourceHandler`

Manually implementing the `backend.CallResourceHandler` interface might be enough for the basic needs. To support a couple of different routes retrieving data you can use a switch with the `req.Path`:

```go
func (p *MyPlugin) 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,
})
}
}
```
Loading