跳至主要内容

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

为后端插件添加日志指标追踪,使插件开发者和 Grafana 操作员都能更容易诊断和解决问题。本文档提供了指导、约定和最佳实践,以帮助您有效地对插件进行插桩,以及在安装插件后如何访问这些数据。

注意

本文档要求您至少使用 grafana-plugin-sdk-go v0.246.0。但是,建议将 Grafana Plugin 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.Logger,来自 backend 包适用于大多数用例并可在任何地方使用。

示例

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

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.Logger 是来自 log 包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))
}

上面的示例每次调用 QueryData 并包含 2 个查询时,会输出类似以下内容。

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 函数。

示例

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

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")
}

上面的示例每次调用 QueryData 并包含 2 个查询时,会输出类似以下内容。

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 struct 作为值时,尤其容易意外发生。

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

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

何时使用哪种日志级别?

  • 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 支持四种不同的指标类型,您可以使用它们

  • Counter: 只能增加或在重启时重置为零。例如,您可以使用计数器表示服务的请求数、完成的任务数或发生的错误数。
  • Gauge: 可以任意增减的数值。例如,您可以使用量规表示温度或当前内存使用情况。
  • Histogram: 对观测值(通常是请求持续时间或响应大小等)进行抽样,并在可配置的桶中计数。它还提供所有观测值的总和。
  • Summary: 与直方图类似,摘要(Summary)对观测值(通常是请求持续时间或响应大小等)进行抽样。虽然它也提供观测值的总计数和所有观测值的总和,但它会在滑动时间窗口上计算可配置的分位数。

有关可使用的不同指标类型及其使用时机的列表和详细描述,请参阅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 的新计数器指标的自动收集和暴露,允许按端点(QueryDataCallResourceCheckHealthcollectMetricsrunStreamsubscribeStreampublishStream)、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 plugin 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.Manageapp.Manage 时进行配置。

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

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

注意

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

在插件中实现追踪

当主 Grafana 实例上启用 OpenTelemetry 追踪且插件也启用追踪时,OpenTelemetry 端点地址和传播格式会在插件启动时传递给插件。这些参数用于配置一个全局追踪器。使用 datasource.Manageapp.Manage 运行插件可以自动配置全局追踪器。使用 CustomAttributes 指定默认追踪器的任何自定义属性。

  1. 配置追踪后,像这样使用全局追踪器:

    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 := backend.DefaultTracer()

    tracing.DefaultTracer()

    这返回一个用于创建 span 的 OpenTelemetry trace.Tracer

    示例

    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 调用追踪

启用追踪后,Grafana 端和插件端都会为每个 gRPC 调用(QueryDataCallResourceCheckHealthcollectMetricsrunStreamsubscribeStreampublishStream 等)自动创建一个新的 span。插件 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 是 QueryDataCallResourceCheckHealthcollectMetricsrunStreamsubscribeStreampublishStream。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 维护者正在使用的。