跳到主要内容

为后端插件添加日志、指标和追踪

为后端插件添加 日志指标追踪,可以更轻松地诊断和解决插件开发者和 Grafana 运维人员的问题。本文档提供指导、约定和最佳实践,以帮助您有效地检测插件,以及在安装插件后如何访问这些数据。

注意

本文档希望您至少使用 grafana-plugin-sdk-go v0.246.0。但是,建议保持 Grafana 插件 SDK for Go 为最新版本,以获得最新的改进、安全性和错误修复。有关更新说明,请参阅 更新 Go SDK

日志

日志是记录软件环境中发生的事件、警告和错误的文件。大多数日志都包含上下文信息,例如事件发生的时间以及与之关联的用户或端点。

SDK 的自动检测

SDK 自动化了一些检测,以简化开发者和运维人员的体验。每次方法调用(QueryDataCallResourceCheckHealth 等)完成后,都会记录一条 Plugin Request Completed 消息。此外,如果 QueryData 响应包含任何错误,则会为每个数据响应错误记录一条 Partial data response error 消息。以下是一些已记录消息的示例

DEBUG[09-05|17:24:16] Plugin Request Completed  logger=plugin.grafana-test-datasource dsUID=edeuvt04gim0we endpoint=queryData pluginID=grafana-test-datasource statusSource=plugin uname=admin dsName=grafana-test-datasource traceID=604e15b6345c2c0896e6902fa86b82f5 duration=1.482975875s status=ok
DEBUG[09-05|18:24:16] Plugin Request Completed logger=plugin.grafana-test-datasource dsUID=edeuvt04gim0we endpoint=queryData pluginID=grafana-test-datasource statusSource=plugin uname=admin dsName=grafana-test-datasource traceID=604e15b6345c2c0896e6902fa86b82f5 duration=1.482975875s status=cancelled error=context.Canceled error
ERROR[09-05|19:24:16] Plugin Request Completed logger=plugin.grafana-test-datasource dsUID=edeuvt04gim0we endpoint=queryData pluginID=grafana-test-datasource statusSource=plugin uname=admin dsName=grafana-test-datasource traceID=604e15b6345c2c0896e6902fa86b82f5 duration=1.482975875s status=error error=something is not working as expected
ERROR[09-06|15:29:47] Partial data response error logger=plugin.grafana-test-datasource status=500.000 statusSource=plugin dsName=grafana-test-datasource dsUID=edeuvt04gim0we endpoint=queryData refID=A error="no handler found for query type 'noise'" pluginID=grafana-test-datasource traceID=981b7761aa295e371757582c7a4043d1 uname=admin

在插件中实现日志记录

backend 包 中使用全局记录器 backend.Logger,适用于所有场景和大多数用例。

示例

以下示例展示了全局记录器在不同严重级别和键值对中的基本用法。

package plugin

import (
"errors"

"github.com/grafana/grafana-plugin-sdk-go/backend"
)

func main() {
backend.Logger.Debug("Debug msg", "someID", 1)
backend.Logger.Info("Info msg", "queryType", "default")
backend.Logger.Warning("Warning msg", "someKey", "someValue")
backend.Logger.Error("Error msg", "error", errors.New("An error occurred"))
}

以上示例将输出如下内容。

DEBUG[11-14|15:26:26] Debug msg     logger=plugin.grafana-basic-datasource someID=1
INFO [11-14|15:26:26] Info msg logger=plugin.grafana-basic-datasource queryType=default
WARN [11-14|15:26:26] Warning msg logger=plugin.grafana-basic-datasource someKey=someValue
ERROR[11-14|15:26:26] Error msg logger=plugin.grafana-basic-datasource error=An error occurred
注意

backend.Loggerlog 包log.DefaultLogger 的便捷包装器,您也可以使用它来访问全局记录器。

重用具有特定键/值对的记录器

您可以记录多条消息,并包含特定的键值对,而无需在每个地方重复代码,例如,当您想根据数据源的配置方式在每个日志消息中包含一些特定的键值对时。为此,请使用已实例化记录器上的 With 方法创建带有参数的新记录器。

示例

以下示例说明了如何为每个 数据源实例 实例化一个记录器,并在该数据源实例的生命周期内使用 With 方法来包含特定的键值对。

package plugin

import (
"context"
"errors"

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
)

func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
logger := backend.Logger.With("key", "value")

return &Datasource{
logger: logger,
}, nil
}

