moby OTEL 观测栈实战:用 contrib/otel 演示栈验证 dockerd 的分布式追踪
contrib/otel 是 moby 仓库内置的一个演示用 Compose 栈,用于端到端验证 Moby(dockerd)的 OpenTelemetry(OTEL)追踪功能:它同时拉起 OTEL Collector、Jaeger 和 Aspire Dashboard 三个服务,让 dockerd 产生的 Trace 可以在两个可视化面板中查看。读完本文,你将掌握这套演示栈的完整配置与启动步骤,并理解 dockerd 侧从环境变量到 TracerProvider 再到 HTTP 协议导出的完整实现链路,从而能自主调通或排查 Moby 的 OTEL 可观测性。
1. 演示栈的组成与设计意图
按照 contrib/otel/README.md 的说明,这个小型 Compose 栈包含三个容器:
- OTEL Collector:接收 dockerd 上报的 Trace(OTLP 协议);
- Jaeger:可视化展示 Trace 的追踪系统;
- Aspire Dashboard:另一个可选的 Trace 可视化面板;
Collector 被配置为把 Traces 同时导出到 Jaeger 和 Aspire 两个后端。目录中的两个核心文件分别是 contrib/otel/compose.yaml(服务编排)和 contrib/otel/otelcol.yaml(Collector 配置)。
2. compose.yaml:三个服务的端口与联动关系
完整的服务编排见 contrib/otel/compose.yaml,各服务的关键配置如下:
| 服务 | 镜像 | 宿主机端口 | 作用 |
|---|---|---|---|
jaeger |
jaegertracing/all-in-one:latest |
16686 |
Jaeger UI,可视化 Trace |
aspire-dashboard |
mcr.microsoft.com/dotnet/nightly/aspire-dashboard |
18888 |
Aspire Dashboard UI |
otelcol |
otel/opentelemetry-collector-contrib:latest |
4318(OTLP HTTP 默认端口) |
接收 Trace 并转发给两个后端 |
几个值得注意的细节:
- Aspire Dashboard 的匿名访问开关。Dashboard 容器通过环境变量
DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS: 'true'允许匿名访问,这样在本地调试时无需处理 OAuth2 凭据。 - 端口映射不对称。宿主机只暴露了 Jaeger 的
16686和 Aspire 的18888(UI 端口),而 Collector 对外只暴露4318(OTLP HTTP 端口)。Collector 到 Jaeger/Aspire 的转发走的是 compose 内部网络,对应otelcol.yaml中的jaeger:4317与aspire-dashboard:18889。 develop.watch热加载机制。Compose 文件中为otelcol服务配置了:
develop:
watch:
- action: sync+restart
path: ./otelcol.yaml
target: /etc/otelcol-contrib/config.yaml
这意味着本地修改 otelcol.yaml 后,Compose 会自动把文件同步进容器并重启 Collector 服务,方便在调试 Collector 配置时省去手动拷贝、重启的循环。
3. otelcol.yaml:接收端与双路导出
Collector 的完整配置见 contrib/otel/otelcol.yaml,原文注释明确写着 “Receive signals over gRPC and HTTP, moby currently uses http”,即 moby 当前使用 HTTP 上报,但 Collector 两种协议都开了监听:
# Receive signals over gRPC and HTTP
# moby currently uses http
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls::insecure: true
otlp/aspire:
endpoint: aspire-dashboard:18889
tls::insecure: true
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp/jaeger, otlp/aspire]
- receivers:
otlp接收器同时监听4317(gRPC)和4318(HTTP),demo 栈实际使用的是 HTTP。 - exporters:两个
otlp/*导出器分别把 Trace 以 gRPC(OTLP)方式发给 Jaeger(jaeger:4317)和 Aspire Dashboard(aspire-dashboard:18889,注意 Aspire 的 OTLP 监听端口是18889,与 UI 端口18888不同);tls::insecure: true表示内部网络明文传输,仅适合本地演示。 - pipelines:
traces管道把otlp接收器的数据同时喂给两个导出器,这就是 README 中 “export Traces to both the Jaeger and Aspire containers” 的落地实现。
此外,仓库根目录还有一份 otelcol-ci-config.yml,是 CI 场景下的 Collector 配置:同样监听 4317/4318,但经过 batch 处理器后用 file 导出器落盘为 JSON,并注释说明单文件轮转上限设为 18MB 是因为 “Jaeger will reject file uploads larger than 20MB by default”——这提示了 CI 中把 Trace 文件上传 Jaeger 调试时的体积约束。
4. 启动步骤(继承自原文档)
按照 contrib/otel/README.md “How can I use it?” 一节的完整流程:
-
导出覆盖 OTLP 端点的环境变量:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318如果在 devcontainer 或其他环境中工作,可能需要调整把这个环境变量传递给 daemon 的方式。
-
启动你想抓取 Trace 的 moby 引擎(确保它拿到了上一步声明的环境变量);
-
启动 otel 演示栈:在
contrib/otel/目录下运行docker compose up -d -
用 Docker CLI 对引擎发起一些调用,让 daemon 产生 Trace 并发送到端点;
-
打开可视化面板:Jaeger 在
http://localhost:16686,Aspire Dashboard 在http://localhost:18888/traces; -
在面板左上角的下拉框中选择
dockerd即可看到引擎的 Trace。
原文档同时给出一个提示:具体步骤会因你的工作方式(编译二进制后本地运行、在 devcontainer 中运行/调试等)而略有差异。
关于第 1 步的环境变量传递,仓库的 Makefile 中有对应佐证:dev/test 容器启动时通过 -e OTEL_EXPORTER_OTLP_ENDPOINT、-e OTEL_EXPORTER_OTLP_PROTOCOL、-e OTEL_SERVICE_NAME 将这三个 OTEL 相关变量透传进容器,这正是 devcontainer 工作流下“让 daemon 拿到环境变量”的具体机制。
清理:在 contrib/otel/ 目录运行 docker compose down 即可销毁演示栈;随后可用 unset OTEL_EXPORTER_OTLP_ENDPOINT 清掉环境中的 OTLP 变量。
5. 源码剖析:dockerd 如何产生并导出 Trace
演示栈只是“接收端”,真正决定能否看到 Trace 的是 dockerd 侧的实现。下面按调用链拆解。
5.1 启动阶段:服务名、传播器与 TracerProvider
在 daemon/command/daemon.go 中,daemon 启动时做了三件关键事情:
- 兜底设置
OTEL_SERVICE_NAME:若环境变量未设置,则用可执行文件名(filepath.Base(os.Args[0]))作为服务名——这就是为什么在 Jaeger 中能看到名为dockerd的服务。 - 设置文本传播器:
otel.SetTextMapPropagator(...)组合了 W3CTraceContext与Baggage两种传播器。 - 初始化 TracerProvider 并挂上日志钩子:调用
otelutil.NewTracerProvider(ctx, true)后经otel.SetTracerProvider(tp)全局注册,并通过tracing.NewLogrusHook()把 Trace 上下文注入 logrus 日志。
同文件中还有 setOTLPProtoDefault()(见 daemon/command/daemon.go#L437-L457):buildkit 的 detect 包默认协议是 gRPC(符合旧版 OTEL 规范),而现行规范默认是 http/protobuf,因此该函数在 OTEL_EXPORTER_OTLP_PROTOCOL 未设置时,把 OTEL_EXPORTER_OTLP_TRACES_PROTOCOL 与 OTEL_EXPORTER_OTLP_METRICS_PROTOCOL 都默认设为 http/protobuf。这与 otelcol.yaml 注释 “moby currently uses http” 相互印证:demo 栈把 HTTP 端口 4318 作为默认 OTLP 端点正是因此。
5.2 导出器探测:环境变量如何触发 OTLP 导出
TracerProvider 由 daemon/internal/otelutil/provider.go 的 NewTracerProvider 创建,其核心逻辑:
- 调用 buildkit 的
detect.NewSpanExporter(ctx)探测导出器;探测失败且allowNoop为 true 时,降级为noop.NewTracerProvider(),并打印 “Failed to initialize tracing, skipping”; - 若检测到的是 none 导出器(即未配置任何 OTLP 端点),同样返回 no-op provider 并打印 “OTEL tracing is not configured, using no-op tracer provider”;
- 否则构建
sdktrace.NewTracerProvider,配置包含:默认 Resource、detect.Recorder同步器(供 buildkit 记录)、sdktrace.WithBatcher(exp)批处理导出器,以及baggagecopy处理器(把所有 Baggage 成员复制到每个 Span 上,便于在 UI 中过滤)。
探测逻辑本身在 vendored 的 buildkit 代码 vendor/github.com/moby/buildkit/util/tracing/detect/otlp.go 中:DetectTraceExporter 只有当 OTEL_TRACES_EXPORTER=otlp、OTEL_EXPORTER_OTLP_ENDPOINT 或 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT 三者之一非空时才启用 OTLP 导出;协议取 OTEL_EXPORTER_OTLP_TRACES_PROTOCOL(未设则回退 OTEL_EXPORTER_OTLP_PROTOCOL,再未设默认 grpc,支持 grpc 与 http/protobuf 两种取值)。由于 daemon 启动前会先执行 setOTLPProtoDefault() 把协议默认值改写成 http/protobuf,所以第 4 节中只需设置 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 一个变量,Trace 就会走 OTLP HTTP 打到 Collector 的 4318 端口——整条链路在这里闭合。
5.3 上下文跨进程传播:TRACEPARENT / TRACESTATE
Trace 并非只停留在 daemon 内部。daemon/internal/otelutil/environ_carrier.go 实现了一个基于环境变量的 TextMapCarrier:把 traceparent/tracestate 两个 W3C 头部映射到 TRACEPARENT/TRACESTATE 环境变量(EnvironCarrier.Environ() / PropagateFromEnvironment()),用于把 Trace 上下文传给外部进程(如插件、构建后端)并在其返回时延续同一条 Trace。配合 daemon/command/httphandler.go 中为 daemon 的 gRPC 服务挂载的 otelgrpc StatsHandler(以及 TraceService/Export 的免追踪直通拦截,避免客户端上报 Trace 时造成递归),moby 的 API 请求、gRPC 调用与 buildkit 构建都能挂到统一的 Trace 树上。
6. 小结与适用边界
contrib/otel是一套“最小可运行”的 OTEL 验证环境:一个 Collector + 两个后端,靠 contrib/otel/compose.yaml 的4318HTTP 端点与 contrib/otel/otelcol.yaml 的双导出器,把 dockerd 的 Trace 同时送进 Jaeger(localhost:16686)与 Aspire Dashboard(localhost:18888/traces)。- 能否出 Trace 的关键在 daemon 环境:
OTEL_EXPORTER_OTLP_ENDPOINT决定导出目标,协议默认被 daemon/command/daemon.go#L437-L457 修正为http/protobuf,OTEL_SERVICE_NAME未设置时兜底为可执行文件名。 - 若未配置任何 OTLP 端点,daemon/internal/otelutil/provider.go 会安全降级为 no-op TracerProvider,对 daemon 性能与行为无影响——因此该功能属于“可选启用”的观测能力。
- 适用前提:需要能启动一个带该环境变量的 dockerd(本地二进制或 devcontainer),以及 Docker Compose v2(
compose.yaml使用了develop.watch语法,依赖较新版本的 Compose 支持)。清理只需docker compose down加unset OTEL_EXPORTER_OTLP_ENDPOINT。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00