跳转到主要内容

将前端数据源插件转换为后端插件

本指南向您展示如何将现有的仅前端数据源插件转换为 后端插件

要将前端数据源转换为后端,我们建议使用 npx @grafana/create-plugin@latest 创建一个新的后端数据源插件。使用以下说明扩展此基础,以从您的原始插件复制功能。

为什么

后端插件中提供了许多仅在后端插件中可用的功能,例如 Grafana 告警或已记录查询。有关实现后端插件的用例,请参阅 后端插件介绍

开始之前

在深入细节之前,您应该熟悉创建后端数据源插件的过程。如果您之前没有这样做过,您可以遵循我们的 构建后端插件教程

关键概念

在进入具体的转换建议之前,了解数据源的主要组件以及这些组件在前端和后端插件之间的区别非常重要。

前端 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 数据源通常包含两种类型的数据:jsonDatasecureJsonData。前者用于存储非敏感信息,而后者用于存储敏感信息,如密码或 API 密钥。

前端和后端类型使用相同的 JSON 数据来认证目标数据源。主要区别在于前端数据源应该为每个请求读取和使用凭据,而后端数据源应该在请求之间共享相同的已认证客户端。

在仅前端的数据源中,任何需要认证的请求都需要通过插件代理。您需要在 plugin.json 文件中定义一个 routes 对象,并指定每个请求要使用的 URL 和凭据。例如,您可以通过设置带有 SecureJsonData 凭据的 Authorization 标头来认证对给定 URL 的请求

src/plugin.json
"routes": [
{
"path": "example",
"url": "https://api.example.com",
"headers": [
{
"name": "Authorization",
"content": "Bearer {{ .SecureJsonData.apiToken }}"
}
]
}
]

要使用此路由,前端数据源应调用 DataSourceApi 类中的 fetch 方法。此方法代理请求并添加 Authorization 标头

src/DataSource.ts
import { getBackendSrv } from '@grafana/runtime';

const routePath = '/example';

const res = getBackendSrv().datasourceRequest({
url: this.url + routePath + '/v1/users',
method: 'GET',
});
// Handle response

在后端数据源中,你应该将认证逻辑移动到Datasource构造函数中。此方法在创建数据源时被调用,应用于创建认证客户端。将此客户端存储在Datasource实例中,并用于每个请求。例如:

pkg/plugin/datasource.go
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请求,如果请求失败则返回错误。

src/DataSource.ts
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方法。如果数据源不健康,此方法将返回一个错误。例如:

pkg/plugin/datasource.go
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转换为帧。

src/DataSource.ts
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实现。

以下示例显示了前面的方法

pkg/plugin/datasource.go
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方法获取数据。例如

src/plugin.json
{
"routes": [
{
"path": "tables",
"url": "https://api.example.com/api/v1/tables",
"method": "GET"
}
]
}
src/DataSource.ts
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接口。该接口应处理不同的可能资源。例如

pkg/plugin/datasource.go
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中公开的方法(例如,getResourcepostResource

src/DataSource.ts
export class DataSource extends DataSourceWithBackend<MyQuery, MyDataSourceOptions> {
async getTables() {
const response = await this.getResource('tables');
return response;
}
}

结论

本指南涵盖了将前端数据源转换为后端数据源的主要步骤。插件种类繁多,如果您有任何问题或需要特定情况的帮助,我们鼓励您在我们的社区论坛中寻求帮助。也欢迎为该指南做出贡献。