为后端插件添加日志、指标和跟踪
为后端插件添加 日志、指标 和 跟踪 有助于插件开发者和 Grafana 操作员更轻松地诊断和解决问题。本文档提供了指导、约定和最佳实践,以帮助您有效地为插件添加监控,以及如何在安装插件后访问这些数据。
本文档要求您至少使用 grafana-plugin-sdk-go v0.246.0。但是,建议您将 Grafana 插件 SDK for Go 保持最新状态,以获取最新的改进、安全性和错误修复。有关更新说明,请参阅 更新 Go SDK。
日志
日志是记录软件环境中发生的事件、警告和错误的文件。大多数日志包含上下文信息,例如事件发生的时间以及与之关联的用户或端点。
SDK 的自动检测
SDK 自动执行一些检测以简化开发者和操作员的体验。每次方法调用(QueryData
、CallResource
、CheckHealth
等)完成后,都会记录一条 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
可用于所有情况和大多数用例。
示例
以下示例显示了全局日志记录器与不同严重级别和键值对的基本用法。
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.DefaultLogger
的便捷包装器,您也可以使用它来访问全局日志记录器。
重用具有特定键/值对的日志记录器
您可以记录多条消息并在不重复代码的情况下包含某些键值对,例如,当您想根据数据源在 Grafana 中的配置方式在每条日志消息中包含一些特定的键值对时。为此,请使用已实例化的日志记录器上的 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
使用上下文日志记录器
使用上下文日志记录器可自动包含附加到 context.Context
的其他键值对。例如,您可以使用 traceID
将日志与跟踪相关联,并将日志与一个通用标识符相关联。您可以通过在已实例化的日志记录器上使用 FromContext
方法创建新的上下文日志记录器;当 重用具有特定键/值对的日志记录器 时,您也可以将此方法组合使用。我们建议在可以访问 context.Context
时始终使用上下文日志记录器。
默认情况下,在使用上下文日志记录器时,日志中会包含以下键值对
- **pluginID:** 插件标识符。例如,
grafana-github-datasource
。 - **endpoint:** 正在处理的请求;即,
callResource
、checkHealth
、collectMetrics
、queryData
、runStream
、subscribeStream
或publishStream
。 - **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 样式;例如,
remoteAddr
或userID
,以与 Go 标识符保持一致。 - 记录 Go 错误时,使用键
error
;例如,logger.Error("Something failed", "error", errors.New("An error occurred")
。 - 在可以访问
context.Context
时始终使用上下文日志记录器。 - 不要记录敏感信息,例如数据源凭据或 IP 地址,或其他个人身份信息。
验证和清理来自用户输入的数据
如果日志消息或键值对源自用户输入,则应对其进行验证和清理。注意不要在日志消息中公开任何敏感信息(密钥、凭据等)。当包含 Go 结构作为值时,很容易不小心这样做。
如果源自用户输入的值是有界的,即当有一组固定的预期值时,建议验证它是否为这些值之一,否则返回错误。
如果源自用户输入的值是无界的,即当值可以是任何内容时,建议验证值的最大长度/大小并返回错误或通过仅允许一定数量/固定字符集来清理。
何时使用哪个日志级别?
- **调试:** 高频信息消息和正常操作期间不太重要的消息。
- **信息:** 低频信息消息和重要消息。
- **警告:** 可以恢复的错误/状态,而不会中断操作。如果使用,它应该是可操作的,以便操作员可以执行某些操作来解决它。
- **错误:** 指示某些操作失败(带有错误)并且程序无法处理该错误的错误消息。
高频的传入请求通常在 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
的新计数器指标的自动收集和公开,允许跟踪每个端点(QueryData
、CallResource
、CheckHealth
等)的插件请求成功率、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
在插件中实现指标
用于 Go 的 Grafana 插件 SDK 使用 用于 Go 应用程序的 Prometheus 检测库。使用 默认注册表 注册的任何自定义指标都将被 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 插件。 - 在命名指标时使用蛇形命名法,例如
http_request_duration_seconds
而不是httpRequestDurationSeconds
。 - 在命名指标标签时使用蛇形命名法,例如
status_code
而不是statusCode
。 - 如果指标类型是计数器,请使用
_total
后缀为其命名,例如http_requests_total
。 - 如果指标类型是直方图,并且您正在测量持续时间,请使用
_<unit>
后缀为其命名,例如http_request_duration_seconds
。 - 如果指标类型是仪表盘,请将其命名为表示其可以增加和减少的值,例如
http_request_in_flight
。
验证和清理来自用户输入的输入
如果标签值源自用户输入,则应对其进行验证和清理。仅允许一组预定义的标签以最大程度地降低高基数问题的风险非常重要。使用或允许过多的标签值可能会导致高基数问题。例如,使用用户 ID、电子邮件地址或其他无界的值集作为标签可能会很容易地造成高基数问题,并导致 Prometheus 中出现大量的时间序列。有关标签和高基数的更多信息,请参阅 Prometheus 标签命名。
注意不要在标签值中公开任何敏感信息(机密、凭据等)。
如果源自用户输入的值是有界的,即存在一组固定的预期值,建议验证它是否为这些值之一,否则返回错误。
如果源自用户输入的值是无界的,即该值可以是任何内容,通常不建议将其用作标签,因为前面提到了高基数问题。如果仍然需要,建议验证值的最大长度/大小并返回错误或通过仅允许一定数量/固定字符集进行清理。
在本地收集和可视化指标
请参阅 将指标从 Grafana 后端插件提取到 Prometheus 中。
此外,请参阅 如何收集和可视化日志、指标和跟踪。
跟踪
分布式跟踪允许后端插件开发人员在其插件中创建自定义跨度,然后将其发送到与主 Grafana 实例相同的端点并使用相同的传播格式。跟踪上下文也从 Grafana 实例传播到插件,因此插件的跨度将与正确的跟踪相关联。
Grafana 中的 OpenTelemetry 配置
Grafana 支持 OpenTelemetry 用于分布式跟踪。如果 Grafana 配置为使用已弃用的跟踪系统(Jaeger 或 OpenTracing),则 SDK 提供的插件中的跟踪将被禁用,并在调用 datasource.Manage | app.Manage
时进行配置。
必须为 Grafana 实例启用和配置 OpenTelemetry。有关更多信息,请参阅 配置 Grafana。
有关 OpenTelemetry 提供的所有功能的深入文档,请参阅 OpenTelemetry Go SDK。
如果在 Grafana 中禁用跟踪,backend.DefaultTracer()
将返回一个无操作跟踪器。
在插件中实现跟踪
当在主 Grafana 实例上启用 OpenTelemetry 跟踪并在插件中启用跟踪时,OpenTelemetry 端点地址和传播格式将在启动期间传递到插件。这些参数用于配置全局跟踪器。
-
使用
datasource.Manage
或app.Manage
运行您的插件以自动配置全局跟踪器。使用CustomAttributes
指定默认跟踪器的任何自定义属性。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)
}
} -
配置完跟踪后,请按如下方式使用全局跟踪器。
tracing.DefaultTracer()
这将返回一个 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 调用
启用跟踪后,将为每个 gRPC 调用(QueryData
、CallResource
、CheckHealth
等)自动创建一个新的跨度,在 Grafana 端和插件端都是如此。插件 SDK 还将跟踪上下文注入传递给这些方法的 context.Context
中。
您可以使用 tracing.SpanContextFromContext
通过将原始 context.Context
传递给它来检索 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>
的方法调用自动创建一个新的跨度,其中端点为 QueryData
、CallResource
、CheckHealth
等。跨度属性可能包括 plugin_id
、org_id
、datasource_name
、datasource_uid
、user
、request_status
(ok、cancelled、error)、status_source
(plugin、downstream)。
跟踪传出 HTTP 请求
启用跟踪时,还会将一个TracingMiddleware
添加到所有使用httpclient.New
或httpclient.NewProvider
创建的默认中间件栈中,除非您指定自定义中间件。此中间件为每个传出的 HTTP 请求创建跨度,并提供一些与请求生命周期相关的有用属性和事件。
本地收集和可视化跟踪
请参考如何收集和可视化日志、指标和跟踪。
插件示例
请参考datasource-http-backend 插件示例,了解具有完整分布式跟踪支持的插件的完整示例。
收集和可视化日志、指标和跟踪
如果您希望在开发插件时使用 Loki、Prometheus 和 Tempo 收集和可视化日志、指标和跟踪,请参考https://github.com/grafana/grafana/tree/main/devenv/docker/blocks/self-instrumentation,Grafana 维护人员正在使用这些工具。