mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 15:42:19 +08:00
Docs: Plugins doc reorganization, part 1 (#69864)
* Initial commit * Prettier fixes * Doc-validator fixes part 1 * Doc-validator fixes part 2 * More doc-validator fixes * More doc-validator fixes * Test * link test * Linnk test * Link test * More fixes * More fixes * Doc-validator fixes * Doc-validator fixes * fix broken link * Fix * Testing * Doc fixes * Link fixes * Fix links * Update docs/sources/developers/plugins/create-a-grafana-plugin/_index.md Co-authored-by: David Harris <david.harris@grafana.com> * Testing * Testing * Testing * Testing * Doc-validator fixes * Doc-validator fixes * Doc-validator fixes * Fix broken links for plugins reorganization project * Prettier fixes * Prettier fixes * Incorporate reviewer feedback * Link fixes * Link fixes * Link fixes * Link fix * Deleted space * Codeowners fix * Change grafana.com links to absolute URLs for Hugo --------- Co-authored-by: David Harris <david.harris@grafana.com>
This commit is contained in:
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Develop a Grafana plugin
|
||||
menuTitle: Develop a plugin
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- development
|
||||
- documentation
|
||||
description: An index of how-to topics for Grafana plugin development.
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# Develop a Grafana plugin
|
||||
|
||||
This section contains how-to topics for developing Grafana plugins:
|
||||
|
||||
- [Build a panel plugin]({{< relref "./build-a-panel-plugin.md" >}})
|
||||
- [Build a panel plugin with d3.js]({{< relref "./build-a-panel-plugin-with-d3.md" >}})
|
||||
- [Build a data source plugin]({{< relref "./build-a-data-source-plugin.md" >}})
|
||||
- [Build a data source backend plugin]({{< relref "./build-a-data-source-backend-plugin.md" >}})
|
||||
- [Build a logs data source plugin]({{< relref "./build-a-logs-data-source-plugin.md" >}})
|
||||
- [Build a streaming data source plugin]({{< relref "./build-a-streaming-data-source-plugin.md" >}})
|
||||
- [Work with data frames]({{< relref "./working-with-data-frames.md" >}})
|
||||
|
||||
Additional resources:
|
||||
|
||||
- [Build a Grafana plugin with the create-plugin tool](https://grafana.github.io/plugin-tools/docs/creating-a-plugin)
|
@ -0,0 +1,184 @@
|
||||
---
|
||||
title: Build a data source backend plugin
|
||||
description: Create a backend for your data source plugin.
|
||||
weight: 400
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- backend
|
||||
- backend data source
|
||||
- datasource
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source.
|
||||
|
||||
For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/).
|
||||
|
||||
In this tutorial, you'll:
|
||||
|
||||
- Build a backend for your data source
|
||||
- Implement a health check for your data source
|
||||
- Enable Grafana Alerting for your data source
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Knowledge about how data sources are implemented in the frontend.
|
||||
- Grafana 7.0
|
||||
- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3))
|
||||
- [Mage](https://magefile.org/)
|
||||
- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66))
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go]({{< relref "../../introduction-to-plugin-development/backend/grafana-plugin-sdk-for-go" >}}).
|
||||
|
||||
The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type:
|
||||
|
||||
```
|
||||
npx @grafana/create-plugin@latest
|
||||
```
|
||||
|
||||
Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin.
|
||||
|
||||
```bash
|
||||
cd my-plugin
|
||||
```
|
||||
|
||||
Install frontend dependencies and build frontend parts of the plugin to _dist_ directory:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
Run the following to update [Grafana plugin SDK for Go]({{< relref "../../introduction-to-plugin-development/backend/grafana-plugin-sdk-for-go" >}}) dependency to the latest minor version:
|
||||
|
||||
```bash
|
||||
go get -u github.com/grafana/grafana-plugin-sdk-go
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory:
|
||||
|
||||
```bash
|
||||
mage -v
|
||||
```
|
||||
|
||||
Now, let's verify that the plugin you've built so far can be used in Grafana when creating a new data source:
|
||||
|
||||
1. Restart your Grafana instance.
|
||||
1. Open Grafana in your web browser.
|
||||
1. Navigate via the side-menu to **Configuration** -> **Data Sources**.
|
||||
1. Click **Add data source**.
|
||||
1. Find your newly created plugin and select it.
|
||||
1. Enter a name and then click **Save & Test** (ignore any errors reported for now).
|
||||
|
||||
You now have a new data source instance of your plugin that is ready to use in a dashboard:
|
||||
|
||||
1. Navigate via the side-menu to **Create** -> **Dashboard**.
|
||||
1. Click **Add new panel**.
|
||||
1. In the query tab, select the data source you just created.
|
||||
1. A line graph is rendered with one series consisting of two data points.
|
||||
1. Save the dashboard.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Grafana doesn't load my plugin
|
||||
|
||||
By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to
|
||||
configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins).
|
||||
For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins).
|
||||
|
||||
## Anatomy of a backend plugin
|
||||
|
||||
The folders and files used to build the backend for the data source are:
|
||||
|
||||
| file/folder | description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Magefile.go` | It’s not a requirement to use mage build files, but we strongly recommend using it so that you can use the build targets provided by the plugin SDK. |
|
||||
| `/go.mod ` | Go modules dependencies, [reference](https://golang.org/cmd/go/#hdr-The_go_mod_file) |
|
||||
| `/src/plugin.json` | A JSON file describing the backend plugin |
|
||||
| `/pkg/main.go` | Starting point of the plugin binary. |
|
||||
|
||||
#### plugin.json
|
||||
|
||||
The [plugin.json](/docs/grafana/latest/developers/plugins/metadata/) file is required for all plugins. When building a backend plugin these properties are important:
|
||||
|
||||
| property | description |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| backend | Should be set to `true` for backend plugins. This tells Grafana that it should start a binary when loading the plugin. |
|
||||
| executable | This is the name of the executable that Grafana expects to start, see [plugin.json reference](/docs/grafana/latest/developers/plugins/metadata/) for details. |
|
||||
| alerting | Should be set to `true` if your backend datasource supports alerting. |
|
||||
|
||||
In the next step we will look at the query endpoint!
|
||||
|
||||
## Implement data queries
|
||||
|
||||
We begin by opening the file `/pkg/plugin/plugin.go`. In this file you will see the `SampleDatasource` struct which implements the [backend.QueryDataHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#QueryDataHandler) interface. The `QueryData` method on this struct is where the data fetching happens for a data source plugin.
|
||||
|
||||
Each request contains multiple queries to reduce traffic between Grafana and plugins. So you need to loop over the slice of queries, process each query, and then return the results of all queries.
|
||||
|
||||
In the tutorial we have extracted a method named `query` to take care of each query model. Since each plugin has their own unique query model, Grafana sends it to the backend plugin as JSON. Therefore the plugin needs to `Unmarshal` the query model into something easier to work with.
|
||||
|
||||
As you can see the sample only returns static numbers. Try to extend the plugin to return other types of data.
|
||||
|
||||
You can read more about how to [build data frames in our docs](/docs/grafana/latest/developers/plugins/data-frames/).
|
||||
|
||||
## Add support for health checks
|
||||
|
||||
Implementing the health check handler allows Grafana to verify that a data source has been configured correctly.
|
||||
|
||||
When editing a data source in Grafana's UI, you can **Save & Test** to verify that it works as expected.
|
||||
|
||||
In this sample data source, there is a 50% chance that the health check will be successful. Make sure to return appropriate error messages to the users, informing them about what is misconfigured in the data source.
|
||||
|
||||
Open `/pkg/plugin/plugin.go`. In this file you'll see that the `SampleDatasource` struct also implements the [backend.CheckHealthHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#CheckHealthHandler) interface. Navigate to the `CheckHealth` method to see how the health check for this sample plugin is implemented.
|
||||
|
||||
## Add authentication
|
||||
|
||||
Implementing authentication allows your plugin to access protected resources like databases or APIs. To learn more about how to authenticate using a backend plugin, refer to [our documentation]({{< relref "../extend-a-plugin/add-authentication-for-data-source-plugins/#authenticate-using-a-backend-plugin" >}}).
|
||||
|
||||
## Enable Grafana Alerting
|
||||
|
||||
1. Open _src/plugin.json_.
|
||||
1. Add the top level `backend` property with a value of `true` to specify that your plugin supports Grafana Alerting, e.g.
|
||||
```json
|
||||
{
|
||||
...
|
||||
"backend": true,
|
||||
"executable": "gpx_simple_datasource_backend",
|
||||
"alerting": true,
|
||||
"info": {
|
||||
...
|
||||
}
|
||||
```
|
||||
1. Rebuild frontend parts of the plugin to _dist_ directory:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
1. Restart your Grafana instance.
|
||||
1. Open Grafana in your web browser.
|
||||
1. Open the dashboard you created earlier in the _Create a new plugin_ step.
|
||||
1. Edit the existing panel.
|
||||
1. Click on the _Alert_ tab.
|
||||
1. Click on _Create Alert_ button.
|
||||
1. Edit condition and specify _IS ABOVE 10_. Change _Evaluate every_ to _10s_ and clear the _For_ field to make the alert rule evaluate quickly.
|
||||
1. Save the dashboard.
|
||||
1. After some time the alert rule evaluates and transitions into _Alerting_ state.
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you created a backend for your data source plugin.
|
@ -0,0 +1,376 @@
|
||||
---
|
||||
title: Build a data source plugin
|
||||
description: Create a plugin to add support for your own data sources.
|
||||
weight: 300
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- data source
|
||||
- datasource
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source.
|
||||
|
||||
In this tutorial, you'll:
|
||||
|
||||
- Build a data source to visualize a sine wave
|
||||
- Construct queries using the query editor
|
||||
- Configure your data source using the config editor
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana >=7.0
|
||||
- NodeJS >=14
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
To learn how to create a backend data source plugin, see [Build a data source backend plugin]({{< relref "./build-a-data-source-backend-plugin.md" >}})
|
||||
|
||||
## Anatomy of a plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Data source plugins
|
||||
|
||||
A data source in Grafana must extend the `DataSourceApi` interface, which requires you to define two methods: `query` and `testDatasource`.
|
||||
|
||||
### The `query` method
|
||||
|
||||
The `query` method is the heart of any data source plugin. It accepts a query from the user, retrieves the data from an external database, and returns the data in a format that Grafana recognizes.
|
||||
|
||||
```
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse>
|
||||
```
|
||||
|
||||
The `options` object contains the queries, or _targets_, that the user made, along with context information, like the current time interval. Use this information to query an external database.
|
||||
|
||||
> The term _target_ originates from Graphite, and the earlier days of Grafana when Graphite was the only supported data source. As Grafana gained support for more data sources, the term "target" became synonymous with any type of query.
|
||||
|
||||
### Test your data source
|
||||
|
||||
`testDatasource` implements a health check for your data source. For example, Grafana calls this method whenever the user clicks the **Save & Test** button, after changing the connection settings.
|
||||
|
||||
```
|
||||
async testDatasource()
|
||||
```
|
||||
|
||||
## Data frames
|
||||
|
||||
Nowadays there are countless different databases, each with their own ways of querying data. To be able to support all the different data formats, Grafana consolidates the data into a unified data structure called _data frames_.
|
||||
|
||||
Let's see how to create and return a data frame from the `query` method. In this step, you'll change the code in the starter plugin to return a [sine wave](https://en.wikipedia.org/wiki/Sine_wave).
|
||||
|
||||
1. In the current `query` method, remove the code inside the `map` function.
|
||||
|
||||
The `query` method now look like this:
|
||||
|
||||
```ts
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
|
||||
const { range } = options;
|
||||
const from = range!.from.valueOf();
|
||||
const to = range!.to.valueOf();
|
||||
|
||||
const data = options.targets.map(target => {
|
||||
// Your code goes here.
|
||||
});
|
||||
|
||||
return { data };
|
||||
}
|
||||
```
|
||||
|
||||
1. In the `map` function, use the `lodash/defaults` package to set default values for query properties that haven't been set:
|
||||
|
||||
```ts
|
||||
const query = defaults(target, defaultQuery);
|
||||
```
|
||||
|
||||
1. Create a default query at the top of datasource.ts:
|
||||
|
||||
```ts
|
||||
export const defaultQuery: Partial<MyQuery> = {
|
||||
constant: 6.5,
|
||||
};
|
||||
```
|
||||
|
||||
1. Create a data frame with a time field and a number field:
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'value', type: FieldType.number },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
`refId` needs to be set to tell Grafana which query that generated this date frame.
|
||||
|
||||
Next, we'll add the actual values to the data frame. Don't worry about the math used to calculate the values.
|
||||
|
||||
1. Create a couple of helper variables:
|
||||
|
||||
```ts
|
||||
// duration of the time range, in milliseconds.
|
||||
const duration = to - from;
|
||||
|
||||
// step determines how close in time (ms) the points will be to each other.
|
||||
const step = duration / 1000;
|
||||
```
|
||||
|
||||
1. Add the values to the data frame:
|
||||
|
||||
```ts
|
||||
for (let t = 0; t < duration; t += step) {
|
||||
frame.add({ time: from + t, value: Math.sin((2 * Math.PI * t) / duration) });
|
||||
}
|
||||
```
|
||||
|
||||
The `frame.add()` accepts an object where the keys corresponds to the name of each field in the data frame.
|
||||
|
||||
1. Return the data frame:
|
||||
|
||||
```ts
|
||||
return frame;
|
||||
```
|
||||
|
||||
1. Rebuild the plugin and try it out.
|
||||
|
||||
Your data source is now sending data frames that Grafana can visualize. Next, we'll look at how you can control the frequency of the sine wave by defining a _query_.
|
||||
|
||||
> In this example, we're generating timestamps from the current time range. This means that you'll get the same graph no matter what time range you're using. In practice, you'd instead use the timestamps returned by your database.
|
||||
|
||||
## Define a query
|
||||
|
||||
Most data sources offer a way to query specific data. MySQL and PostgreSQL use SQL, while Prometheus has its own query language, called _PromQL_. No matter what query language your databases are using, Grafana lets you build support for it.
|
||||
|
||||
Add support for custom queries to your data source, by implementing your own _query editor_, a React component that enables users to build their own queries, through a user-friendly graphical interface.
|
||||
|
||||
A query editor can be as simple as a text field where the user edits the raw query text, or it can provide a more user-friendly form with drop-down menus and switches, that later gets converted into the raw query text before it gets sent off to the database.
|
||||
|
||||
### Define the query model
|
||||
|
||||
The first step in designing your query editor is to define its _query model_. The query model defines the user input to your data source.
|
||||
|
||||
We want to be able to control the frequency of the sine wave, so let's add another property.
|
||||
|
||||
1. Add a new number property called `frequency` to the query model:
|
||||
|
||||
**src/types.ts**
|
||||
|
||||
```ts
|
||||
export interface MyQuery extends DataQuery {
|
||||
queryText?: string;
|
||||
constant: number;
|
||||
frequency: number;
|
||||
}
|
||||
```
|
||||
|
||||
1. Set a default value to the new `frequency` property:
|
||||
|
||||
```ts
|
||||
export const defaultQuery: Partial<MyQuery> = {
|
||||
constant: 6.5,
|
||||
frequency: 1.0,
|
||||
};
|
||||
```
|
||||
|
||||
### Bind the model to a form
|
||||
|
||||
Now that you've defined the query model you wish to support, the next step is to bind the model to a form. The `FormField` is a text field component from `grafana/ui` that lets you register a listener which will be invoked whenever the form field value changes.
|
||||
|
||||
1. Define the `frequency` from the `query` object and add a new form field to the query editor to control the new frequency property in the `render` method.
|
||||
|
||||
**QueryEditor.tsx**
|
||||
|
||||
```ts
|
||||
const { queryText, constant, frequency } = query;
|
||||
```
|
||||
|
||||
```ts
|
||||
<InlineField label="Frequency" labelWidth={16}>
|
||||
<Input onChange={onFrequencyChange} value={frequency} />
|
||||
</InlineField>
|
||||
```
|
||||
|
||||
1. Add a event listener for the new property.
|
||||
|
||||
```ts
|
||||
const onFrequencyChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({ ...query, frequency: parseFloat(event.target.value) });
|
||||
// executes the query
|
||||
onRunQuery();
|
||||
};
|
||||
```
|
||||
|
||||
The registered listener, `onFrequencyChange`, calls `onChange` to update the current query with the value from the form field.
|
||||
|
||||
`onRunQuery();` tells Grafana to run the query after each change. For fast queries, this is recommended to provide a more responsive experience.
|
||||
|
||||
### Use the property
|
||||
|
||||
The new query model is now ready to use in our `query` method.
|
||||
|
||||
1. In the `query` method, use the `frequency` property to adjust our equation.
|
||||
|
||||
```ts
|
||||
frame.add({ time: from + t, value: Math.sin((2 * Math.PI * query.frequency * t) / duration) });
|
||||
```
|
||||
|
||||
## Configure your data source
|
||||
|
||||
To access a specific data source, you often need to configure things like hostname, credentials, or authentication method. A _config editor_ lets your users configure your data source plugin to fit their needs.
|
||||
|
||||
The config editor looks similar to the query editor, in that it defines a model and binds it to a form.
|
||||
|
||||
Since we're not actually connecting to an external database in our sine wave example, we don't really need many options. To show you how you can add an option however, we're going to add the _wave resolution_ as an option.
|
||||
|
||||
The resolution controls how close in time the data points are to each other. A higher resolution means more points closer together, at the cost of more data being processed.
|
||||
|
||||
### Define the options model
|
||||
|
||||
1. Add a new number property called `resolution` to the options model.
|
||||
|
||||
**types.ts**
|
||||
|
||||
```ts
|
||||
export interface MyDataSourceOptions extends DataSourceJsonData {
|
||||
path?: string;
|
||||
resolution?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Bind the model to a form
|
||||
|
||||
Just like query editor, the form field in the config editor calls the registered listener whenever the value changes.
|
||||
|
||||
1. Add a new form field to the query editor to control the new resolution option.
|
||||
|
||||
**ConfigEditor.tsx**
|
||||
|
||||
```ts
|
||||
<InlineField label="Resolution" labelWidth={12}>
|
||||
<Input onChange={onResolutionChange} value={jsonData.resolution || ''} placeholder="Enter a number" width={40} />
|
||||
</InlineField>
|
||||
```
|
||||
|
||||
1. Add a event listener for the new option.
|
||||
|
||||
```ts
|
||||
const onResolutionChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const jsonData = {
|
||||
...options.jsonData,
|
||||
resolution: parseFloat(event.target.value),
|
||||
};
|
||||
onOptionsChange({ ...options, jsonData });
|
||||
};
|
||||
```
|
||||
|
||||
The `onResolutionChange` listener calls `onOptionsChange` to update the current options with the value from the form field.
|
||||
|
||||
### Use the option
|
||||
|
||||
1. Create a property called `resolution` to the `DataSource` class.
|
||||
|
||||
```ts
|
||||
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
|
||||
resolution: number;
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
|
||||
super(instanceSettings);
|
||||
|
||||
this.resolution = instanceSettings.jsonData.resolution || 1000.0;
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
1. In the `query` method, use the `resolution` property to calculate the step size.
|
||||
|
||||
**src/datasource.ts**
|
||||
|
||||
```ts
|
||||
const step = duration / this.resolution;
|
||||
```
|
||||
|
||||
## Get data from an external API
|
||||
|
||||
So far, you've generated the data returned by the data source. A more realistic use case would be to fetch data from an external API.
|
||||
|
||||
While you can use something like [axios](https://github.com/axios/axios) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make requests, we recommend using the [`getBackendSrv` function](https://github.com/grafana/grafana/blob/main/packages/grafana-runtime/src/services/backendSrv.ts) from the [`grafana-runtime` package](https://github.com/grafana/grafana/tree/main/packages/grafana-runtime).
|
||||
|
||||
The main advantage of `getBackendSrv` is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins]({{< relref "../extend-a-plugin/add-authentication-for-data-source-plugins.md" >}}).
|
||||
|
||||
1. Import `getBackendSrv`.
|
||||
|
||||
**src/datasource.ts**
|
||||
|
||||
```ts
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
```
|
||||
|
||||
1. Create a helper method `doRequest` and use the `datasourceRequest` method to make a request to your API. Replace `https://api.example.com/metrics` to point to your own API endpoint.
|
||||
|
||||
```ts
|
||||
async doRequest(query: MyQuery) {
|
||||
const result = await getBackendSrv().datasourceRequest({
|
||||
method: "GET",
|
||||
url: "https://api.example.com/metrics",
|
||||
params: query,
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
1. Make a request for each query. `Promises.all` waits for all requests to finish before returning the data.
|
||||
|
||||
```ts
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
|
||||
const promises = options.targets.map((query) =>
|
||||
this.doRequest(query).then((response) => {
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: "Time", type: FieldType.time },
|
||||
{ name: "Value", type: FieldType.number },
|
||||
],
|
||||
});
|
||||
|
||||
response.data.forEach((point: any) => {
|
||||
frame.appendRow([point.time, point.value]);
|
||||
});
|
||||
|
||||
return frame;
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.all(promises).then((data) => ({ data }));
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you built a complete data source plugin for Grafana that uses a query editor to control what data to visualize. You've added a data source option, commonly used to set connection options and more.
|
||||
|
||||
### Learn more
|
||||
|
||||
Learn how you can improve your plugin even further, by reading our advanced guides:
|
||||
|
||||
- [Add support for variables](/docs/grafana/latest/developers/plugins/add-support-for-variables/)
|
||||
- [Add support for annotations](/docs/grafana/latest/developers/plugins/add-support-for-annotations/)
|
||||
- [Add support for Explore queries](/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/)
|
||||
- [Build a logs data source](/docs/grafana/latest/developers/plugins/build-a-logs-data-source-plugin/)
|
@ -0,0 +1,159 @@
|
||||
---
|
||||
title: Build a logs data source plugin
|
||||
description: How to build a logs data source plugin.
|
||||
aliases:
|
||||
- ../../../plugins/build-a-logs-data-source-plugin/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- logs
|
||||
- logs data source
|
||||
- datasource
|
||||
weight: 500
|
||||
---
|
||||
|
||||
# Build a logs data source plugin
|
||||
|
||||
Grafana data source plugins support metrics, logs, and other data types. The steps to build a logs data source plugin are largely the same as for a metrics data source, but there are a few differences which we will explain in this guide.
|
||||
|
||||
## Before you begin
|
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "./build-a-data-source-plugin" >}}) for metrics. We recommend that you review this material before continuing.
|
||||
|
||||
## Add logs support to your data source
|
||||
|
||||
To add logs support to an existing data source, you need to:
|
||||
|
||||
1. Enable logs support
|
||||
1. Construct the log data
|
||||
|
||||
When these steps are done, then you can improve the user experience with one or more [optional features](#enhance-your-logs-data-source-plugin-with-optional-features).
|
||||
|
||||
### Step 1: Enable logs support
|
||||
|
||||
Tell Grafana that your data source plugin can return log data, by adding `"logs": true` to the [plugin.json]({{< relref "../../metadata.md" >}}) file.
|
||||
|
||||
```json
|
||||
{
|
||||
"logs": true
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Construct the log data
|
||||
|
||||
As it does with metrics data, Grafana expects your plugin to return log data as a [data frame]({{< relref "../../introduction-to-plugin-development/data-frames.md" >}}).
|
||||
|
||||
To return log data, return a data frame with at least one time field and one text field from the data source's `query` method.
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'content', type: FieldType.string },
|
||||
],
|
||||
});
|
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered' });
|
||||
frame.add({ time: 1589189406480, content: 'user logged in' });
|
||||
```
|
||||
|
||||
That's all you need to start returning log data from your data source. Go ahead and try it out in [Explore]({{< relref "../../../../explore" >}}) or by adding a [Logs panel]({{< relref "../../../../panels-visualizations/visualizations/logs" >}}).
|
||||
|
||||
Congratulations, you just wrote your first logs data source plugin! Next, let's look at a couple of features that can further improve the experience for the user.
|
||||
|
||||
## Enhance your logs data source plugin with optional features
|
||||
|
||||
Add visualization type hints, labels, and other optional features to logs.
|
||||
|
||||
### Add a preferred visualization type hint to the data frame
|
||||
|
||||
To make sure Grafana recognizes data as logs and shows logs visualization automatically in Explore, set `meta.preferredVisualisationType` to `'logs'` in the returned data frame. See [Selecting preferred visualization section]({{< relref "../extend-a-plugin/add-support-for-explore-queries#select-a-preferred-visualization-type" >}})
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
meta: {
|
||||
preferredVisualisationType: 'logs',
|
||||
},
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'content', type: FieldType.string },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Add labels to your logs
|
||||
|
||||
Many log systems let you query logs based on metadata, or _labels_, to help filter log lines.
|
||||
|
||||
Add labels to a stream of logs by setting the `labels` property on the Field.
|
||||
|
||||
**Example**:
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'content', type: FieldType.string, labels: { filename: 'file.txt' } },
|
||||
],
|
||||
});
|
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered' });
|
||||
frame.add({ time: 1589189406480, content: 'user logged in' });
|
||||
```
|
||||
|
||||
### Extract detected fields from your logs
|
||||
|
||||
Add additional information about each log line by supplying more data frame fields.
|
||||
|
||||
If a data frame has more than one text field, then Grafana assumes the first field in the data frame to be the actual log line. Grafana treats subsequent text fields as detected fields.
|
||||
|
||||
Any number of custom fields can be added to your data frame; Grafana comes with two dedicated fields: `levels` and `id`.
|
||||
|
||||
#### Levels
|
||||
|
||||
To set the level for each log line, add a `level` field.
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'content', type: FieldType.string, labels: { filename: 'file.txt' } },
|
||||
{ name: 'level', type: FieldType.string },
|
||||
],
|
||||
});
|
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered', level: 'info' });
|
||||
frame.add({ time: 1589189406480, content: 'unknown error', level: 'error' });
|
||||
```
|
||||
|
||||
#### 'id' for assigning unique identifiers to log lines
|
||||
|
||||
By default, Grafana offers basic support for deduplicating log lines. You can improve the support by adding an `id` field to explicitly assign identifiers to each log line.
|
||||
|
||||
**Example:**
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'content', type: FieldType.string, labels: { filename: 'file.txt' } },
|
||||
{ name: 'level', type: FieldType.string },
|
||||
{ name: 'id', type: FieldType.string },
|
||||
],
|
||||
});
|
||||
|
||||
frame.add({ time: 1589189388597, content: 'user registered', level: 'info', id: 'd3b07384d113edec49eaa6238ad5ff00' });
|
||||
frame.add({ time: 1589189406480, content: 'unknown error', level: 'error', id: 'c157a79031e1c40f85931829bc5fc552' });
|
||||
```
|
@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Build a panel plugin with D3.js
|
||||
description: how to use D3.js in your panel plugins.
|
||||
weight: 200
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- d3js
|
||||
- d3
|
||||
- panel
|
||||
- panel plugin
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using [D3.js](https://d3js.org/).
|
||||
|
||||
For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/features/panels/panels/).
|
||||
|
||||
In this tutorial, you'll:
|
||||
|
||||
- Build a simple panel plugin to visualize a bar chart.
|
||||
- Learn how to use D3.js to build a panel using data-driven transformations.
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana 7.0
|
||||
- NodeJS 12.x
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Data-driven documents
|
||||
|
||||
[D3.js](https://d3js.org/) is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations.
|
||||
|
||||
Wait a minute. Manipulating documents based on data? That's sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let's see how you can create an SVG from data, using only React.
|
||||
|
||||
In **SimplePanel.tsx**, change `SimplePanel` to return an `svg` with a `rect` element.
|
||||
|
||||
```ts
|
||||
export const SimplePanel = ({ options, data, width, height }: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<rect x={0} y={0} width={10} height={10} fill={theme.palette.greenBase} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
One single rectangle might not be very exciting, so let's see how you can create rectangles from data.
|
||||
|
||||
1. Create some data that we can visualize.
|
||||
|
||||
```ts
|
||||
const values = [4, 8, 15, 16, 23, 42];
|
||||
```
|
||||
|
||||
1. Calculate the height of each bar based on the height of the panel.
|
||||
|
||||
```ts
|
||||
const barHeight = height / values.length;
|
||||
```
|
||||
|
||||
1. Inside a SVG group, `g`, create a `rect` element for every value in the dataset. Each rectangle uses the value as its width.
|
||||
|
||||
```ts
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g>
|
||||
{values.map((value, i) => (
|
||||
<rect x={0} y={i * barHeight} width={value} height={barHeight - 1} fill={theme.palette.greenBase} />
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
```
|
||||
|
||||
1. Rebuild the plugin and reload your browser to see the changes you've made.
|
||||
|
||||
As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3.
|
||||
|
||||
So why would you use even use D3? In the next step, we'll see how you can take advantage of D3's data transformations.
|
||||
|
||||
## Transform data using D3.js
|
||||
|
||||
In this step, you'll see how you can transform data using D3 before rendering it using React.
|
||||
|
||||
D3 is already bundled with Grafana, and you can access it by importing the `d3` package. However, we're going to need the type definitions while developing.
|
||||
|
||||
1. Install the D3 type definitions:
|
||||
|
||||
```bash
|
||||
yarn add --dev @types/d3
|
||||
```
|
||||
|
||||
1. Import `d3` in **SimplePanel.tsx**.
|
||||
|
||||
```ts
|
||||
import * as d3 from 'd3';
|
||||
```
|
||||
|
||||
In the previous step, we had to define the width of each bar in pixels. Instead, let's use _scales_ from the D3 library to make the width of each bar depend on the width of the panel.
|
||||
|
||||
Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel.
|
||||
|
||||
1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We'll be using this to calculate the width of the bar.
|
||||
|
||||
```ts
|
||||
const scale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, d3.max(values) || 0.0])
|
||||
.range([0, width]);
|
||||
```
|
||||
|
||||
1. Pass the value to the scale function to calculate the width of the bar in pixels.
|
||||
|
||||
```ts
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g>
|
||||
{values.map((value, i) => (
|
||||
<rect x={0} y={i * barHeight} width={scale(value)} height={barHeight - 1} fill={theme.palette.greenBase} />
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
```
|
||||
|
||||
As you can see, even if we're using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it.
|
||||
|
||||
## Add an axis
|
||||
|
||||
Another useful tool in the D3 toolbox is the ability to generate _axes_. Adding axes to our chart makes it easier for the user to understand the differences between each bar.
|
||||
|
||||
Let's see how you can use D3 to add a horizontal axis to your bar chart.
|
||||
|
||||
1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis.
|
||||
|
||||
```ts
|
||||
const axis = d3.axisBottom(scale);
|
||||
```
|
||||
|
||||
1. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a `ref` to a group element `g`.
|
||||
|
||||
```ts
|
||||
<g
|
||||
ref={(node) => {
|
||||
d3.select(node).call(axis as any);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
By default, the axis renders at the top of the SVG element. We'd like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar.
|
||||
|
||||
1. Calculate the new bar height based on the padded height.
|
||||
|
||||
```ts
|
||||
const padding = 20;
|
||||
const chartHeight = height - padding;
|
||||
const barHeight = chartHeight / values.length;
|
||||
```
|
||||
|
||||
1. Translate the axis by adding a transform to the `g` element.
|
||||
|
||||
```ts
|
||||
<g
|
||||
transform={`translate(0, ${chartHeight})`}
|
||||
ref={(node) => {
|
||||
d3.select(node).call(axis as any);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Congrats! You've created a simple and responsive bar chart.
|
||||
|
||||
## Complete example
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { SimpleOptions } from 'types';
|
||||
import { useTheme } from '@grafana/ui';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
interface Props extends PanelProps<SimpleOptions> {}
|
||||
|
||||
export const SimplePanel = ({ options, data, width, height }: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const values = [4, 8, 15, 16, 23, 42];
|
||||
|
||||
const scale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, d3.max(values) || 0.0])
|
||||
.range([0, width]);
|
||||
|
||||
const axis = d3.axisBottom(scale);
|
||||
|
||||
const padding = 20;
|
||||
const chartHeight = height - padding;
|
||||
const barHeight = chartHeight / values.length;
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g>
|
||||
{values.map((value, i) => (
|
||||
<rect x={0} y={i * barHeight} width={scale(value)} height={barHeight - 1} fill={theme.palette.greenBase} />
|
||||
))}
|
||||
</g>
|
||||
<g
|
||||
transform={`translate(0, ${chartHeight})`}
|
||||
ref={(node) => {
|
||||
d3.select(node).call(axis as any);
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you built a panel plugin with D3.js.
|
@ -0,0 +1,260 @@
|
||||
---
|
||||
title: Build a panel plugin
|
||||
description: Learn how to create a custom visualization for your dashboards.
|
||||
weight: 100
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- visualization
|
||||
- custom visualization
|
||||
- dashboard
|
||||
- dashboards
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Panels are the building blocks of Grafana. They allow you to visualize data in different ways. While Grafana has several types of panels already built-in, you can also build your own panel, to add support for other visualizations.
|
||||
|
||||
For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/panels/).
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana >=7.0
|
||||
- NodeJS >=14
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Anatomy of a plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Panel plugins
|
||||
|
||||
Since Grafana 6.x, panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html).
|
||||
|
||||
Prior to Grafana 6.0, plugins were written in [AngularJS](https://angular.io/). Even though we still support plugins written in AngularJS, we highly recommend that you write new plugins using ReactJS.
|
||||
|
||||
### Panel properties
|
||||
|
||||
The [PanelProps](https://github.com/grafana/grafana/blob/747b546c260f9a448e2cb56319f796d0301f4bb9/packages/grafana-data/src/types/panel.ts#L27-L40) interface exposes runtime information about the panel, such as panel dimensions, and the current time range.
|
||||
|
||||
You can access the panel properties through `props`, as seen in your plugin.
|
||||
|
||||
**src/SimplePanel.tsx**
|
||||
|
||||
```js
|
||||
const { options, data, width, height } = props;
|
||||
```
|
||||
|
||||
### Development workflow
|
||||
|
||||
Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made.
|
||||
|
||||
First, you need to add your panel to a dashboard:
|
||||
|
||||
1. Open Grafana in your browser.
|
||||
1. Create a new dashboard, and add a new panel.
|
||||
1. Select your panel from the list of visualization types.
|
||||
1. Save the dashboard.
|
||||
|
||||
Now that you can view your panel, try making a change to the panel plugin:
|
||||
|
||||
1. In `SimplePanel.tsx`, change the fill color of the circle.
|
||||
1. Run `yarn dev` to build the plugin.
|
||||
1. In the browser, reload Grafana with the new changes.
|
||||
|
||||
## Add panel options
|
||||
|
||||
Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring _panel options_ for your plugin, your panel will be able to accept user input.
|
||||
|
||||
In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor.
|
||||
|
||||
#### Add an option
|
||||
|
||||
Panel options are defined in a _panel options object_. `SimpleOptions` is an interface that describes the options object.
|
||||
|
||||
1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from:
|
||||
|
||||
```
|
||||
type CircleColor = 'red' | 'green' | 'blue';
|
||||
```
|
||||
|
||||
1. In the `SimpleOptions` interface, add a new option called `color`:
|
||||
|
||||
```
|
||||
color: CircleColor;
|
||||
```
|
||||
|
||||
Here's the updated options definition:
|
||||
|
||||
**src/types.ts**
|
||||
|
||||
```ts
|
||||
type SeriesSize = 'sm' | 'md' | 'lg';
|
||||
type CircleColor = 'red' | 'green' | 'blue';
|
||||
|
||||
// interface defining panel options type
|
||||
export interface SimpleOptions {
|
||||
text: string;
|
||||
showSeriesCount: boolean;
|
||||
seriesCountSize: SeriesSize;
|
||||
color: CircleColor;
|
||||
}
|
||||
```
|
||||
|
||||
#### Add an option control
|
||||
|
||||
To change the option from the panel editor, you need to bind the `color` option to an _option control_.
|
||||
|
||||
Grafana supports a range of option controls, such as text inputs, switches, and radio groups.
|
||||
|
||||
Let's create a radio control and bind it to the `color` option.
|
||||
|
||||
1. In `src/module.ts`, add the control at the end of the builder:
|
||||
|
||||
```ts
|
||||
.addRadio({
|
||||
path: 'color',
|
||||
name: 'Circle color',
|
||||
defaultValue: 'red',
|
||||
settings: {
|
||||
options: [
|
||||
{
|
||||
value: 'red',
|
||||
label: 'Red',
|
||||
},
|
||||
{
|
||||
value: 'green',
|
||||
label: 'Green',
|
||||
},
|
||||
{
|
||||
value: 'blue',
|
||||
label: 'Blue',
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`.
|
||||
|
||||
Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section.
|
||||
|
||||
#### Use the new option
|
||||
|
||||
You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that.
|
||||
|
||||
1. To convert option value to the colors used by the current theme, add a `switch` statement right before the `return` statement in `SimplePanel.tsx`.
|
||||
|
||||
**src/SimplePanel.tsx**
|
||||
|
||||
```ts
|
||||
let color: string;
|
||||
switch (options.color) {
|
||||
case 'red':
|
||||
color = theme.palette.redBase;
|
||||
break;
|
||||
case 'green':
|
||||
color = theme.palette.greenBase;
|
||||
break;
|
||||
case 'blue':
|
||||
color = theme.palette.blue95;
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
1. Configure the circle to use the color.
|
||||
|
||||
```ts
|
||||
<g>
|
||||
<circle style={{ fill: color }} r={100} />
|
||||
</g>
|
||||
```
|
||||
|
||||
Now, when you change the color in the panel editor, the fill color of the circle changes as well.
|
||||
|
||||
## Create dynamic panels using data frames
|
||||
|
||||
Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series.
|
||||
|
||||
> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData](/docs/grafana/latest/features/datasources/testdata) data source while developing.
|
||||
|
||||
The results from a data source query within your panel are available in the `data` property inside your panel component.
|
||||
|
||||
```ts
|
||||
const { data } = props;
|
||||
```
|
||||
|
||||
`data.series` contains the series returned from a data source query. Each series is represented as a data structure called _data frame_. A data frame resembles a table, where data is stored by columns, or _fields_, instead of rows. Every value in a field share the same data type, such as string, number, or time.
|
||||
|
||||
Here's an example of a data frame with a time field, `Time`, and a number field, `Value`:
|
||||
|
||||
| Time | Value |
|
||||
| ------------- | ----- |
|
||||
| 1589189388597 | 32.4 |
|
||||
| 1589189406480 | 27.2 |
|
||||
| 1589189513721 | 15.0 |
|
||||
|
||||
Let's see how you can retrieve data from a data frame and use it in your visualization.
|
||||
|
||||
1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement:
|
||||
|
||||
```ts
|
||||
const radii = data.series
|
||||
.map((series) => series.fields.find((field) => field.type === 'number'))
|
||||
.map((field) => field?.values.get(field.values.length - 1));
|
||||
```
|
||||
|
||||
`radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle.
|
||||
|
||||
1. Change the `svg` element to the following:
|
||||
|
||||
```ts
|
||||
<svg
|
||||
className={styles.svg}
|
||||
width={width}
|
||||
height={height}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox={`0 -${height / 2} ${width} ${height}`}
|
||||
>
|
||||
<g fill={color}>
|
||||
{radii.map((radius, index) => {
|
||||
const step = width / radii.length;
|
||||
return <circle r={radius} transform={`translate(${index * step + step / 2}, 0)`} />;
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Note how we're creating a `<circle>` element for each value in `radii`:
|
||||
|
||||
```ts
|
||||
{
|
||||
radii.map((radius, index) => {
|
||||
const step = width / radii.length;
|
||||
return <circle r={radius} transform={`translate(${index * step + step / 2}, 0)`} />;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
We use the `transform` here to distribute the circle horizontally within the panel.
|
||||
|
||||
1. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard.
|
||||
|
||||
If you want to know more about data frames, check out our introduction to [Data frames](/docs/grafana/latest/developers/plugins/data-frames/).
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you learned how to create a custom visualization for your dashboards.
|
@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Build a streaming data source plugin
|
||||
aliases:
|
||||
- ../../../plugins/build-a-streaming-data-source-plugin/
|
||||
description: How to build a streaming data source plugin.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- streaming
|
||||
- streaming data source
|
||||
- datasource
|
||||
weight: 600
|
||||
---
|
||||
|
||||
# Build a streaming data source plugin
|
||||
|
||||
In Grafana, you can set your dashboards to automatically refresh at a certain interval, no matter what data source you use. Unfortunately, this means that your queries are requesting all the data to be sent again, regardless of whether the data has actually changed. Adding streaming to a plugin helps reduce queries so your dashboard is only updated when new data becomes available.
|
||||
|
||||
## Before you begin
|
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "./build-a-data-source-plugin" >}})
|
||||
|
||||
Grafana uses [RxJS](https://rxjs.dev/) to continuously send data from a data source to a panel visualization.
|
||||
|
||||
> **Note:** To learn more about RxJs, refer to the [RxJS documentation](https://rxjs.dev/guide/overview).
|
||||
|
||||
## Add streaming to your data source
|
||||
|
||||
Enable streaming for your data source plugin to update your dashboard when new data becomes available.
|
||||
|
||||
For example, a streaming data source plugin can connect to a websocket, or subscribe to a message bus, and update the visualization whenever a new message is available.
|
||||
|
||||
### Step 1: Edit the `plugin.json` file
|
||||
|
||||
Enable streaming for your data source in the `plugin.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"streaming": true
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Change the signature of the `query` method
|
||||
|
||||
Modify the signature of the `query` method to return an `Observable` from the `rxjs` package. Make sure you remove the `async` keyword.
|
||||
|
||||
```ts
|
||||
import { Observable } from 'rxjs';
|
||||
```
|
||||
|
||||
```ts
|
||||
query(options: DataQueryRequest<MyQuery>): Observable<DataQueryResponse> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create an `Observable` instance for each query
|
||||
|
||||
Create an `Observable` instance for each query, and then combine them all using the `merge` function from the `rxjs` package.
|
||||
|
||||
```ts
|
||||
import { Observable, merge } from 'rxjs';
|
||||
```
|
||||
|
||||
```ts
|
||||
const observables = options.targets.map((target) => {
|
||||
return new Observable<DataQueryResponse>((subscriber) => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
|
||||
return merge(...observables);
|
||||
```
|
||||
|
||||
### Step 4: Create a `CircularDataFrame` instance
|
||||
|
||||
In the `subscribe` function, create a `CircularDataFrame` instance.
|
||||
|
||||
```ts
|
||||
import { CircularDataFrame } from '@grafana/data';
|
||||
```
|
||||
|
||||
```ts
|
||||
const frame = new CircularDataFrame({
|
||||
append: 'tail',
|
||||
capacity: 1000,
|
||||
});
|
||||
|
||||
frame.refId = query.refId;
|
||||
frame.addField({ name: 'time', type: FieldType.time });
|
||||
frame.addField({ name: 'value', type: FieldType.number });
|
||||
```
|
||||
|
||||
Circular data frames have a limited capacity. When a circular data frame reaches its capacity, the oldest data point is removed.
|
||||
|
||||
### Step 5: Send the updated data frame
|
||||
|
||||
Use `subscriber.next()` to send the updated data frame whenever you receive new updates.
|
||||
|
||||
```ts
|
||||
import { LoadingState } from '@grafana/data';
|
||||
```
|
||||
|
||||
```ts
|
||||
const intervalId = setInterval(() => {
|
||||
frame.add({ time: Date.now(), value: Math.random() });
|
||||
|
||||
subscriber.next({
|
||||
data: [frame],
|
||||
key: query.refId,
|
||||
state: LoadingState.Streaming,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
```
|
||||
|
||||
> **Note:** In practice, you'd call `subscriber.next` as soon as you receive new data from a websocket or a message bus. In the example above, data is being received every 500 milliseconds.
|
||||
|
||||
### Example code for final `query` method
|
||||
|
||||
```ts
|
||||
query(options: DataQueryRequest<MyQuery>): Observable<DataQueryResponse> {
|
||||
const streams = options.targets.map(target => {
|
||||
const query = defaults(target, defaultQuery);
|
||||
|
||||
return new Observable<DataQueryResponse>(subscriber => {
|
||||
const frame = new CircularDataFrame({
|
||||
append: 'tail',
|
||||
capacity: 1000,
|
||||
});
|
||||
|
||||
frame.refId = query.refId;
|
||||
frame.addField({ name: 'time', type: FieldType.time });
|
||||
frame.addField({ name: 'value', type: FieldType.number });
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
frame.add({ time: Date.now(), value: Math.random() });
|
||||
|
||||
subscriber.next({
|
||||
data: [frame],
|
||||
key: query.refId,
|
||||
state: LoadingState.Streaming,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return merge(...streams);
|
||||
}
|
||||
```
|
||||
|
||||
One limitation with this example is that the panel visualization is cleared every time you update the dashboard. If you have access to historical data, you can add it, or _backfill_ it, to the data frame before the first call to `subscriber.next()`.
|
||||
|
||||
For another example of a streaming plugin, refer to the [streaming websocket example](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-streaming-websocket) on GitHub.
|
@ -0,0 +1,207 @@
|
||||
---
|
||||
title: Build an app plugin
|
||||
description: Learn at how to create an app for Grafana.
|
||||
weight: 700
|
||||
draft: true
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- app
|
||||
- app plugin
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also let you create _custom pages_ within Grafana. Custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services over HTTP.
|
||||
|
||||
Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana 7.0
|
||||
- NodeJS 12.x
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Anatomy of a plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}}
|
||||
|
||||
## App plugins
|
||||
|
||||
App plugins let you bundle resources such as dashboards, panels, and data sources into a single plugin.
|
||||
|
||||
Any resource you want to include needs to be added to the `includes` property in the `plugin.json` file. To add a resource to your app plugin, you need to include it to the `plugin.json`.
|
||||
|
||||
Plugins that are included in an app plugin are available like any other plugin.
|
||||
|
||||
Dashboards and pages can be added to the app menu by setting `addToNav` to `true`.
|
||||
|
||||
By setting `"defaultNav": true`, users can navigate to the dashboard by clicking the app icon in the side menu.
|
||||
|
||||
## Add a custom page
|
||||
|
||||
App plugins let you extend the Grafana user interface through the use of _custom pages_.
|
||||
|
||||
Any requests sent to `/a/<plugin-id>`, e.g. `/a/myorgid-simple-app/`, are routed to the _root page_ of the app plugin. The root page is a React component that returns the content for a given route.
|
||||
|
||||
While you're free to implement your own routing, in this tutorial you'll use a tab-based navigation page that you can use by calling `onNavChange`.
|
||||
|
||||
Let's add a tab for managing server instances.
|
||||
|
||||
1. In the `src/pages` directory, add a new file called `Instances.tsx`. This component contains the content for the new tab.
|
||||
|
||||
```ts
|
||||
import { AppRootProps } from '@grafana/data';
|
||||
import React from 'react';
|
||||
|
||||
export const Instances = ({ query, path, meta }: AppRootProps) => {
|
||||
return <p>Hello</p>;
|
||||
};
|
||||
```
|
||||
|
||||
1. Register the page by adding it to the `pages` array in `src/pages/index.ts`.
|
||||
|
||||
**index.ts**
|
||||
|
||||
```ts
|
||||
import { Instances } from './Instances';
|
||||
```
|
||||
|
||||
```ts
|
||||
{
|
||||
component: Instances,
|
||||
icon: 'file-alt',
|
||||
id: 'instances',
|
||||
text: 'Instances',
|
||||
}
|
||||
```
|
||||
|
||||
1. Add the page to the app menu, by including it in `plugin.json`. This will be the main view of the app, so we'll set `defaultNav` to let users quickly get to it by clicking the app icon in the side menu.
|
||||
|
||||
**plugin.json**
|
||||
|
||||
```json
|
||||
"includes": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Instances",
|
||||
"path": "/a/myorgid-simple-app?tab=instances",
|
||||
"role": "Viewer",
|
||||
"addToNav": true,
|
||||
"defaultNav": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> **Note:** While `page` includes typically reference pages created by the app, you can set `path` to any URL, internal or external. Try setting `path` to `https://grafana.com`.
|
||||
|
||||
## Configure the app
|
||||
|
||||
Let's add a new configuration page where users are able to configure default zone and regions for any instances they create.
|
||||
|
||||
1. In `module.ts`, add new configuration page using the `addConfigPage` method. `body` is the React component that renders the page content.
|
||||
|
||||
**module.ts**
|
||||
|
||||
```ts
|
||||
.addConfigPage({
|
||||
title: 'Defaults',
|
||||
icon: 'fa fa-info',
|
||||
body: DefaultsConfigPage,
|
||||
id: 'defaults',
|
||||
})
|
||||
```
|
||||
|
||||
## Add a dashboard
|
||||
|
||||
#### Include a dashboard in your app
|
||||
|
||||
1. In `src/`, create a new directory called `dashboards`.
|
||||
1. Create a file called `overview.json` in the `dashboards` directory.
|
||||
1. Copy the JSON definition for the dashboard you want to include and paste it into `overview.json`. If you don't have one available, you can find a sample dashboard at the end of this step.
|
||||
1. In `plugin.json`, add the following object to the `includes` property.
|
||||
|
||||
- The `name` of the dashboard needs to be the same as the `title` in the dashboard JSON model.
|
||||
- `path` points out the file that contains the dashboard definition, relative to the `plugin.json` file.
|
||||
|
||||
```json
|
||||
"includes": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "System overview",
|
||||
"path": "dashboards/overview.json",
|
||||
"addToNav": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
1. Save and restart Grafana to load the new changes.
|
||||
|
||||
## Bundle a plugin
|
||||
|
||||
An app plugin can contain panel and data source plugins that get installed along with the app plugin.
|
||||
|
||||
In this step, you'll add a data source to your app plugin. You can add panel plugins the same way by changing `datasource` to `panel`.
|
||||
|
||||
1. In `src/`, create a new directory called `datasources`.
|
||||
1. Create a new data source using Grafana create-plugin tool in a temporary directory.
|
||||
|
||||
```bash
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
npx @grafana/create-plugin@latest
|
||||
```
|
||||
|
||||
1. Move the `src` directory in the data source plugin to `src/datasources`, and rename it to `my-datasource`.
|
||||
|
||||
```bash
|
||||
mv ./my-datasource/src ../src/datasources/my-datasource
|
||||
```
|
||||
|
||||
Any bundled plugins are built along with the app plugin. Grafana looks for any subdirectory containing a `plugin.json` file and attempts to load a plugin in that directory.
|
||||
|
||||
To let users know that your plugin bundles other plugins, you can optionally display it on the plugin configuration page. This is not done automatically, so you need to add it to the `plugin.json`.
|
||||
|
||||
1. Include the data source in the `plugin.json`. The `name` property is only used for displaying in the Grafana UI.
|
||||
|
||||
```json
|
||||
"includes": [
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "My data source"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Include external plugins
|
||||
|
||||
If you want to let users know that your app requires an existing plugin, you can add it as a dependency in `plugin.json`. Note that they'll still need to install it themselves.
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"plugins": [
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Worldmap Panel",
|
||||
"id": "grafana-worldmap-panel",
|
||||
"version": "^0.3.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you learned how to create an app plugin.
|
@ -0,0 +1,131 @@
|
||||
---
|
||||
title: Work with data frames
|
||||
aliases:
|
||||
- ../../../plugins/working-with-data-frames/
|
||||
description: How to work with data frames.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- data frames
|
||||
- dataframes
|
||||
weight: 900
|
||||
---
|
||||
|
||||
# Work with data frames
|
||||
|
||||
The [data frame]({{< relref "../../introduction-to-plugin-development/data-frames" >}}) is a columnar data structure that allows for efficient querying of large amounts of data. Since data frames are a central concept when developing plugins for Grafana, in this guide we'll look at some ways you can use them.
|
||||
|
||||
The `DataFrame` interface contains a `name` and an array of `fields` where each field contains the name, type, and the values for the field.
|
||||
|
||||
> **Note:** If you want to migrate an existing plugin to use the data frame format, refer to [Migrate to data frames]({{< relref "../../migration-guide/v6.x-v7.x#migrate-to-data-frames" >}}).
|
||||
|
||||
## Create a data frame
|
||||
|
||||
If you build a data source plugin, then you'll most likely want to convert a response from an external API to a data frame. Let's look at how to do this.
|
||||
|
||||
Let's start with creating a simple data frame that represents a time series. The easiest way to create a data frame is to use the `toDataFrame` function.
|
||||
|
||||
```ts
|
||||
// Need to be of the same length.
|
||||
const timeValues = [1599471973065, 1599471975729];
|
||||
const numberValues = [12.3, 28.6];
|
||||
|
||||
// Create data frame from values.
|
||||
const frame = toDataFrame({
|
||||
name: 'http_requests_total',
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: timeValues },
|
||||
{ name: 'Value', type: FieldType.number, values: numberValues },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
> **Note:** Data frames representing time series contain at least a `time` field and a `number` field. By convention, built-in plugins use `Time` and `Value` as field names for data frames containing time series data.
|
||||
|
||||
As you can see from the example, to create data frames like this, your data must already be stored as columnar data. If you already have the records in the form of an array of objects, then you can pass it to `toDataFrame`. In this case, `toDataFrame` tries to guess the schema based on the types and names of the objects in the array. To create complex data frames this way, be sure to verify that you get the schema you expect.
|
||||
|
||||
```ts
|
||||
const series = [
|
||||
{ Time: 1599471973065, Value: 12.3 },
|
||||
{ Time: 1599471975729, Value: 28.6 },
|
||||
];
|
||||
|
||||
const frame = toDataFrame(series);
|
||||
frame.name = 'http_requests_total';
|
||||
```
|
||||
|
||||
## Read values from a data frame
|
||||
|
||||
When you're building a panel plugin, the data frames returned by the data source are available from the `data` prop in your panel component.
|
||||
|
||||
```ts
|
||||
function SimplePanel({ data: Props }) {
|
||||
const frame = data.series[0];
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Before you start reading the data, think about what data you expect. For example, to visualize a time series you need at least one time field and one number field.
|
||||
|
||||
```ts
|
||||
const timeField = frame.fields.find((field) => field.type === FieldType.time);
|
||||
const valueField = frame.fields.find((field) => field.type === FieldType.number);
|
||||
```
|
||||
|
||||
Other types of visualizations might need multiple dimensions. For example, a bubble chart that uses three numeric fields: the X-axis, Y-axis, and one for the radius of each bubble. In this case, instead of hard coding the field names, we recommend that you let the user choose the field to use for each dimension.
|
||||
|
||||
```ts
|
||||
const x = frame.fields.find((field) => field.name === xField);
|
||||
const y = frame.fields.find((field) => field.name === yField);
|
||||
const size = frame.fields.find((field) => field.name === sizeField);
|
||||
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
const row = [x?.values[i], y?.values[i], size?.values[i]];
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use the `DataFrameView`, which gives you an array of objects that contain a property for each field in the frame.
|
||||
|
||||
```ts
|
||||
const view = new DataFrameView(frame);
|
||||
|
||||
view.forEach((row) => {
|
||||
console.log(row[options.xField], row[options.yField], row[options.sizeField]);
|
||||
});
|
||||
```
|
||||
|
||||
## Display values from a data frame
|
||||
|
||||
Field options let the user control how Grafana displays the data in a data frame.
|
||||
|
||||
To apply the field options to a value, use the `display` method on the corresponding field. The result contains information such as the color and suffix to use when display the value.
|
||||
|
||||
```ts
|
||||
const valueField = frame.fields.find((field) => field.type === FieldType.number);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{valueField
|
||||
? valueField.values.map((value) => {
|
||||
const displayValue = valueField.display!(value);
|
||||
return (
|
||||
<p style={{ color: displayValue.color }}>
|
||||
{displayValue.text} {displayValue.suffix ? displayValue.suffix : ''}
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
To apply field options to the name of a field, use `getFieldDisplayName`.
|
||||
|
||||
```ts
|
||||
const valueField = frame.fields.find((field) => field.type === FieldType.number);
|
||||
const valueFieldName = getFieldDisplayName(valueField, frame);
|
||||
```
|
Reference in New Issue
Block a user