概要

Grafanaのバックエンドデータソースプラグインの開発手順を記載する。

目次

開発環境の構築

https://grafana.com/tutorials/build-a-data-source-backend-plugin/ を参考に開発環境を構築する。

ビルドには以下が必要との事 (2021/10 現在)

  • Grafana 7.0
  • Go 1.14+
  • Mage
  • NodeJS
  • yarn

 セットアップ

cd /tmp

echo "## apt update ##"
apt update

echo "## apt install (initial) ##"
apt install -y wget curl git curl nodejs npm

echo "## go install ##"
curl -L -o go1.16.9.linux-amd64.tar.gz https://golang.org/dl/go1.16.9.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.16.9.linux-amd64.tar.gz
echo "export PATH=\$PATH:/usr/local/go/bin" >> ~/.bashrc

echo "## npm install n ##"
npm install -g n
echo "## n install 16.11.0 ##"
n install 16.11.0
echo "## apt remove nodejs npm ##"
apt remove -y nodejs npm

echo "## apt remove yarn ##"
apt remove -y cmdtest yarn
echo "## re install yarn ##"
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
apt update && apt install -y yarn

echo "## install mage ##"
mkdir -p ~/go/bin
git clone https://github.com/magefile/mage
cd mage
GOPATH=~/go go run bootstrap.go
echo "export GOPATH=\$HOME/go" >>~/.bashrc
echo "export PATH=\$PATH:\$GOPATH/bin" >>~/.bashrc
source ~/.bashrc

echo "## npm install @grafana/toolkit ##"
npm install -g @grafana/toolkit

プラグインの雛形の作成

npx @grafana/toolkit plugin:create my-backend-datasource-test
Need to install the following packages:
Ok to proceed? (y) y

? Select plugin type Backend Datasource Plugin
✔ Fetching plugin template...
? Plugin name my-backend-datasource-test
? Organization (used as part of plugin ID) sample
? Description 
? Keywords (separated by comma) 
? Author myname
? Your URL (i.e. organisation url) 
    
    Your plugin details
    ---
    Name:  my-backend-datasource-test
    ID:  sample-my-backend-datasource-test
    Description:  
    Keywords:  []
    Author:  myname
    Organisation:  sample
    Website:  
    
? Is that ok? Yes
✔ Saving package.json and plugin.json files
✔ Cleaning
    
    Congrats! You have just created Grafana Backend Datasource Plugin.
    
    Learn more about Grafana Plugins at https://grafana.com/docs/grafana/latest/plugins/developing/development/

ビルド確認

cd my-backend-datasource-test
go get -u github.com/grafana/grafana-plugin-sdk-go
go mod tidy
mage -v

データソースプラグインの作成

バックエンドのデータソースプラグインは設定画面とバックエンド処理の2つで成り立っており、フロント部分は TypeScrypt 、バックエンド部分は Go で作成する必要がある。
その為、ビルドは別々に行う事になるが、ビルド結果は両方とも dist 配下に出力される為、プラグインのデプロイは dist フォルダを grafana のプラグインディレクトリに配置するだけでOK。

データソース 及び クエリの設定画面

各設定画面は、データソース自体の設定画面と各パネルで使用するクエリ編集画面用の2つある。

データソース設定画面( ConfigEditor.tsx ) の作成

中身は react コンポーネントだが、各入力フィールドは Grafana の UIコンポーネントを利用して作成する。
通常の必要なフィールドは FormField、よりセキュアな情報を扱うフィールドは SecretFormField コンポーネントを利用して作成する。

以下、雛形生成された ConfigEditor.tsx より抜粋。

      :
    return (
      <div className="gf-form-group">
        <div className="gf-form">
          <FormField
            label="Path"
            labelWidth={6}
            inputWidth={20}
            onChange={this.onPathChange}
            value={jsonData.path || ''}
            placeholder="json field returned to frontend"
          />
        </div>

        <div className="gf-form-inline">
          <div className="gf-form">
            <SecretFormField
              isConfigured={(secureJsonFields && secureJsonFields.apiKey) as boolean}
              value={secureJsonData.apiKey || ''}
              label="API Key"
              placeholder="secure json field (backend only)"
              labelWidth={6}
              inputWidth={20}
              onReset={this.onResetAPIKey}
              onChange={this.onAPIKeyChange}
            />
          </div>
        </div>
      </div>
    );
  } 
} 

値変更時の処理は、項目毎に onChange メソッドを実装する必要がある。
※セキュア項目は onReset 時のメソッドも必要。

以下、雛形生成された ConfigEditor.tsx より抜粋。

  :
export class ConfigEditor extends PureComponent<Props, State> {
  onPathChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { onOptionsChange, options } = this.props;
    const jsonData = {
      ...options.jsonData,
      path: event.target.value,
    };
    onOptionsChange({ ...options, jsonData });
  };

  // Secure field (only sent to the backend)
  onAPIKeyChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { onOptionsChange, options } = this.props;
    onOptionsChange({
      ...options,
      secureJsonData: {
        apiKey: event.target.value,
      },
    });
  };

  onResetAPIKey = () => {
    const { onOptionsChange, options } = this.props;
    onOptionsChange({
      ...options,
      secureJsonFields: {
        ...options.secureJsonFields,
        apiKey: false,
      },
      secureJsonData: {
        ...options.secureJsonData,
        apiKey: '',
      },
    });
  };
  :

