Skip to content

Commit

Permalink
Alerting support - minimal code changes
Browse files Browse the repository at this point in the history
  • Loading branch information
retrodaredevil committed Jan 12, 2024
1 parent f04e97c commit fbaa309
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 97 deletions.
105 changes: 105 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Development



https://grafana.com/developers/plugin-tools/create-a-plugin/develop-a-plugin/best-practices

https://grafana.com/developers/plugin-tools/tutorials/build-a-data-source-backend-plugin


## Building and Development

### Setup your system

* Web requirements
* Install nvm https://github.com/nvm-sh/nvm#installing-and-updating
* Backend requirements
* Install go https://go.dev/doc/install
* Install Mage https://magefile.org/
* Grafana requirements
* Install Docker: https://docs.docker.com/engine/install/
* `npm run server` command uses `docker compose` to bring up Grafana
* Make sure your user is part of the `docker` group

An example of commands you *could* run.
Customize this setup to your liking.

```shell
# install nvm https://github.com/nvm-sh/nvm#installing-and-updating
nvm install 20

rm -rf /usr/local/go
wget -c https://dl.google.com/go/go1.21.5.linux-amd64.tar.gz -O - | sudo tar -xz -C /usr/local
# /usr/local/go is GOROOT $HOME/go is GOPATH, so add both bins to path
echo 'export PATH="$PATH:/usr/local/go/bin:$HOME/go/bin"' >> ~/.bashrc

cd ~/Documents/Clones
git clone https://github.com/magefile/mage
cd mage
# This will install to GOPATH, which is $HOME/go by default
go run bootstrap.go

# *docker installation not shown*
# *adding $USER to docker group not shown*
```

### Setup build environment

Go setup
```shell
# This will install to your GOPATH, which is $HOME/go by default.
go get -u github.com/grafana/grafana-plugin-sdk-go
go mod tidy
mage -v
mage -l
```

Node setup:

```shell
npm install
```


### Test inside Grafana instance

Note that `npm run server` uses `docker compose` under the hood.

```shell
npm run dev
mage -v build:linux
npm run server
```

### Recommended development environment

You may choose to use VS Code, which has free tools for developing TypeScript and Go code.
IntelliJ IDEA Ultimate is a non-free choice that has support for both TypeScript and Go code.
Alternatively, WebStorm (also not free) covers TypeScript development and GoLand covers Go development.

If you are using IntelliJ IDEA Ultimate, make sure go to "Language & Frameworks > Go Modules" and click "Enable go modules integration".

If you are using VS Code, this is a good read: https://github.com/golang/vscode-go/blob/master/docs/gopath.md


### Common Errors During Development

* `Watchpack Error (watcher): Error: ENOSPC: System limit for number of file watchers reached, watch`
* https://stackoverflow.com/a/55543310/5434860

### Example repos

Some random examples of data source plugin source code

* https://github.com/grafana/grafana-infinity-datasource/tree/main/pkg
* https://github.com/cnosdb/grafana-datasource-plugin/blob/main/cnosdb/pkg/plugin/query_model.go
* https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-http-backend

## Dependency Notes

This section contains notes about dependencies.

* `graphql-ws` is not actually required by us, but this issue is unresolved so that's why we include it
* https://github.com/graphql/graphiql/issues/2405#issuecomment-1469851608 (yes as of writing it says it's closed, but it's not)
* It's not a bad thing that we include this dependency because it gives us a couple of types that we end up using

117 changes: 23 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,109 +6,38 @@ This is a Grafana datasource that aims to make requesting time series data via a
This datasource is similar to https://github.com/fifemon/graphql-datasource, but is not compatible.
This datasource tries to reimagine how GraphQL queries should be made from Grafana.

Requests are made in the backend. Results are consistent between queries and alerting.

## Development
## Uses for Timeseries data

https://grafana.com/developers/plugin-tools/create-a-plugin/develop-a-plugin/best-practices
### Using a field as the display name

https://grafana.com/developers/plugin-tools/tutorials/build-a-data-source-backend-plugin
If you have data that needs to be "grouped by" or "partitioned by", you first need to add "Partition by values"
as a transform and select `packet.identifier.representation` and `packet.identityInfo.displayName` as fields.
Once you do that, you can go into the "Standard options" and find "Display name" and set it to
`${__field.labels["packet.identityInfo.displayName"]}`.

References:

## Building and Development
* [documentation for Display name under standard options](https://grafana.com/docs/grafana/latest/panels-visualizations/configure-standard-options/#display-name).
* [partition by values](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/)
* Note: Partition by values was [added in 9.3](https://grafana.com/docs/grafana/latest/whatsnew/whats-new-in-v9-3/#new-transformation-partition-by-values) ([blog](https://grafana.com/blog/2022/11/29/grafana-9.3-release/))

### Setup your system
## Common errors

* Web requirements
* Install nvm https://github.com/nvm-sh/nvm#installing-and-updating
* Backend requirements
* Install go https://go.dev/doc/install
* Install Mage https://magefile.org/
* Grafana requirements
* Install Docker: https://docs.docker.com/engine/install/
* `npm run server` command uses `docker compose` to bring up Grafana
* Make sure your user is part of the `docker` group
### Alerting Specific Errors

An example of commands you *could* run.
Customize this setup to your liking.

```shell
# install nvm https://github.com/nvm-sh/nvm#installing-and-updating
nvm install 20

rm -rf /usr/local/go
wget -c https://dl.google.com/go/go1.21.5.linux-amd64.tar.gz -O - | sudo tar -xz -C /usr/local
# /usr/local/go is GOROOT $HOME/go is GOPATH, so add both bins to path
echo 'export PATH="$PATH:/usr/local/go/bin:$HOME/go/bin"' >> ~/.bashrc

cd ~/Documents/Clones
git clone https://github.com/magefile/mage
cd mage
# This will install to GOPATH, which is $HOME/go by default
go run bootstrap.go

# *docker installation not shown*
# *adding $USER to docker group not shown*
```

### Setup build environment

Go setup
```shell
# This will install to your GOPATH, which is $HOME/go by default.
go get -u github.com/grafana/grafana-plugin-sdk-go
go mod tidy
mage -v
mage -l
```

Node setup:

```shell
npm install
```


### Test inside Grafana instance

Note that `npm run server` uses `docker compose` under the hood.

```shell
npm run dev
mage -v build:linux
npm run server
```

### Recommended development environment

You may choose to use VS Code, which has free tools for developing TypeScript and Go code.
IntelliJ IDEA Ultimate is a non-free choice that has support for both TypeScript and Go code.
Alternatively, WebStorm (also not free) covers TypeScript development and GoLand covers Go development.

If you are using IntelliJ IDEA Ultimate, make sure go to "Language & Frameworks > Go Modules" and click "Enable go modules integration".

If you are using VS Code, this is a good read: https://github.com/golang/vscode-go/blob/master/docs/gopath.md


### Common Errors During Development

* `Watchpack Error (watcher): Error: ENOSPC: System limit for number of file watchers reached, watch`
* https://stackoverflow.com/a/55543310/5434860

### Example repos

Some random examples of data source plugin source code

* https://github.com/grafana/grafana-infinity-datasource/tree/main/pkg
* https://github.com/cnosdb/grafana-datasource-plugin/blob/main/cnosdb/pkg/plugin/query_model.go

## Dependency Notes

This section contains notes about dependencies.

* `graphql-ws` is not actually required by us, but this issue is unresolved so that's why we include it
* https://github.com/graphql/graphiql/issues/2405#issuecomment-1469851608 (yes as of writing it says it's closed, but it's not)
* It's not a bad thing that we include this dependency because it gives us a couple of types that we end up using
* `Failed to evaluate queries and expressions: input data must be a wide series but got type long (input refid)`
* This error indicates that the query returns more fields than just the time and the datapoint.
* For alerts, the response from the GraphQL query cannot contain more than the time and datapoint. At this time, you cannot use other attributes from the result to filter the data.

## To-Do

* Add metrics to backend component: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-logs-metrics-traces-for-backend-plugins#implement-metrics-in-your-plugin
* Support returning logs data: https://grafana.com/developers/plugin-tools/tutorials/build-a-logs-data-source-plugin
* We could just add `"logs": true` to `plugin.json`, however we need to support the renaming of fields because sometimes the `body` or `timestamp` fields will be nested
* Publish as a plugin
* https://grafana.com/developers/plugin-tools/publish-a-plugin/publish-a-plugin
* https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin#generate-an-access-policy-token
* https://grafana.com/legal/plugins/
* https://grafana.com/developers/plugin-tools/publish-a-plugin/provide-test-environment
2 changes: 2 additions & 0 deletions pkg/plugin/util/parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func ParseData(graphQlResponseData map[string]interface{}, parsingOption Parsing
switch valueValue := value.(type) {
case string:
// TODO allow user to customize time format
// Look at https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats
// and also consider using time.RFC339Nano instead
parsedTime, err := time.Parse(time.RFC3339, valueValue)
if err != nil {
return nil, errors.New(fmt.Sprintf("Time could not be parsed! Time: %s", valueValue))
Expand Down
4 changes: 4 additions & 0 deletions src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ function createFetcher(url: string, withCredentials: boolean, basicAuth?: string
headers['Authorization'] = basicAuth;
}
const backendSrv = getBackendSrv();
// NOTE: getTemplateSrv() is something that is only updated after a query is performed on a Grafana dashboard.
// If you navigate straight to "Alert rules", for example, getTemplateSrv() will not be able to replace $__to and $__from variables.
// This has the implication that the "Execute query" button performs a query with "to" and "from" variables that are unlike what is actually configured.
const templateSrv = getTemplateSrv();
return async (graphQLParams: FetcherParams, opts?: FetcherOpts) => {
const variables = {
Expand All @@ -68,6 +71,7 @@ function createFetcher(url: string, withCredentials: boolean, basicAuth?: string
...graphQLParams,
variables: variables
};
console.log(query);
const observable = backendSrv.fetch({
url,
headers,
Expand Down
15 changes: 13 additions & 2 deletions src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import {CoreApp, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings
import {DataSourceWithBackend, getTemplateSrv} from '@grafana/runtime';
import {Observable} from 'rxjs';

import {DEFAULT_QUERY, getQueryVariablesAsJson, WildGraphQLDataSourceOptions, WildGraphQLMainQuery} from './types';
import {
DEFAULT_ALERTING_QUERY,
DEFAULT_QUERY,
getQueryVariablesAsJson,
WildGraphQLDataSourceOptions,
WildGraphQLMainQuery
} from './types';

export class DataSource extends DataSourceWithBackend<WildGraphQLMainQuery, WildGraphQLDataSourceOptions> {
settings: DataSourceInstanceSettings<WildGraphQLDataSourceOptions>;
Expand All @@ -12,7 +18,12 @@ export class DataSource extends DataSourceWithBackend<WildGraphQLMainQuery, Wild
this.settings = instanceSettings;
}

getDefaultQuery(_: CoreApp): Partial<WildGraphQLMainQuery> {
getDefaultQuery(app: CoreApp): Partial<WildGraphQLMainQuery> {
if (app === CoreApp.CloudAlerting || app === CoreApp.UnifiedAlerting) {
// we have a different default query for alerts because alerts only support returning time and value columns.
// Additional columns in the data frame will return in an "input data must be a wide series" error.
return DEFAULT_ALERTING_QUERY;
}
return DEFAULT_QUERY;
}
query(request: DataQueryRequest<WildGraphQLMainQuery>): Observable<DataQueryResponse> {
Expand Down
3 changes: 2 additions & 1 deletion src/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
"id": "wildmountainfarms-wildgraphql-datasource",
"metrics": true,
"backend": true,
"alerting": true,
"executable": "gpx_wild_graphql_datasource",
"info": {
"description": "Grafana datasource to interpret GraphQL query results as timeseries data",
"author": {
"name": "wildmountainfarms"
"name": "Lavender Shannon"
},
"keywords": ["datasource"],
"logos": {
Expand Down
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,15 @@ export const DEFAULT_QUERY: Partial<WildGraphQLMainQuery> = {
queryStatus(sourceId: $sourceId, from: $from, to: $to) {
batteryVoltage {
dateMillis
fragmentId
packet {
batteryVoltage
identifier {
representation
}
identityInfo {
displayName
}
}
}
}
Expand All @@ -79,6 +86,26 @@ export const DEFAULT_QUERY: Partial<WildGraphQLMainQuery> = {
]
};

export const DEFAULT_ALERTING_QUERY: Partial<WildGraphQLMainQuery> = {
queryText: `query BatteryVoltage($from: Long!, $to: Long!) {
queryStatus(sourceId: "default", from: $from, to: $to) {
batteryVoltage {
dateMillis
packet {
batteryVoltage
}
}
}
}
`,
parsingOptions: [
{
dataPath: "queryStatus.batteryVoltage",
timePath: "dateMillis"
}
]
};

/**
* These are options configured for each DataSource instance
*/
Expand Down

0 comments on commit fbaa309

Please sign in to comment.