为后端插件添加日志、指标和跟踪
为后端插件添加 日志、指标 和 跟踪,使插件开发者和 Grafana 运维人员更容易诊断和解决问题。本文档提供了指导、约定和最佳实践,以帮助您有效地为插件添加监控,以及如何在插件安装时访问这些数据。
本文件要求您使用至少 grafana-plugin-sdk-go v0.246.0。然而,建议您保持 Grafana Go 插件 SDK 的最新状态,以获取最新的改进、安全和错误修复。有关更新说明,请参阅 更新 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 包 的全局日志记录器 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 包 的 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
使用上下文日志记录器
使用上下文日志记录器自动包含附加到 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))
}
每次调用 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 函数。
示例
以下示例通过添加额外的上下文键值对并允许将它们传播到其他方法(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")
}
每次调用 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 风格;例如,使用
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、已取消、错误)和status_source
(插件、下游)。通过调用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使用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
时进行配置。
必须启用并配置 OpenTelemetry 以用于 Grafana 实例。有关更多信息,请参阅 配置 Grafana。
有关 OpenTelemetry 提供的所有功能的详细文档,请参阅 OpenTelemetry Go SDK。
如果 Grafana 中禁用了追踪,则 backend.DefaultTracer()
返回一个 no-op 追踪器。
在您的插件中实现追踪
当主 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 调用
当启用追踪时,Grafana 和插件端都会为每个 gRPC 调用(例如 QueryData
、CallResource
、CheckHealth
等)自动创建一个新的跨度。插件 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>
的方法调用(其中 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 客户端默认中间件堆栈中,除非您指定了自定义中间件。此中间件为每个出站 HTTP 请求创建跨度,并提供一些与请求生命周期相关的有用属性和事件。
本地收集和可视化跟踪
参考 如何收集和可视化日志、指标和跟踪。
插件示例
参考 datasource-http-backend 插件示例,以获取一个支持完整分布式跟踪的插件的完整示例。
收集和可视化日志、指标和跟踪
如果您在开发插件时想使用 Loki、Prometheus 和 Tempo 收集和可视化日志、指标和跟踪,请参考 Grafana 维护者正在使用的 https://github.com/grafana/grafana/tree/main/devenv/docker/blocks/self-instrumentation。