2025年11月10日·阅读约1分钟

使用 Go 与 OpenTelemetry 实现端到端 API 可观测性

讲解 Go 的 OpenTelemetry 跟踪,并给出实用步骤,帮助你在 HTTP 请求、后台任务和第三方调用之间关联 trace、metrics 与日志。

使用 Go 与 OpenTelemetry 实现端到端 API 可观测性

对于 Go API 来说,端到端追踪意味着什么

一个 trace 是一个请求在系统中流动的时间线。它从 API 调用到达开始,到你发送响应结束。

在 trace 里有若干 span。span 是一个有时间的步骤,比如“解析请求”、“执行 SQL”或“调用支付提供商”。span 还可以携带有用的细节,例如 HTTP 状态码、可安全使用的用户标识符,或查询返回的行数。

“端到端”意味着 trace 不会在第一个处理器处停止。它会跟随请求通过那些问题通常隐藏的地方:中间件、数据库查询、缓存调用、后台任务、第三方 API(支付、邮件、地图)和其他内部服务。

追踪在问题偶发时最有价值。如果 200 次请求中有一次很慢,日志在快慢情况下常常几乎相同。trace 会把差异显现出来:某个请求在外部调用上等待了 800 ms,重试了两次,然后触发了后续任务。

跨服务连接日志也很难。你可能在 API 有一条日志,在 worker 有另一条,中间什么都没有。通过追踪,这些事件共享相同的 trace ID,你可以无须猜测地沿链路查找。

Trace、Metrics 与 Logs:它们如何协同

Trace、metrics 和 logs 回答的是不同的问题。

Trace 显示某个真实请求发生了什么。它告诉你时间花在处理器、数据库调用、缓存查找和第三方请求的哪一部分。

Metrics 显示趋势。它们是告警的最佳工具,因为它们稳定且便于聚合:延迟百分位、请求速率、错误率、队列深度和饱和度。

Logs 以纯文本说明“为什么”:校验失败、意外输入、边缘情况以及代码做出的决策。

真正的价值在于关联。当相同的 trace ID 出现在 spans 和结构化日志中时,你可以从一条错误日志跳到确切的 trace,立刻看到是哪个依赖变慢或哪个步骤失败。

一个简单的心智模型

用每种信号来做它最擅长的事:

  • Metrics 告诉你某处出现了问题。
  • Trace 展示单次请求的时间去向。
  • Logs 解释代码为什么做出某个决定。

举例:你的 POST /checkout 端点开始超时。Metrics 显示 p95 延迟飙升。一个 trace 显示大部分时间花在支付提供商的调用上。该 span 内相关的日志行显示由于 502 在重试,这会指向退避设置或上游故障。

在添加代码之前:命名、抽样与追踪内容

事先做一点规划可以让稍后能搜索到 trace。否则你仍然会收集数据,但基本问题会变得难以回答:“这是 staging 还是 prod?”“哪个服务发起了问题?”

先从一致的身份开始。为每个 Go API 选一个明确的 service.name(例如 checkout-api),以及一个统一的环境字段,例如 deployment.environment=dev|staging|prod。保持这些稳定。如果名称在一周中途改变,图表和搜索会看起来像不同的系统。

接着,决定采样策略。在开发环境追踪每个请求很好,但在生产通常代价太高。常见做法是对正常流量采样一小部分,并保留错误和慢请求的追踪。如果你已知某些端点流量很高(健康检查、轮询),就降低或取消对它们的追踪。

最后,商定你会在 span 上标记什么以及绝不会收集什么。保留一个简短的允许属性白名单,用于跨服务连接事件,并制定简单的隐私规则。

好的标签通常包含稳定 ID 和粗粒度的请求信息(路由模板、方法、状态码)。完全避免敏感负载:密码、支付数据、完整邮箱、鉴权令牌和原始请求体。如果必须包括与用户相关的值,请在添加前先哈希或脱敏。

逐步操作:在 Go HTTP API 中添加 OpenTelemetry 追踪