func (ds *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ds.logger.Debug("QueryData", "queries", len(req.Queries))
}

每次调用 QueryData 时,以上示例将输出如下内容。

DEBUG[11-14|15:26:26] QueryData     logger=plugin.grafana-basic-datasource key=value queries=2
注意

您还可以使用 backend 包 中的 backend.NewLoggerWith,这是一个辅助方法,它调用 log 包 中的 log.New().With(args...)

使用上下文记录器

使用上下文记录器可以自动包含附加到 context.Context 的其他键值对。例如,您可以使用 traceID 将日志与追踪相关联,并将日志与通用标识符相关联。您可以使用已实例化记录器上的 FromContext 方法创建一个新的上下文记录器;您还可以在 重用具有特定键值对的记录器 时组合此方法。我们建议在可以访问 context.Context 时始终使用上下文记录器。

默认情况下,使用上下文记录器时,以下键值对包含在日志中

  • pluginID: 插件标识符。例如,grafana-github-datasource
  • endpoint: 正在处理的请求;即 callResourcecheckHealthcollectMetricsqueryDatarunStreamsubscribeStreampublishStream
  • traceID: 如果可用,则包含分布式追踪标识符。
  • dsName: 如果可用,则为已配置数据源实例的名称。
  • dsUID: 如果可用,则为已配置数据源实例的唯一标识符 (UID)。
  • uname: 如果可用,则为发出请求的用户的用户名。

示例

以下示例扩展了 重用具有特定键值对的记录器 示例,以包含上下文记录器的用法。

package plugin

import (
"context"
"errors"

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
)

func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
logger := backend.Logger.With("key", "value")

return &Datasource{
logger: logger,
}, nil
}

func (ds *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ctxLogger := ds.logger.FromContext(ctx)
ctxLogger.Debug("QueryData", "queries", len(req.Queries))
}

每次使用 2 个查询调用 QueryData 时,以上示例将输出如下内容。

DEBUG[11-14|15:26:26] QueryData     logger=plugin.grafana-basic-datasource pluginID=grafana-basic-datasource endpoint=queryData traceID=399c275ebb516a53ec158b4d0ddaf914 dsName=Basic datasource dsUID=kXhzRl7Mk uname=admin key=value queries=2

在日志中包含其他上下文信息

如果您想将其他上下文键值对传播到后续代码/逻辑,可以使用 log.WithContextualAttributes 函数。

示例

以下示例扩展了 使用上下文记录器 示例,通过添加其他上下文键值对并允许将这些键值对传播到其他方法 (handleQuery) 来使用 log.WithContextualAttributes 函数。

package plugin

import (
"context"
"errors"

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)

func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
logger := backend.Logger.With("key", "value")

return &Datasource{
logger: logger,
}, nil
}

func (ds *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ctxLogger := ds.logger.FromContext(ctx)
ctxLogger.Debug("QueryData", "queries", len(req.Queries))

for _, q := range req.Queries {
childCtx = log.WithContextualAttributes(ctx, []any{"refID", q.RefID, "queryType", q.QueryType})
ds.handleQuery(childCtx, q)
}
}

func (ds *Datasource) handleQuery(ctx context.Context, q backend.DataQuery) {
ctxLogger := ds.logger.FromContext(ctx)
ctxLogger.Debug("handleQuery")
}

每次使用 2 个查询调用 QueryData 时,以上示例将输出如下内容。

DEBUG[11-14|15:26:26] QueryData     logger=plugin.grafana-basic-datasource pluginID=grafana-basic-datasource endpoint=queryData traceID=399c275ebb516a53ec158b4d0ddaf914 dsName=Basic datasource dsUID=kXhzRl7Mk uname=admin queries=2
DEBUG[11-14|15:26:26] handleQuery logger=plugin.grafana-basic-datasource pluginID=grafana-basic-datasource endpoint=queryData traceID=399c275ebb516a53ec158b4d0ddaf914 dsName=Basic datasource dsUID=kXhzRl7Mk uname=admin refID=A queryType=simpleQuery
DEBUG[11-14|15:26:26] handleQuery logger=plugin.grafana-basic-datasource pluginID=grafana-basic-datasource endpoint=queryData traceID=399c275ebb516a53ec158b4d0ddaf914 dsName=Basic datasource dsUID=kXhzRl7Mk uname=admin refID=B queryType=advancedQuery

最佳实践

  • 日志消息以大写字母开头;例如,logger.Info("Hello world") 而不是 logger.Info("hello world")
  • 日志消息应该是日志条目的标识符,尽量避免参数化;例如,logger.Debug(fmt.Sprintf(“Something happened, got argument %d”, “arg”)),而是使用键值对来表示其他数据;例如,logger.Info(“Something happened”, “argument”, “arg”)
  • 命名日志键时,首选使用 camelCase 样式;例如,remoteAddruserID,以与 Go 标识符保持一致。
  • 记录 Go 错误时,使用键 error;例如,logger.Error("Something failed", "error", errors.New("An error occurred")
  • 在可以访问 context.Context 时始终使用上下文记录器。
  • 不要记录敏感信息,例如数据源凭据或 IP 地址,或其他个人身份信息。

验证和清理来自用户输入的输入

如果日志消息或键值对来自用户输入,则应进行验证和清理。注意不要在日志消息中暴露任何敏感信息(密钥、凭据等)。当包含 Go 结构体作为值时,尤其容易出错。

如果来自用户输入的值是有限的,即当存在一组固定的预期值时,建议验证它是否是这些值之一,否则返回错误。

如果来自用户输入的值是无限的,即该值可以是任何值,则建议验证值的最大长度/大小,并返回错误或通过仅允许一定数量/固定字符集来清理。

何时使用哪个日志级别?

  • Debug: 正常操作期间的高频率信息性消息和不太重要的消息。
  • Info: 低频率信息性消息和重要消息。
  • Warning: 可以从中恢复而不会中断操作的错误/状态。如果使用,则应该是可操作的,以便运维人员可以执行某些操作来解决它。
  • Error: 错误消息,指示某些操作失败(并出现错误),并且程序没有处理错误的方法。
注意

对于 QueryData 端点,高频率的传入请求通常更常见,因为例如仪表板的性质会为每个面板或查询生成一个请求。

在本地检查日志

来自后端插件的日志由连接的 Grafana 实例使用,并包含在 Grafana 服务器日志中。

后端插件的每条日志消息都将包含一个记录器名称,logger=plugin.<plugin id>。示例

DEBUG[11-14|15:26:26] Debug msg     logger=plugin.grafana-basic-datasource someID=1
INFO [11-14|15:26:26] Info msg logger=plugin.grafana-basic-datasource queryType=default
WARN [11-14|15:26:26] Warning msg logger=plugin.grafana-basic-datasource someKey=someValue
ERROR[11-14|15:26:26] Error msg logger=plugin.grafana-basic-datasource error=An error occurred

您可以启用 Grafana 实例中的调试日志记录,这通常会输出大量信息,并且难以找到与特定插件相关的日志。但是,使用命名记录器可以方便地仅为某个命名记录器和插件启用调试日志记录

[log]
filters = plugin.<plugin id>:debug

有关设置日志记录的更多详细信息,请参阅 配置 Grafana

此外,请参阅 如何收集和可视化日志、指标和追踪

指标

指标是可量化的测量值,反映应用程序或基础设施的健康状况和性能。

考虑使用指标来提供对资源状态的实时洞察。如果您想知道插件的响应速度如何,或者识别可能是性能问题早期迹象的异常情况,那么指标是关键的可视性来源。

指标类型

Prometheus 中支持四种不同的指标类型,您可以使用它们

  • 计数器: 只能增加或在重启时重置为零。例如,您可以使用计数器来表示已处理的请求数、已完成的任务数或错误数。
  • 仪表盘: 可以任意上下浮动的数值。例如,您可以使用仪表盘来表示温度或当前内存使用量。
  • 直方图: 对观测值(通常是请求持续时间或响应大小之类的内容)进行采样,并在可配置的桶中对其进行计数。它还提供所有观测值的总和。
  • 摘要: 与直方图类似,摘要对观测值(通常是请求持续时间和响应大小之类的内容)进行采样。虽然它也提供观测值的总计数和所有观测值的总和,但它会计算滑动时间窗口上的可配置分位数。

有关您可以使用的不同指标类型以及何时使用它们的列表和详细描述,请参阅 Prometheus 指标类型

SDK 的自动检测

SDK 自动化了一些检测,以简化开发者和运维人员的体验。本节探讨收集和暴露的默认指标。

Go 运行时指标

SDK 提供 Go 运行时、CPU、内存和进程指标的自动收集和暴露,以简化开发者和运维人员的体验。这些指标在 go_process_ 命名空间下暴露,其中包括一些

  • go_info:有关 Go 环境的信息。
  • go_memstats_alloc_bytes:已分配且仍在使用的字节数。
  • go_goroutines:当前存在的 goroutine 数量。
  • process_cpu_seconds_total:以秒为单位的总用户和系统 CPU 时间。

有关插件自动收集和暴露哪些指标的更多详细信息和最新列表,建议调用 Grafana 的 HTTP API,/api/plugins/:pluginID/metrics。另请参阅 在本地收集和可视化指标,以获取有关如何将指标拉取到 Prometheus 中的更多说明。

请求指标

SDK 提供了一个名为 grafana_plugin_request_total 的新计数器指标的自动收集和暴露,允许跟踪每个端点(QueryDataCallResourceCheckHealth 等)、status(ok、cancelled、error)、status_source(plugin、downstream)的插件请求成功率。通过调用 Grafana HTTP API,/api/plugins/:pluginID/metrics 获取的指标示例输出

# HELP grafana_plugin_request_total The total amount of plugin requests
# TYPE grafana_plugin_request_total counter
grafana_plugin_request_total{endpoint="queryData",status="error",status_source="plugin"} 1
grafana_plugin_request_total{endpoint="queryData",status="ok",status_source="plugin"} 4

在插件中实现指标

Grafana 插件 SDK for Go 使用 Prometheus instrumentation library for Go applications默认注册表 中注册的任何自定义指标都将被 SDK 拾取,并通过 收集指标功能 公开。

为了方便起见,建议在创建自定义指标时使用 promauto 包,因为它会自动在 默认注册表 中注册指标,并将其公开给 Grafana。

示例

以下示例展示了如何定义和使用名为 grafana_plugin_queries_total 的自定义计数器指标,该指标跟踪每个查询类型的查询总数。

package plugin

import (
"context"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"

"github.com/grafana/grafana-plugin-sdk-go/backend"
)

var queriesTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: "grafana_plugin",
Name: "queries_total",
Help: "Total number of queries.",
},
[]string{"query_type"},
)