項目を追加する場合は、 types.ts に追加する。
デフォルトの状態だと、通常の設定項目は MyDataSourceOptions、セキュア項目は MySecureJsonData に定義されている為、ここに追加する。

types.ts

import { DataQuery, DataSourceJsonData } from '@grafana/data';
  
export interface MyQuery extends DataQuery {
  // ここはクエリ画面の項目定義
  queryText?: string;
  constant: number;
  withStreaming: boolean;
}

export const defaultQuery: Partial<MyQuery> = {
  // ここはクエリ画面の項目のデフォルト値
  constant: 6.5,
  withStreaming: false,
};

/**
 * These are options configured for each DataSource instance.
 */
export interface MyDataSourceOptions extends DataSourceJsonData {
  // ここに通常の設定項目
  path?: string;
}

/**
 * Value that is used in the backend, but never sent over HTTP to the frontend
 */
export interface MySecureJsonData {
  // ここにセキュアな設定項目
  apiKey?: string;
}

クエリの設定画面 ( QueryEditor.tsx ) の作成

クエリ画面側も設定画面と同じく react コンポーネントとして作成する。
※項目自体を追加する場合は、設定画面と同じく types.ts の編集も必要。

以下、雛形生成された QueryEditor.tsx をそのまま記載。

import { LegacyForms } from '@grafana/ui';
import { QueryEditorProps } from '@grafana/data';
import { DataSource } from './datasource';
import { defaultQuery, MyDataSourceOptions, MyQuery } from './types';

const { FormField, Switch } = LegacyForms;

type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;

export class QueryEditor extends PureComponent<Props> {
  onQueryTextChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { onChange, query } = this.props;
    onChange({ ...query, queryText: event.target.value });
  };

  onConstantChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { onChange, query, onRunQuery } = this.props;
    onChange({ ...query, constant: parseFloat(event.target.value) });
    // executes the query
    onRunQuery();
  };

  onWithStreamingChange = (event: SyntheticEvent<HTMLInputElement>) => {
    const { onChange, query, onRunQuery } = this.props;
    onChange({ ...query, withStreaming: event.currentTarget.checked });
    // executes the query
    onRunQuery();
  };

  render() {
    const query = defaults(this.props.query, defaultQuery);
    const { queryText, constant, withStreaming } = query;

    return (
      <div className="gf-form">
        <FormField
          width={4}
          value={constant}
          onChange={this.onConstantChange}
          label="Constant"
          type="number"
          step="0.1"
        />
        <FormField
          labelWidth={8}
          value={queryText || ''}
          onChange={this.onQueryTextChange}
          label="Query Text"
          tooltip="Not used yet"
        />
        <Switch checked={withStreaming || false} label="Enable streaming (v8+)" onChange={this.onWithStreamingChange} />
      </div>
    );
  }
}

ビルド等は yarn を使用して行う事ができる。( package.json を参照 )

依存モジュールのインストール

yarn install

ビルド

yarn build

バックエンド処理の作成

バックエンド部分は pkg/plugin/plugin.go に記述する。

クエリに対する処理は query メソッドに記述する。

:

func (d *SampleDatasource) query(_ context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
    response := backend.DataResponse{}

    // Unmarshal the JSON into our queryModel.
    var qm queryModel

    response.Error = json.Unmarshal(query.JSON, &qm)
    if response.Error != nil {
        return response
    }   

    log.DefaultLogger.Info("### pCtx  ###", "pCtx", pCtx)
    log.DefaultLogger.Info("### query ###", "query", query)

    // create data frame response.
    frame := data.NewFrame("response")

    // add fields.
    frame.Fields = append(frame.Fields,
        data.NewField("time", nil, []time.Time{query.TimeRange.From, query.TimeRange.To}),
        data.NewField("values", nil, []int64{10, 20}),
    )   

    // add the frames to the response.
    response.Frames = append(response.Frames, frame)

    return response
}
:

設定画面でデータソース作成時行うヘルスチェックの内容は CheckHealth メソッドに記述する。
雛形生成直後の状態だとランダムでエラーになるように記述されている為、ヘルスチェックを行わない場合でも必ず書き換えが必要。

 :
func (d *SampleDatasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
    log.DefaultLogger.Info("CheckHealth called", "request", req)

    var status = backend.HealthStatusOk
    var message = "Data source is working"

    // ランダムでエラーになるように記述されている為、コメントアウトしておく
    //if rand.Int()%2 == 0 {
    //    status = backend.HealthStatusError
    //    message = "randomized error"
    //}

    return &backend.CheckHealthResult{
        Status:  status,
        Message: message,
    }, nil
}
 :

Grafanaへの配備

grafana の plugins ディレクトリに dist フォルダ事コピーする。
Grafana8 からは署名が必須になっているが、開発途中などで、直ちに署名できない場合は、
環境変数 GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS に該当のプラグイン名を列挙する事でプラグインのロードが可能。

https://grafana.com/docs/grafana/latest/developers/plugins/sign-a-plugin/
https://grafana.com/docs/grafana/latest/administration/configuration/#allow_loading_unsigned_plugins

サンプル作成

サンプルとして他サーバで動作する WebAPI をデータソースとする、バックエンドデータソースを作成してみる。

TODO:

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-02-04 (金) 00:51:04 (234d)