你将在启动时设置一次 tracer provider。它决定 span 去向以及每个 span 附带的 resource 属性。

1) 初始化 OpenTelemetry

确保设置了 service.name。没有它,不同服务的 trace 会混在一起,图表很难阅读。

// main.go (startup)
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())

res, _ := resource.New(context.Background(),
	resource.WithAttributes(
		semconv.ServiceName("checkout-api"),
	),
)

tp := sdktrace.NewTracerProvider(
	sdktrace.WithBatcher(exp),
	sdktrace.WithResource(res),
)

otel.SetTracerProvider(tp)

这是 Go OpenTelemetry 追踪的基础。接下来,你需要为每个传入请求创建一个 span。

2) 添加 HTTP 中间件并捕获关键字段

使用会自动启动 span 并记录状态码与持续时间的 HTTP 中间件。用路由模板(比如 /users/:id)来设置 span 名称,而不是原始 URL,否则你会得到成千上万的唯一路径。

目标是一个干净的基线:每个请求一个 server span,基于路由的 span 名称、记录 HTTP 状态、处理器失败反映为 span 错误,以及在 trace 查看器中能看到持续时间。

3) 让失败明显可见

当出现错误时,返回错误并将当前 span 标记为失败。这能让 trace 在你查看日志前就凸显出来。

在处理器中,你可以这样做:

span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())

4) 在本地验证 trace ID

运行 API 并请求一个端点。把请求上下文里的 trace ID 记录到日志中以确认它会随每次请求变化。如果总是为空,说明你的中间件没有使用处理器收到的相同上下文。

在数据库与第三方调用中传递上下文

原型:带追踪的结账
创建一个具备明确 span 边界、重试与超时策略的结账服务原型。
开始构建

当你丢失 context.Context 时,端到端可视化就断裂了。传入请求的上下文应该是你传给每个 DB 调用、HTTP 调用和辅助函数的线索。如果你用 context.Background() 替代或忘记向下传递,上下文就断开,trace 会变成相互独立的工作片段。

对于出站 HTTP,请使用已仪表化的 transport,这样每个 Do(req) 都会成为当前请求下的子 span。在外发请求上转发 W3C trace 头,以便下游服务能够将它们的 span 附加到同一个 trace。

数据库调用也需要相同处理。使用已仪表化的驱动或在 QueryContextExecContext 周围用 span 包裹。只记录安全的细节。你想找到慢查询,但不要泄露数据。

有用且低风险的属性包括操作名(例如 SELECT user_by_id)、表或模型名、返回行数(仅计数)、持续时间、重试次数以及粗粒度的错误类型(超时、已取消、约束错误)。

超时是故事的一部分,不只是失败。为 DB 和第三方调用用 context.WithTimeout 设置超时,并让取消向上冒泡。当调用被取消时,将 span 标记为错误并添加简短原因,如 deadline_exceeded

追踪后台任务与队列

拥有源代码控制权
生成可投入生产的代码,并以你团队偏好的方式导出与监测它们。
试用 AppMaster

后台工作是 trace 常常中断的地方。一个 HTTP 请求结束后,worker 可能在不同机器上稍后接手消息且没有共享上下文。如果不做任何处理,你会得到两个故事:API trace 和看起来无根的任务 trace。

解决方法很直接:在入列任务时,捕获当前的 trace 上下文并把它存入任务元数据(负载、头或属性,视队列而定)。当 worker 启动时,提取该上下文并以原始请求为父开启新的 span。

安全地传播上下文

只复制 trace 上下文,而不是用户数据。

  • 仅注入 trace 标识符和采样标志(W3C traceparent 风格)。
  • 与业务字段分离(例如专门的 "otel" 或 "trace" 字段)。
  • 将其视为不受信任的输入(验证格式,处理缺失数据)。
  • 避免将令牌、邮箱或请求体放入任务元数据。

要添加的 span(不要把 trace 变成噪音)

