cosmossdk.io/log/v2 是 Cosmos SDK 的日志包。
从高层看,需要理解三个部分:
log.NewLogger(...)用于创建默认的 Cosmos SDK logger。它基于zerolog。cosmossdk.io/log/v2/slog让你可以用标准库的*slog.Logger来满足同一个 SDKLogger接口。log.NewMultiLogger(...)会把一次日志调用分发到多个 SDK logger。SDK 在服务启动且启用了 OpenTelemetry 日志导出时会使用它。想了解我们如何支持 OpenTelemetry,请阅读遥测文档。
log.NewLogger,它会被自动创建并设置到 sdk.Context 上。
默认 Logger
默认实现是对zerolog 的一个轻量封装。
NewLogger 默认会输出人类可读的控制台日志。服务命令的组装会根据 CLI 配置切换选项,例如:
OutputJSONOption():用于输出 JSON 日志LevelOption(...):用于设置全局日志级别FilterOption(...):用于按模块过滤TraceOption(true):用于在错误日志中包含堆栈追踪VerboseLevelOption(...):用于临时详细模式
module 字段。这个包为此暴露了 log.ModuleKey:
consensus:debug,*:error 这样的值时依赖 module 字段。
结构化上下文
Logger.With(...) 会返回一个带有额外字段的派生 logger:
感知 Context 的日志
v2 的Logger 接口增加了 *Context 方法:
Info、Warn、Error和Debug在记录日志时不会检查context.ContextInfoContext、WarnContext、ErrorContext和DebugContext会使用传入的 context 进行 trace 关联
zerolog 实现,*Context 方法会从 ctx 中提取当前激活的 OpenTelemetry span,并添加:
trace_idspan_id- 存在时添加
trace_flags
Trace 关联
当你希望日志与 span 对齐时,请使用感知 context 的方法。sdk.Context.StartSpan(...)会返回一个新的sdk.Context,其中 Go 的context.Context已更新并包含该 span。- 只有当你用这个更新后的 context 调用 logger 的某个
*Context方法时,logger 才能看到 trace 信息。
*Context 方法,默认 logger 不会把 trace 字段添加到日志记录中。
log/slog
cosmossdk.io/log/v2/slog 是一个适配器,适用于已经持有标准库 *slog.Logger 的代码。
*slog.Logger 满足 Cosmos SDK 的 Logger 接口。过滤、格式化、输出目标以及 handler 行为,都取决于底层 slog.Logger 的配置。
MultiLogger
log.NewMultiLogger(loggers...) 会返回一个 logger,把每次日志调用分发给所有被包装的 logger。
这包括:
- 普通日志方法,例如
Info(...) - 感知 context 的方法,例如
InfoContext(...) With(...),它会为每个被包装的 logger 派生一个子 logger
VerboseModeLogger,那么 SetVerboseMode(...) 也会被转发。
换句话说,MultiLogger 只是做分发。它不会自行合并记录,也不会自行添加新字段。
SDK 何时配置 MultiLogger
MultiLogger 并不会为每个应用自动创建。
在节点服务启动期间,SDK 会先根据 CLI/config 标志构建普通的服务 logger。这个 logger 就是常见的基于 zerolog 的 logger。
随后,SDK 会从 config/otel.yaml 初始化 OpenTelemetry。如果 telemetry.IsOtelLoggerEnabled() 报告全局 OpenTelemetry logger provider 存在活跃的日志处理器或导出器,SDK 就会像这样包装现有的服务 logger:
- 现有的控制台/stdout logger
- 用于导出的、基于 OpenTelemetry 的 logger
otelslog 是什么
otelslog 是 Go log/slog 包的一个 OpenTelemetry bridge。
更具体地说,它提供了一个 slog.Handler 和 slog.Logger,会把 slog.Record 转换为 OpenTelemetry 日志记录,并发送给已配置的 OpenTelemetry logger provider。
在 Cosmos SDK 的启动路径中:
otelslog.NewLogger("")会创建一个由该 bridge 支持的*slog.Loggercosmossdk.io/log/v2/slog.NewCustomLogger(...)会对它进行包装,使其满足 SDK 的Logger接口log.NewMultiLogger(...)会把日志同时分发给普通的zerologlogger 和 OpenTelemetry bridge
slog 原生支持 InfoContext/WarnContext/ErrorContext/DebugContext 方法,otelslog 这一侧会直接接收到 context。这意味着 trace/span 关联由 OpenTelemetry 日志管线处理,SDK 不需要手动把 trace_id 字段注入到这一分支中。
两种常见配置
1. 仅 stdout
如果你没有配置 OpenTelemetry logger provider,日志只会输出到普通的 SDK logger。不过,这并不妨碍你做日志关联。 为了在 Grafana Tempo 和 Loki 这类工具中进行 trace 关联,你可以:- 把 JSON 日志输出到 stdout/stderr。
- 用 OpenTelemetry Collector 的 filelog receiver 这类代理抓取这些日志。
- 把它们转发到 Loki。
- 在日志中按
trace_id字段查询。
trace_id 才会被注入到日志中。
2. 启用 OpenTelemetry 日志导出器
如果otel.yaml 启用了带有真实日志处理器或导出器的 OpenTelemetry 日志管线,SDK 就会配置一个 MultiLogger。
在这种配置下:
- 控制台日志仍然像以前一样工作
- 日志也会通过 OpenTelemetry 导出
- 感知 context 的日志调用也会把 trace 上下文带到 OpenTelemetry 分支中
未来方向
当前 SDK 使用MultiLogger,是因为默认 logger 是 zerolog,而 OpenTelemetry 目前为 slog 提供了 bridge,而不是为 zerolog 提供。
如果未来出现可用且合适的一等 zerolog bridge,那么它很可能会比维护一个单独的分发 logger 更简单。相关讨论:
- https://github.com/rs/zerolog/pull/682
- https://github.com/open-telemetry/opentelemetry-go-contrib/issues/5969
cosmossdk.io/log/v2 is the Cosmos SDK logging package.
At a high level, there are three pieces to understand:
log.NewLogger(...)creates the default Cosmos SDK logger. It is backed byzerolog.cosmossdk.io/log/v2/sloglets you satisfy the same SDKLoggerinterface with a standard library*slog.Logger.log.NewMultiLogger(...)fans one log call out to multiple SDK loggers. The SDK uses this during server startup when OpenTelemetry log exporting is enabled. To learn more about how we support OpenTelemetry, read the Telemetry docs.
log.NewLogger, which is automatically provisioned and set on sdk.Context.
Default Logger
The default implementation is a small wrapper aroundzerolog.
NewLogger writes human-readable console output by default. The server command wiring switches options based on CLI configuration, for example:
OutputJSONOption()for JSON logsLevelOption(...)for a global log levelFilterOption(...)for module-based filteringTraceOption(true)to include stack traces on error logsVerboseLevelOption(...)for temporary verbose mode
module field consistently. The package exposes log.ModuleKey for this:
module field when parsing values such as consensus:debug,*:error.
Structured Context
Logger.With(...) returns a derived logger with additional fields:
Context-Aware Logging
The v2Logger interface adds *Context methods:
Info,Warn,Error, andDebuglog without inspecting acontext.ContextInfoContext,WarnContext,ErrorContext, andDebugContextuse the provided context for trace correlation
zerolog implementation, the *Context methods extract the active OpenTelemetry span from ctx and add:
trace_idspan_idtrace_flagswhen present
Trace Correlation
When you want logs to line up with spans, use the context-aware methods.sdk.Context.StartSpan(...)returns a newsdk.Contextwith the Gocontext.Contextupdated to include the span.- The logger only sees trace information when you call one of the logger’s
*Contextmethods with that updated context.
*Context call, the default logger will not add trace fields to the log record.
log/slog
cosmossdk.io/log/v2/slog is an adapter for code that already has a standard library *slog.Logger.
*slog.Logger satisfy the Cosmos SDK Logger interface. Filtering, formatting, sinks, and handler behavior are whatever the underlying slog.Logger is configured to do.
MultiLogger
log.NewMultiLogger(loggers...) returns a logger that dispatches each log call to every wrapped logger.
That includes:
- ordinary log methods such as
Info(...) - context-aware methods such as
InfoContext(...) With(...), which derives a child logger for each wrapped logger
VerboseModeLogger, SetVerboseMode(...) is also forwarded.
In other words, MultiLogger is just fanout. It does not merge records or add new fields on its own.
When The SDK Configures MultiLogger
MultiLogger is not created for every app automatically.
During the node’s server start, the SDK first builds the normal server logger from CLI/config flags. That logger is the usual zerolog-backed logger.
Then the SDK initializes OpenTelemetry from config/otel.yaml. If telemetry.IsOtelLoggerEnabled() reports that the global OpenTelemetry logger provider has active log processors/exporters, the SDK wraps the existing server logger like this:
- the existing console/stdout logger
- an OpenTelemetry-backed logger for export
What otelslog Is
otelslog is an OpenTelemetry bridge for Go’s log/slog package.
More specifically, it provides a slog.Handler and slog.Logger that convert slog.Record values into OpenTelemetry log records and sends them to the configured OpenTelemetry logger provider.
In the Cosmos SDK startup path:
otelslog.NewLogger("")creates an*slog.Loggerbacked by that bridgecosmossdk.io/log/v2/slog.NewCustomLogger(...)wraps it so it satisfies the SDKLoggerinterfacelog.NewMultiLogger(...)fans logs out to both the normalzerologlogger and the OpenTelemetry bridge
slog has native InfoContext/WarnContext/ErrorContext/DebugContext methods, the otelslog side receives the context directly. That means trace/span correlation is handled by the OpenTelemetry logging pipeline without the SDK needing to manually inject trace_id fields into that branch.
Two Common Setups
1. Stdout only
If you do not configure an OpenTelemetry logger provider, logs only go to the normal SDK logger output. This does not restrict you from log correlation, however. For trace correlation in tools such as Grafana Tempo and Loki, you can:- Emit JSON logs to stdout/stderr.
- Scrape those logs with an agent such as the OpenTelemetry Collector filelog receiver.
- Forward them to Loki.
- Query by the
trace_idfield in the logs.
trace_id is only injected into the log if a contextual method was called with a context that contains an active span.
2. OpenTelemetry log exporter enabled
Ifotel.yaml enables an OpenTelemetry log pipeline with real log processors/exporters, the SDK configures a MultiLogger.
In that setup:
- console logging still works as before
- logs are also exported through OpenTelemetry
- context-aware log calls carry trace context into the OpenTelemetry branch as well
Future Direction
Today the SDK uses aMultiLogger because the default logger is zerolog, while OpenTelemetry currently offers a bridge for slog rather than zerolog.
If a first-class zerolog bridge becomes available and suitable, that would likely be a simpler export path than maintaining a separate fanout logger. Relevant discussion: