自动埋点覆盖不全时,手动埋点是补全链路的关键。本文用 Go 演示 OpenTelemetry SDK 的初始化、Span 创建、属性与事件、错误标记和跨服务传播,附可直接运行的代码片段。
自动埋点能覆盖 HTTP、数据库、消息队列等常见调用,但业务关键逻辑——一个循环、一次外部调用、一段加密计算——它看不到。手动埋点就是在这些位置显式创建 Span,把业务语义补进链路里。本文以 Go 为例,从头搭一个可用的埋点工程。
先引入依赖并初始化 TracerProvider,导出到 Collector:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/trace"
)
func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
exp, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.1))),
)
otel.SetTracerProvider(tp)
return tp, nil
}
WithBatcher 批量上报,TraceIDRatioBased(0.1) 按 10% 采样。生产环境建议在 HTTP、数据库这类框架的入口已经由自动埋点覆盖的前提下,手动埋点只用来补充业务语义。
Span 是链路的基本单元。用 context 贯穿整个调用,父子关系自动建立:
func ProcessOrder(ctx context.Context, orderID string) error {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(ctx, "process_order")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
attribute.String("service.version", "1.4.2"),
)
// 业务逻辑...
return nil
}
属性要克制:只放需要检索的字段(订单号、用户 ID、版本号),不要塞整个请求体,否则存储和查询成本都会涨。事件用于记录时间点,比如"支付回调已收到":
span.AddEvent("payment_callback_received",
trace.WithAttributes(attribute.String("channel", "wechat")))
调用失败时,除了记日志,还要给 Span 打上错误状态,这样链路图里能一眼标红:
if err := callInventory(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
RecordError 把错误信息作为事件记录,SetStatus 把 Span 状态置为 ERROR。上层查询时按 status = ERROR 过滤,就能捞到所有失败链路。
链路要不断,context 必须跟着请求走。HTTP 服务用中间件把 trace context 注入 header,下游用同一套 SDK 会自动接续:
otelhttp.NewHandler(handler, "order-api")
异步场景(goroutine、消息队列)要手动把 context 传进去,别在 goroutine 里用新的 context.Background(),否则父子关系就断了。采样的坑也在这里:父 Span 被采样了,子 Span 却做了新的采样决策,链路会缺一半。
发几笔请求,到控制台按 trace id 查一条链路,确认每个 Span 的耗时、属性、错误状态都对。埋点不是写完就完事,链路完整性要靠一次真实请求来验证。