将前端数据源插件转换为后端插件
本指南将向您展示如何将现有的前端数据源插件转换为 后端插件。
要转换前端数据源,我们建议使用 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
实例中。
健康检查
将身份验证逻辑移到后端后,您可以在后端进行健康检查。
您需要删除前端实现 testDatasource
(位于前端的 Datasource
类中)以使用后端的健康检查。
在此前端示例中,健康检查向 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
方法。此方法应该返回一个帧列表。
与健康检查一样,您需要删除前端实现 query
(位于前端的 Datasource
类中)。
以下示例展示了前述方法
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
}
其他资源请求
最后,插件可以实现一种可选的请求类型。我们称之为 resources。资源是插件公开的附加端点,用于填充查询编辑器或配置编辑器。例如,您可以使用资源来填充下拉菜单,其中包含数据库中可用表的列表。
在前端数据源中,插件应该将资源定义为 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;
}
}
结论
本指南介绍了将前端数据源转换为后端数据源的主要步骤。存在各种各样的插件,如果您有任何问题或需要特定情况的帮助,我们鼓励您访问我们的社区论坛寻求帮助。我们也欢迎您为本指南做出贡献。