func (ds *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
for _, q := range req.Queries {
queriesTotal.WithLabelValues(q.QueryType).Inc()
}
}

最佳实践

  • 考虑使用命名空间 grafana_plugin,这将为任何定义的指标名称添加 plugin 前缀。这将使运维人员清楚地知道,任何名为 grafana_plugin 的指标都来自 Grafana 插件。
  • 命名指标时使用 snake case 样式,例如 http_request_duration_seconds 而不是 httpRequestDurationSeconds
  • 命名指标标签时使用 snake case 样式,例如 status_code 而不是 statusCode
  • 如果指标类型是计数器,则使用 _total 后缀命名,例如 http_requests_total
  • 如果指标类型是直方图,并且您正在测量持续时间,则使用 _<unit> 后缀命名,例如 http_request_duration_seconds
  • 如果指标类型是仪表盘,则将其命名为表示它可以增加和减少的值,例如 http_request_in_flight

验证和清理来自用户输入的输入

如果标签值来自用户输入,则应进行验证和清理。非常重要的是,只允许预定义的标签集,以最大限度地降低高基数问题的风险。使用或允许过多的标签值可能会导致高基数问题。例如,使用用户 ID、电子邮件地址或其他无界值集作为标签很容易产生高基数问题,并导致 Prometheus 中出现大量时间序列。有关标签和高基数的更多信息,请参阅 Prometheus 标签命名

注意不要在标签值中暴露任何敏感信息(密钥、凭据等)。

如果来自用户输入的值是有限的,即当存在一组固定的预期值时,建议验证它是否是这些值之一,否则返回错误。

如果来自用户输入的值是无限的,即该值可以是任何值,则通常不建议将其用作标签,因为前面提到过高基数问题。如果仍然需要,建议验证值的最大长度/大小,并返回错误或通过仅允许一定数量/固定字符集来清理。

在本地收集和可视化指标

请参阅 将指标从 Grafana 后端插件拉取到 Prometheus 中

此外,请参阅 如何收集和可视化日志、指标和追踪

追踪

分布式追踪允许后端插件开发者在其插件中创建自定义 span,然后将它们发送到与主 Grafana 实例相同的端点,并使用相同的传播格式。追踪上下文也从 Grafana 实例传播到插件,因此插件的 span 将与正确的追踪相关联。

