将前端数据源插件转换为后端插件
本指南介绍如何将现有的纯前端数据源插件转换为后端插件。
要转换前端数据源,建议使用 npx @grafana/create-plugin@latest
脚手架生成一个新的后端数据源插件。请按照以下说明扩展此基础,以复制原始插件的功能。
原因
许多功能仅在后端插件中可用,例如 Grafana Alerting、Recorded queries 或外部共享仪表盘(以前称为 Public dashboards)。请参阅后端插件介绍中实现后端插件的用例。
开始之前
在深入了解细节之前,您应该熟悉创建后端数据源插件的过程。如果您之前没有做过,可以按照我们的构建后端插件教程进行操作。
关键概念
在讨论具体的转换建议之前,了解数据源的主要组件以及它们在前端和后端插件之间的区别非常重要。
前端 DataSource
类
数据源插件实现一个新的 DataSourcePlugin
。此类将 DataSource
类作为参数,对于前端数据源,该类扩展 DataSourceApi
,对于后端数据源,该类扩展 DataSourceWithBackend
。由于 DatasourceWithBackend
类已经实现了大多数所需的方法,您可以迁移到它来显著简化您的代码。
数据源插件需要两个组件:查询编辑器和配置编辑器。
示例
查询和配置编辑器
将前端数据源转换为后端数据源时,无需更改这两个前端组件。但是,如果您为数据源添加后端组件,则可以向其请求 resources
(资源)。资源是插件公开的额外端点,可用于填充或验证查询或配置编辑器。请参阅资源请求部分了解更多信息。
插件结构对比
以下文件夹说明了为插件添加后端时引入的新组件
myorg-myplugin-datasource/
├── .config/
├── .eslintrc
├── .github
│ └── workflows
├── .gitignore
├── .nvmrc
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── Magefile.go # Build definition for backend executable
├── README.md
│ └── integration
├── docker-compose.yaml
├── go.mod # Dependencies
├── go.sum # Checksums
├── jest-setup.js
├── jest.config.js
├── node_modules
├── package.json
├── pkg
│ ├── main.go # Entry point for backend
│ └── plugin # Other plugin packages
├── playwright.config.ts
├── src
│ ├── README.md
│ ├── components
│ ├── datasource.ts
│ ├── img
│ ├── module.ts
│ ├── plugin.json # Modified to include backend=true and executable=<name-of-built-binary>
│ └── types.ts
├── tsconfig.json
└── tests
将前端函数转换为后端函数
大多数插件只需实现三个方法即可完全运行:运行查询的函数、测试数据源连接的函数以及获取不同资源(用于填充查询编辑器或配置编辑器)的任何额外 GET 请求。这三个方法通常共享针对目标数据源的相同认证机制。
现在,让我们讨论如何将认证逻辑从前端移动到后端。
认证
Grafana 数据源通常包含两种类型的数据:jsonData
和 secureJsonData
。前者用于存储非敏感信息,后者用于存储密码或 API 密钥等敏感信息。
前端和后端类型都使用相同的 JSON 数据来针对目标数据源进行认证。主要区别在于,前端数据源应在每次请求时读取和使用凭据,而后端数据源应在请求之间共享同一个已认证的客户端。
在纯前端数据源中,任何需要认证的请求都需要通过插件代理。您需要在 plugin.json
文件中定义一个 routes
对象,并在其中指定每个请求使用的 URL 和凭据。例如,您可以通过设置包含 SecureJsonData
凭据的 Authorization
头来认证对给定 URL 的请求
"routes": [
{
"path": "example",
"url": "https://api.example.com",
"headers": [
{
"name": "Authorization",
"content": "Bearer {{ .SecureJsonData.apiToken }}"
}
]
}
]
要使用此路由,前端数据源应调用 DataSourceApi
类中的 fetch
方法。此方法代理请求并添加 Authorization
头
import { getBackendSrv } from '@grafana/runtime';
const routePath = '/example';
const res = getBackendSrv().datasourceRequest({
url: this.url + routePath + '/v1/users',
method: 'GET',
});
// Handle response
在后端数据源中,您应将认证逻辑移动到 Datasource
构造函数。此方法在创建数据源时调用,应用于创建已认证的客户端。将此客户端存储在 Datasource
实例中,并在每个请求中使用它。例如
package plugin
import (
...
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
...
)
func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
opts, err := settings.HTTPClientOptions(ctx)
if err != nil {
return nil, fmt.Errorf("http client options: %w", err)
}
opts.Header.Add("Authorization", "Bearer " + settings.DecryptedSecureJSONData["token"])
cli, err := httpclient.New(opts)
if err != nil {
return nil, fmt.Errorf("httpclient new: %w", err)
}
return &Datasource{
httpClient: cl,
}, nil
}
// In any other method
res, err := d.httpClient.Get("https://api.example.com/v1/users")
// Handle response
同样的原理适用于任何其他认证机制。例如,基于 SQL 的数据源应使用 Datasource
构造函数创建数据库连接并将其存储在 Datasource
实例中。
健康检查
将认证逻辑移动到后端后,您可以在后端进行健康检查。
要使用后端中的健康检查,您需要删除前端 `Datasource` 类中的前端实现 `testDatasource`。
在此前端示例中,健康检查向 `https://api.example.com`(如 `plugin.json` 中的 `routes` 字段所定义)发出 API 请求,如果请求失败则返回错误
import { getBackendSrv } from '@grafana/runtime';
const routePath = '/example';
export class MyDatasource extends DataSourceApi<MyQuery, MyDataSourceJsonData> {
...
async testDatasource() {
try {
await getBackendSrv().datasourceRequest({
url: this.url + routePath + '/v1/users',
method: 'GET',
});
return {
status: 'success',
message: 'Health check passed.',
};
} catch (error) {
return { status: 'error', message: error.message };
}
}
}
对于后端数据源,`Datasource` 结构应实现 `CheckHealth` 方法。如果数据源不健康,此方法将返回错误。例如
func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
opts, err := settings.HTTPClientOptions(ctx)
if err != nil {
return nil, fmt.Errorf("http client options: %w", err)
}
cl, err := httpclient.New(opts)
if err != nil {
return nil, fmt.Errorf("httpclient new: %w", err)
}
return &Datasource{
settings: settings,
httpClient: cl,
}, nil
}
func (d *Datasource) CheckHealth(ctx context.Context, _ *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
resp, err := d.httpClient.Get(d.settings.URL + "/v1/users")
if err != nil {
// Log the error here
return &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: "request error",
}, nil
}
if resp.StatusCode != http.StatusOK {
return &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: fmt.Sprintf("got response code %d", resp.StatusCode),
}, nil
}
return &backend.CheckHealthResult{
Status: backend.HealthStatusOk,
Message: "Data source is working",
}, nil
}
此示例涵盖了纯 HTTP 数据源。因此,如果您的数据源需要数据库连接,您可以使用数据库的 Go 客户端并执行简单的查询,例如 `SELECT 1` 或 `ping` 函数。
查询
下一步是移动查询逻辑。这将根据插件如何查询数据源并将响应转换为帧而有很大差异。本指南将展示如何迁移一个简单的示例。
当访问 `/metrics` 端点时,我们的数据源返回一个包含 `datapoints` 列表的 JSON 对象。前端 `query` 方法将这些 `datapoints` 转换为帧
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
const response = await lastValueFrom(
getBackendSrv().fetch<DataSourceResponse>({
url: `${this.url}/metrics`,
method: 'GET',
})
);
const df: DataFrame = {
length: response.data.datapoints.length,
refId: options.targets[0].refId,
fields: [
{ name: 'Time', values: [], type: FieldType.time, config: {} },
{
name: 'Value',
values: [],
type: FieldType.number,
config: {},
},
],
};
response.data.datapoints.forEach((datapoint: any) => {
df.fields[0].values.push(datapoint.time);
df.fields[1].values.push(datapoint.value);
});
return { data: [df] };
}
}
现在让我们看看如何将其转换为后端。`Datasource` 实例应实现 `QueryData` 方法。此方法应返回一个帧列表。
与健康检查一样,您需要删除前端 `Datasource` 类中的前端实现 `query`。
以下示例展示了上述方法
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
res, err := d.httpClient.Get(d.settings.URL + "/metrics")
// Handle errors (omitted)
// Decode response
var body struct {
DataPoints []apiDataPoint `json:"datapoints"`
}
if err := json.NewDecoder(httpResp.Body).Decode(&body); err != nil {
return backend.DataResponse{}, fmt.Errorf("%w: decode: %s", errRemoteRequest, err)
}
// Create slice of values for time and values.
times := make([]time.Time, len(body.DataPoints))
values := make([]float64, len(body.DataPoints))
for i, p := range body.DataPoints {
times[i] = p.Time
values[i] = p.Value
}
// Create frame and add it to the response
dataResp := backend.DataResponse{
Frames: []*data.Frame{
data.NewFrame(
"response",
data.NewField("time", nil, times),
data.NewField("values", nil, values),
),
},
}
return dataResp, err
}
其他资源请求
最后,还有一种插件可能实现的可选请求类型。这就是我们所说的资源。资源是插件公开的额外端点,用于填充查询编辑器或配置编辑器。例如,您可以使用资源填充下拉菜单,其中包含数据库中可用表的列表。
在前端数据源中,插件应在 `plugin.json` 文件中将资源定义为 `routes`,并使用 `fetch` 方法获取数据。例如
{
"routes": [
{
"path": "tables",
"url": "https://api.example.com/api/v1/tables",
"method": "GET"
}
]
}
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
async getTables() {
const response = await lastValueFrom(
getBackendSrv().fetch<MetricsResponse>({
url: `${this.url}/tables`,
method: 'GET',
})
);
return response.data;
}
}
为简单起见,此示例中省略了认证。
对于后端数据源,插件应实现 `CallResourceHandler` 接口。此接口应处理不同的可能资源。例如
func NewDatasource(_ context.Context, _ backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return &Datasource{
CallResourceHandler: newResourceHandler(),
}, nil
}
func newResourceHandler() backend.CallResourceHandler {
mux := http.NewServeMux()
mux.HandleFunc("/tables", handleTables)
return httpadapter.New(mux)
}
func handleTables(w http.ResponseWriter, r *http.Request) {
// Get tables
res, err := http.DefaultClient.Get("https://api.example.com/api/v1/tables")
// Handle errors (omited)
body, err := io.ReadAll(res.Body)
// Handle errors (omited)
w.Write(body)
w.WriteHeader(http.StatusOK)
}
要在前端请求资源,您可以使用基础类 `DataSourceWithBackend` 中公开的方法(例如 `getResource` 或 `postResource`)
export class DataSource extends DataSourceWithBackend<MyQuery, MyDataSourceOptions> {
async getTables() {
const response = await this.getResource('tables');
return response;
}
}
结论
本指南涵盖了将前端数据源转换为后端数据源的主要步骤。插件种类繁多,如果您有任何问题或需要特定案例的帮助,我们鼓励您在我们的社区论坛中寻求帮助。也欢迎您对本指南做出贡献。