可读的 trace 通常包含几个有意义的 span,而不是数十个小 span。在边界和“等待点”周围创建 span 是个好起点。一个合理的起点是在 API 处理器中创建一个 enqueue span,在 worker 中创建一个 job.run span。

添加少量上下文:尝试次数、队列名、任务类型和负载大小(不要包含负载内容)。如果发生重试,将其记录为单独的 span 或事件,这样你可以看到退避延迟。

定时任务也需要父 span。如果没有传入请求,为每次运行创建一个新的根 span 并打上计划名称标签。

将日志与 trace 关联(并保证日志安全)

Trace 告诉你时间花到哪儿了。Logs 告诉你发生了什么以及为什么。将它们连接最简单的方法是把 trace_idspan_id 作为结构化字段添加到每条日志中。

在 Go 中,从 context.Context 获取活动 span,并在每次请求(或任务)开始时扩展你的 logger。这样每条日志都指向一个特定的 trace。

span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger := baseLogger.With(
  "trace_id", sc.TraceID().String(),
  "span_id",  sc.SpanID().String(),
)
logger.Info("charge_started", "order_id", orderID)

这就足以从一条日志条目跳转到当时正在运行的确切 span。它也会让缺失上下文变得明显:trace_id 会是空的。

在不泄露 PII 的情况下保持日志有用

日志通常保存更久并传播得更广,所以要更严格。优先使用稳定的标识符和结果:user_idorder_idpayment_providerstatuserror_code。如果必须记录用户输入,请先脱敏并限制长度。

让错误容易聚合

使用一致的事件名和错误类型,这样你可以计数和搜索。如果措辞每次都变, 同一个问题看起来会像许多不同的问题。

添加真正有助于定位问题的指标

将前端连接到可追踪的 API
发布 Web 和移动应用,后端可通过日志中的 trace ID 来排查问题。
立即试用

Metrics 是你的预警系统。在已经使用 Go OpenTelemetry 追踪的设置中,指标应回答:发生频率、严重程度和从何时开始。

从一组对几乎所有 API 都通用的小指标开始:请求计数、错误计数(按状态类)、延迟百分位(p50、p95、p99)、进行中的请求数,以及对数据库和关键第三方调用的依赖延迟。

为让指标与 trace 对齐,使用相同的路由模板和名称。如果你的 span 使用 /users/{id},你的指标也应该使用相同的形式。这样当图表显示“/checkout 的 p95 上升”时,你可以直接跳到筛选为该路由的 trace。

注意标签(属性)。一个不当的标签可能导致成本暴涨并让仪表板变得无用。路由模板、方法、状态类和服务名通常是安全的。用户 ID、邮箱、完整 URL 和原始错误消息通常不是。

为关键业务事件添加少量自定义指标(例如 checkout 开始/完成、按结果码分组的支付失败、后台任务成功与重试)。保持集合精简,删除从不用的数据。

导出遥测与逐步发布

导出是 OpenTelemetry 变得实际的地方。你的服务必须把 spans、metrics 和 logs 发送到可靠的地方,且不能阻塞请求。

本地开发保持简单。控制台导出(或将 OTLP 发送到本地 collector)可以快速查看 trace 并验证 span 名称与属性。在生产环境,优先使用 OTLP 发到代理或 OpenTelemetry Collector 靠近服务的地方。它提供单点的重试、路由和过滤能力。

批量发送很重要。在短间隔内批量发送遥测,并设置紧凑的超时,以免网络卡住阻塞应用。遥测不应处于关键路径。如果导出器跟不上,应该丢弃数据而不是占用内存。

采样让成本可预测。先采用基于 head 的采样(例如 1-10% 的请求),然后加上简单规则:错误始终采样、慢请求超过阈值时始终采样。对高并发的后台任务,可降低采样率。

分步发布:开发环境 100% 采样,staging 用真实流量且采样较低,生产则采用保守采样并对导出失败设置告警。

那些毁掉端到端可视化的常见错误

