Backend Plugins

Grafana added support for plugins in Grafana 3.0 and this enabled the Grafana community to create panel plugins and datasource plugins. It was wildly successful and has made Grafana much more useful as you can integrate it with anything and do any type of custom visualization that you want. However, these plugin hooks are on the frontend only and we also want to provide hooks into the Grafana backend to allow the community to extend and improve Grafana in new ways.

Once Grafana introduced the alerting feature, external datasource plugins needed a backend component for the Grafana server to execute queries for evaluating alert rules (as the alerting engine cannot call frontend JavaScript code). So the the obvious first backend plugin type is the Datasource backend plugin and it is a new component for an existing datasource plugin. This new plugin type will enable alerting for external datasource plugins but can also be used for achieving different goals such as query caching, request proxying, custom authentication methods, and more.

Grafana’s Backend Plugin System

The backend plugin feature is implemented with the HashiCorp plugin system which is a Go plugin system over RPC. Grafana server launches each plugin as a subprocess and communicates with it over RPC. This approach has a number of benefits:

  • Plugins can’t crash your grafana process: a panic in a plugin doesn’t panic the server.
  • Plugins are easy to develop: just write a Go application and go build (or use any other language which supports gRPC).
  • Plugins can be relatively secure: The plugin only has access to the interfaces and args given to it, not to the entire memory space of the process.

Datasource Plugin Interface

The plugin interface is very simple and described as a Go interface type in Grafana and as a general RPC service in the corresponding .proto (protocol buffer file):

  1. type TsdbQueryEndpoint interface {
  2. Query(ctx context.Context, ds *models.DataSource, query *TsdbQuery) (*Response, error)
  3. }
  1. service DatasourcePlugin {
  2. rpc Query(DatasourceRequest) returns (DatasourceResponse);
  3. }

Thus, a datasource plugin should only implement the Query() method.

Introduction to building a backend component for a plugin

The Simple JSON backend datasource is a good example of writing a simple backend plugin in Go. Let’s take a look at some key points.

Metadata

The plugin needs to know it has a backend component, this is done in the plugin.json file by setting two fields: backend and executable. If you want to enable alerting for your datasource, set the alerting field to true as well.

  1. {
  2. "id": "grafana-simple-json-backend-datasource",
  3. "name": "Simple Json backend",
  4. "type": "datasource",
  5. "metrics": true,
  6. "annotations": true,
  7. "backend": true,
  8. "alerting": true,
  9. "executable": "simple-json-plugin",
  10. ...
  11. }

executable should be the the the first part of the binary filename. The actual binary filename has 3 possible endings:

  • _linux_amd64
  • _darwin_amd64
  • _windows_amd64.exeWhen Grafana loads the plugin binary, it uses the executable field plus the current OS (Grafana knows which OS it is running on) to load in the correct version of the plugin. So in Simple JSON the executable field is simple-json-plugin and the 3 binaries are named:

  • simple-json-plugin_darwin_amd64

  • simple-json-plugin_linux_amd64
  • simple-json-plugin_windows_amd64.exeThe resulting plugin directory will look like this:
  1. simple-json-backend-datasource/
  2. |-- dist/
  3. | |-- partials/
  4. | |-- module.js
  5. | |-- plugin.json
  6. | |-- simple-json-plugin_linux_amd64
  7. | |-- simple-json-plugin_darwin_amd64
  8. | |-- simple-json-plugin_windows_amd64.exe
  9. ...

Plugin code

A pkg/ directory contains three .go files:

  • plugin.go - an entry point of the plugin. This file would be very similar for your datasource - you just need to change some details like the plugin name etc.
  • datasource.go - contains Query() method implementation and other plugin logic.
  • models.go - types for request and response specific to your datasource.The datasource type is declared in datasource.go:
  1. package main
  2. import (
  3. plugin "github.com/hashicorp/go-plugin"
  4. )
  5. type JsonDatasource struct {
  6. plugin.NetRPCUnsupportedPlugin
  7. }

The only requirement for the plugin type is that it should extend plugin.NetRPCUnsupportedPlugin. You can include more fields into your struct if you want to add some datasource-specific features, like logging, cache etc:

  1. type JsonDatasource struct {
  2. plugin.NetRPCUnsupportedPlugin
  3. logger hclog.Logger
  4. }

The main method you should implement is the Query():

  1. func (t *JsonDatasource) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) {
  2. ...

Request format

In order to call this method from the frontend part of your datasource, use the /api/tsdb/query endpoint:

  1. class SimpleJSONDatasource {
  2. ...
  3. doTsdbRequest(options) {
  4. const tsdbRequest = {
  5. from: options.range.from.valueOf().toString(),
  6. to: options.range.to.valueOf().toString(),
  7. queries: options.targets,
  8. };
  9. return this.backendSrv.datasourceRequest({
  10. url: '/api/tsdb/query',
  11. method: 'POST',
  12. data: tsdbRequest
  13. });
  14. }
  15. }

This endpoint gets data in the following format (see pkg/api/metrics.go and pkg/api/dtos/models.go):

  1. {
  2. from: "1555324640782", // Optional, time range from
  3. to: "1555328240782", // Optional, time range to
  4. queries: [
  5. {
  6. datasourceId: 42, // Required
  7. refId: "A", // Optional, default is "A"
  8. maxDataPoints: 100, // Optional, default is 100
  9. intervalMs: 1000, // Optional, default is 1000
  10. myFieldFoo: "bar", // Any other fields,
  11. myFieldBar: "baz", // defined by user
  12. ...
  13. },
  14. ...
  15. ]
  16. }

There is only one query function but it is possible to move all your queries to the backend. In order to achieve this, you could add a kind of queryType field to your query model and check this type in the backend code. The Stackdriver and Cloudwatch core plugins have examples of supporting multiple types of queries if you need/want to do this:

Response format

Go types for the query response can be found in Grafana tsdb models (pkg/tsdb/models.go) or in the corresponding protocol buffer file (datasource.proto)

  1. // datasource.proto
  2. message DatasourceResponse {
  3. repeated QueryResult results = 1;
  4. }
  5. message QueryResult {
  6. string error = 1;
  7. string refId = 2;
  8. string metaJson = 3;
  9. repeated TimeSeries series = 4;
  10. repeated Table tables = 5;
  11. }
  1. // pkg/tsdb/models.go
  2. type Response struct {
  3. Results map[string]*QueryResult `json:"results"`
  4. Message string `json:"message,omitempty"`
  5. }
  6. type QueryResult struct {
  7. Error error `json:"-"`
  8. ErrorString string `json:"error,omitempty"`
  9. RefId string `json:"refId"`
  10. Meta *simplejson.Json `json:"meta,omitempty"`
  11. Series TimeSeriesSlice `json:"series"`
  12. Tables []*Table `json:"tables"`
  13. }

The resulting JSON response which the frontend will receive looks like this:

  1. results: {
  2. A: {
  3. refId: "A",
  4. series: [
  5. { name: "series_1", points: [...] },
  6. { name: "series_2", points: [...] },
  7. ...
  8. ],
  9. tables: null,
  10. // Request metadata (any arbitrary JSON).
  11. // Optional, empty field will be omitted.
  12. meta: {},
  13. // Error message. Optional, empty field will be omitted.
  14. error: "Request failed",
  15. }
  16. }

Logging

Logs from the plugin will be automatically sent to the Grafana server and will appear in its log flow. Grafana server reads logs from the plugin’s stderr stream, so with the standard log package you have to set output to os.Stderr first:

  1. func main() {
  2. log.SetOutput(os.Stderr)
  3. log.Println("from plugin!")
  4. ...
  5. }

Another option for logging - using go-hclog package:

  1. package main
  2. import (
  3. hclog "github.com/hashicorp/go-hclog"
  4. )
  5. var pluginLogger = hclog.New(&hclog.LoggerOptions{
  6. Name: "simple-json-backend-datasource",
  7. Level: hclog.LevelFromString("DEBUG"),
  8. })
  9. func main() {
  10. pluginLogger.Debug("Running Simple JSON backend datasource")
  11. ...
  12. }

Building the backend binary

Building the binary depends on which OS you are using.

For a Linux distro, the build command would be:

  1. go build -o ./dist/simple-json-plugin_linux_amd64 ./pkg

On Windows, the command would be:

  1. go build -o ./dist/simple-json-plugin_windows_amd64.exe ./pkg

Restart your Grafana server and then check the Grafana logs to make sure your plugin is loaded properly.