Grafana 中的 OpenTelemetry 配置

Grafana 支持用于分布式追踪的 OpenTelemetry。如果 Grafana 配置为使用已弃用的追踪系统(Jaeger 或 OpenTracing),则 SDK 提供的插件中的追踪将被禁用,并在调用 datasource.Manage | app.Manage 时配置。

必须为 Grafana 实例启用和配置 OpenTelemetry。有关更多信息,请参阅 配置 Grafana

有关 OpenTelemetry 提供的所有功能的深入文档,请参阅 OpenTelemetry Go SDK

注意

如果在 Grafana 中禁用了追踪,则 backend.DefaultTracer() 返回一个 no-op tracer。

在插件中实现追踪

当在主 Grafana 实例上启用 OpenTelemetry 追踪并且为插件启用了追踪时,OpenTelemetry 端点地址和传播格式将在启动期间传递给插件。这些参数用于配置全局 tracer。

  1. 使用 datasource.Manageapp.Manage 运行插件以自动配置全局 tracer。使用 CustomAttributes 为默认 tracer 指定任何自定义属性

    func main() {
    if err := datasource.Manage("MY_PLUGIN_ID", plugin.NewDatasource, datasource.ManageOpts{
    TracingOpts: tracing.Opts{
    // Optional custom attributes attached to the tracer's resource.
    // The tracer will already have some SDK and runtime ones pre-populated.
    CustomAttributes: []attribute.KeyValue{
    attribute.String("my_plugin.my_attribute", "custom value"),
    },
    },
    }); err != nil {
    log.DefaultLogger.Error(err.Error())
    os.Exit(1)
    }
    }
  2. 配置追踪后,像这样使用全局 tracer

    tracing.DefaultTracer()

    这将返回一个 OpenTelemetry trace.Tracer,用于创建 span。

    示例

    func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (backend.DataResponse, error) {
    ctx, span := tracing.DefaultTracer().Start(
    ctx,
    "query processing",
    trace.WithAttributes(
    attribute.String("query.ref_id", query.RefID),
    attribute.String("query.type", query.QueryType),
    attribute.Int64("query.max_data_points", query.MaxDataPoints),
    attribute.Int64("query.interval_ms", query.Interval.Milliseconds()),
    attribute.Int64("query.time_range.from", query.TimeRange.From.Unix()),
    attribute.Int64("query.time_range.to", query.TimeRange.To.Unix()),
    ),
    )
    defer span.End()

    // ...
    }

SDK 的自动检测

SDK 自动化了一些检测,以简化开发者体验。本节探讨添加到 gRPC 调用和传出 HTTP 请求的默认追踪。

追踪 gRPC 调用

启用追踪后,将为每个 gRPC 调用(QueryDataCallResourceCheckHealth 等)自动创建一个新的 span,无论是在 Grafana 端还是在插件端。插件 SDK 还会将追踪上下文注入到传递给这些方法的 context.Context 中。

您可以通过传递原始 context.Context 来使用 tracing.SpanContextFromContext 检索 trace.SpanContext

func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (backend.DataResponse, error) {
spanCtx := trace.SpanContextFromContext(ctx)
traceID := spanCtx.TraceID()

// ...
}

追踪方法调用

启用追踪后,将为每个方法调用自动创建一个名为 sdk.<endpoint> 的新 span,其中 endpoint 是 QueryDataCallResourceCheckHealth 等。Span 属性可能包括 plugin_idorg_iddatasource_namedatasource_uiduserrequest_status(ok、cancelled、error)、status_source(plugin、downstream)。

追踪传出 HTTP 请求

启用追踪后,TracingMiddleware 也会添加到默认中间件堆栈,用于使用 httpclient.Newhttpclient.NewProvider 创建的所有 HTTP 客户端,除非您指定自定义中间件。此中间件为每个传出的 HTTP 请求创建 span,并提供一些与请求生命周期相关的有用属性和事件。

在本地收集和可视化追踪

请参阅 如何收集和可视化日志、指标和追踪

插件示例

有关具有完整分布式追踪支持的插件的完整示例,请参阅 datasource-http-backend 插件示例

收集和可视化日志、指标和追踪

如果您想在开发插件时使用 Loki、Prometheus 和 Tempo 收集和可视化日志、指标和追踪,请参阅 https://github.com/grafana/grafana/tree/main/devenv/docker/blocks/self-instrumentation,Grafana 维护人员正在使用它。