规范服务命名
生成真实的 Go 服务并在应用间保持一致的命名与环境标签。
开始使用

端到端可视化最常因为一些简单原因失败:数据存在,但没有连接起来。

在 Go 中破坏分布式追踪的问题通常包括:

  • 在层之间丢失上下文。处理器创建了 span,但 DB 调用、HTTP 客户端或 goroutine 使用了 context.Background() 而不是请求上下文。
  • 返回错误却不标记 span。如果你不记录错误并设置 span 状态,trace 看起来仍然是“绿色”的,即便用户看到 500。
  • 给每个小辅助都加上 instrumentation。如果每个 helper 都变成一个 span,trace 会变成噪音且成本更高。
  • 添加高基数属性。带 ID 的完整 URL、邮箱、原始 SQL 值、请求体或原始错误字符串会创建数百万个唯一值。
  • 用平均值判断性能。事件出现在百分位(p95/p99)和错误率,而不是平均延迟。

一个快速的健康检查是挑一个真实请求并跟踪它跨越边界。如果你看不到同一个 trace ID 在入站请求、DB 查询、第三方调用和异步 worker 间流动,那么你还没有实现端到端可视化。

实用的“完成”清单

创建内部工具
构建内部工具和管理面板,从第一天起就可被观测的 API。
开始使用

当你能从用户报告追踪到确切请求并沿每个跳点跟踪时,就差不多完成了。

  • 选一条 API 日志并通过 trace_id 定位确切 trace。确认来自同一请求的更深层日志(DB、HTTP 客户端、worker)携带相同的 trace 上下文。
  • 打开 trace 并验证嵌套:顶部是 HTTP server span,子级是 DB 调用和第三方 API。如果是扁平列表通常意味着上下文丢失。
  • 触发一个由 API 请求发起的后台任务(例如发送邮件凭证),并确认 worker span 与请求相连。
  • 检查基本指标:请求计数、错误率和延迟百分位。确认你可以按路由或操作筛选。
  • 扫描属性与日志以保证安全:没有密码、令牌、完整信用卡号或原始个人数据。

一个简单的现实测试是模拟一个支付提供商延迟导致的慢结账。你应该看到一个 trace,其中清晰标注了外部调用的 span,并且结账路由的 p95 延迟在指标图上有峰值。

如果你在生成 Go 后端(例如使用 AppMaster),把这份检查表作为发布流程的一部分会有帮助,这样新端点和 worker 在应用增长时仍能被追踪。AppMaster (appmaster.io) 可以生成真实的 Go 服务,因此你可以在服务和后台任务间标准化一个 OpenTelemetry 配置。

示例:跨服务调试慢结账

一条客户消息写道:“结账有时会卡住。”你无法按需重现,这正是 Go OpenTelemetry 追踪最有用的场景。

先看指标以了解问题形状。查看结账端点的请求率、错误率和 p95 / p99 延迟。如果慢速请求在短时间内成簇出现且只影响一部分请求,通常指向某个依赖、排队或重试行为,而不是 CPU 问题。

接着打开同一时间窗口内的一条慢 trace。一个 trace 往往就够。一笔健康的结账可能是 300 到 600 ms 端到端。一个糟糕的可能是 8 到 12 秒,大部分时间在单个 span 内。

常见模式是:API 处理器很快,DB 工作大致正常,然后支付提供商的 span 显示重试与退避,下游调用在锁或队列后等待。响应有时仍会返回 200,因此仅基于错误的告警永远不会触发。

相关日志会告诉你精确路径的明文描述:“retrying Stripe charge: timeout”,随后是“db tx aborted: serialization failure”,再接着“retry checkout flow”。这清晰表明多个小问题合并成了糟糕的用户体验。

找到瓶颈后,保持一致性能让长期可读性维持。标准化 span 名称、属性(安全的用户 ID 哈希、订单 ID、依赖名)和采样规则,让所有人以相同方式阅读 trace。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧
使用 Go 与 OpenTelemetry 实现端到端 API 可观测性 | AppMaster