将前端数据源插件转换为后端插件
本指南向您展示如何将现有的仅前端数据源插件转换为 后端插件。
要将前端数据源转换为后端,我们建议使用 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 数据源通常包含两种类型的数据: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;
}
}
结论
本指南涵盖了将前端数据源转换为后端数据源的主要步骤。插件种类繁多,如果您有任何问题或需要特定情况的帮助,我们鼓励您在我们的社区论坛中寻求帮助。也欢迎为该指南做出贡献。