Skip to content

Commit

Permalink
Docs: Add/update resource handler topics for datasource and app plugi…
Browse files Browse the repository at this point in the history
…ns (#1081)
  • Loading branch information
marefr authored Sep 10, 2024
1 parent 006bfe8 commit 5f8096f
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 116 deletions.
59 changes: 59 additions & 0 deletions docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md
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`);
```

## 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)
}

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

0 comments on commit 5f8096f

Please sign in to comment.