将前端数据源插件转换为后端插件
本指南向你展示如何将现有的仅前端数据源插件转换为后端插件。
要转换前端数据源,我们建议使用 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;
}
}
结论
本指南介绍了将前端数据源转换为后端数据源的主要步骤。插件种类繁多,因此,如果你有任何问题或需要特定案例的帮助,我们鼓励你在我们的社区论坛中联系我们。也欢迎对此指南做出贡献。