Go mod 好菜系列 - 0x16 otel 这口链路追踪不是把日志换个颜色
详细聊 OpenTelemetry 在 Go 项目里的位置、trace/span 到底在看什么、它和日志与 metrics 的关系,以及接入时最容易踩的几类坑。
很多团队开始接入 OpenTelemetry 的起点都很像:服务多起来了,请求跨了好几跳,日志也有,指标也有,但一遇到“到底是哪一跳慢了”这种问题,大家还是得靠猜。这个时候,链路追踪就开始上桌了。
otel 在解决什么问题
它想回答的,不只是“有没有报错”,而是:
- 这次请求经过了哪些服务
- 每一段各花了多少时间
- 是哪一层在拖后腿
- 上下游上下文有没有串起来
如果说日志更像事件记录,metrics 更像体检指标,那 trace 更像一次完整就诊路径。
trace 和 span 别背概念,先背直觉
- Trace:一次完整请求旅程
- Span:旅程中的一个步骤
比如用户发起一次下单请求,这条链路里可能包含:
- API 网关接入
- 订单服务校验
- 库存服务扣减
- 支付服务预创建
- 数据库写入
这一整串是一个 Trace,每一段是一个 Span。
为什么 otel 现在很常见
因为它已经逐渐成了“可观测性三件套”的公共语言。团队不想被某个单一厂商 SDK 死死绑住时,otel 往往是个更稳的选择。它可以把数据再送去 Jaeger、Tempo、Zipkin,甚至各种商业平台。
Go 项目里最常见的接法
- HTTP server middleware 自动起 span
- gRPC client/server interceptor 自动透传上下文
- 数据库和外部 HTTP 调用做埋点
- 把 trace context 跟日志字段串起来
你会发现真正有价值的,不是“代码里能不能 new 一个 tracer”,而是上下游能不能接力把上下文传下去。
一个最小可用的 tracing 初始化示例
exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
if err != nil {
log.Fatal(err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewSchemaless(
semconv.ServiceNameKey.String("order-api"),
)),
)
otel.SetTracerProvider(tp)
tracer := otel.Tracer("order-api")
ctx, span := tracer.Start(context.Background(), "CreateOrder")
defer span.End()这段代码只是最小骨架,但能帮助你先把 trace/span 的落点看明白。后面再接 Jaeger、Tempo 或其他后端时,整体思路不会变。
日志、指标、追踪是什么关系
这三样不是互相替代,而是互相补位:
- 日志告诉你发生了什么细节
- 指标告诉你整体状态正在往哪边偏
- 追踪告诉你问题出在链路的哪一段
很多团队最大的误区,是只上了 tracing UI,就以为自己已经“可观测”了。实际上链路图只是入口,不是答案本身。
接入时最容易踩的坑
- 只在入口服务打点,后面服务上下文没传下去
- span 名称乱写,最后一张图像一锅乱码
- 把所有函数都打 span,追踪成本和噪音一起爆炸
- 链路有了,但日志里没有 trace_id,定位还是断的
所以 tracing 不是“点越多越高级”,而是要围绕关键链路、关键外部调用、关键瓶颈点来布。
什么时候特别值得做
- 服务间调用层级明显增加
- 超时、慢请求、偶发错误难以复现
- 你已经不满足于只看单机日志
小结
otel 这口菜更像基础设施,不是表演菜:
- 它解决的是跨服务链路可见性问题
- trace / span 的核心是请求旅程,不是概念题
- 要和日志、metrics 配合起来才真正有用
- 别盲目全量埋点,关键路径比数量更重要