
Go OpenTelemetry 集成手动 Span 和自动插桩如何配合一、背景与问题OpenTelemetry 的 Go SDK 提供两种插桩方式自动插桩通过 instrumentation library 自动创建 Span和手动 Span代码中显式创建。两种方式各有适用场景但实际项目中经常出现混用不当的问题自动插桩覆盖了 HTTP/gRPC 的框架层调用但业务逻辑的关键步骤没有手动 Span或者反过来手动 Span 太多但框架层缺少自动插桩导致追踪数据只有业务细节缺少基础设施上下文。配合不当的结果是追踪数据要么太粗只有 HTTP 请求和 gRPC 调用两个 Span看不出业务逻辑要么太碎每个函数都是 Span几百个 Span 看不出重点要么断裂自动 Span 和手动 Span 的父子关系不对链路中间断开。正确的配合原则很简单自动插桩负责框架层网络调用、HTTP handler手动 Span 负责业务关键路径推理预处理、模型选择、缓存查询。两者通过正确的父子嵌套形成完整链路。二、自动与手动插桩的职责划分flowchart TB subgraph 自动插桩层 A[HTTP Handler Span] B[gRPC Client Span] C[gRPC Server Span] D[HTTP Client Span] end subgraph 手动 Span 层 E[推理预处理 Span] F[模型选择 Span] G[缓存查询 Span] H[结果格式化 Span] end A -- E E -- G E -- F F -- B B -- C C -- H style A fill:#0d47a1,color:#fff style B fill:#1565c0,color:#fff style C fill:#1976d2,color:#fff style D fill:#1e88e5,color:#fff style E fill:#ff6f00 style F fill:#ff8f00 style G fill:#ffa726 style H fill:#ffb74d蓝色是自动插桩层橙色是手动 Span 层。链路结构HTTP 请求进来自动→预处理手动→缓存查询手动→模型选择手动→gRPC 调用推理引擎自动→推理引擎处理自动→结果格式化手动。职责划分的核心逻辑层级负责方创建方式关注点网络入口/出口框架自动插桩HTTP/gRPC 的协议层信息业务关键步骤开发者手动 Span预处理、缓存、模型选择等业务逻辑外部调用框架开发者自动手动属性自动创建 Span手动标注业务属性关键规则不要在自动插桩覆盖的地方再手动创建 Span。如果 othttp instrumentation 已经为 HTTP handler 创建了 Span你再在 handler 内部手动创建一个 http_handler Span就会出现两个 Span 嵌套做同一件事追踪数据冗余且混乱。三、实现细节3.1 自动插桩配置Go 的 OTel 自动插桩通过 instrumentation library 实现需要在服务启动时初始化package main import ( net/http go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc google.golang.org/grpc ) func main() { // 初始化 OTel Provider省略详细配置 tp : initTracerProvider() otel.SetTracerProvider(tp) // HTTP 自动插桩 handler : otelhttp.NewHandler(http.HandlerFunc(inferenceHandler), inference-api) // gRPC 自动插桩 server : grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()), ) client : grpc.Dial( inference-engine:50051, grpc.WithStatsHandler(otelgrpc.NewClientHandler()), ) http.Handle(/predict, handler) server.Serve(lis) }otelhttp.NewHandler为每个 HTTP 请求自动创建 Span包含http.method、http.status_code、http.route等属性。otelgrpc.NewServerHandler/NewClientHandler同理处理 gRPC。3.2 手动 Span 嵌套在自动 Span 内自动插桩创建的 HTTP handler Span 是根 Span。手动 Span 必须嵌套在它的 context 内func inferenceHandler(w http.ResponseWriter, r *http.Request) { // r.Context() 已经被 otelhttp 注入了 trace context ctx : r.Context() tracer : otel.Tracer(inference-service) // 手动 Span 嵌套在自动 HTTP Span 下 ctx, preprocessSpan : tracer.Start(ctx, inference.preprocess) tokens : preprocess(r) preprocessSpan.SetAttribute(input_length, len(r.Body)) preprocessSpan.SetAttribute(tokenizer, bpe-v3) preprocessSpan.End() // 缓存查询 ctx, cacheSpan : tracer.Start(ctx, inference.cache_lookup) cached, hit : cache.Get(tokens) cacheSpan.SetAttribute(cache_hit, hit) cacheSpan.End() if hit { // 直接返回缓存结果 writeResponse(w, cached) return } // gRPC 调用自动插桩会自动嵌套在 cacheSpan 的 context 下 resp, err : inferenceClient.Predict(ctx, pb.PredictRequest{Tokens: tokens}) // ... }关键点tracer.Start(ctx, ...)使用从 HTTP handler 传入的ctx自动成为 HTTP Span 的子 Span。后续的 gRPC 调用也使用同一个ctxgRPC 自动插桩会在这个 context 下创建 client Span形成正确的父子链路。如果不使用ctx而用context.Background()创建手动 Span链路就会断裂——手动 Span 没有父 SpangRPC 调用也不在追踪链路内。3.3 属性标注的分工自动插桩标注协议层属性手动 Span 标注业务属性两者互补Span 来源标注内容示例otelhttp (自动)http.method, http.route, http.status_codePOST, /predict, 200otelgrpc (自动)rpc.system, rpc.service, rpc.methodgrpc, InferenceService, Predict手动 Spanmodel_name, input_length, cache_hitllama-3-8b, 256, false不要在手动 Span 上重复标注自动插桩已经覆盖的属性。例如http.method已经在 HTTP Span 上你在业务 Span 上再标一个method: POST是冗余。属性标注的唯一性原则每个属性只在最合适的 Span 上出现一次。四、生产实践与踩坑otelhttp 和框架中间件的顺序。如果你的 HTTP handler 使用了多个中间件认证、限流、日志otelhttp.NewHandler必须在最外层确保所有中间件的执行都在追踪范围内// 正确顺序otel → auth → ratelimit → handler handler : otelhttp.NewHandler( authMiddleware(rateLimitMiddleware(http.HandlerFunc(inferenceHandler))), inference-api, )如果 otelhttp 在内层认证和限流的执行时间不在追踪范围内延迟飙升时你无法区分是认证慢还是推理慢。gRPC 拦截器和 StatsHandler 的冲突。Go gRPC 有两种插桩方式Interceptor旧方式和 StatsHandler新方式。两者不要同时使用否则每个 gRPC 调用会产生两个 Span。OTel Go SDK 的推荐方式是 StatsHandler上面代码示例用的就是 StatsHandler。手动 Span 的命名规范。Span 名称用{服务名}.{操作名}格式如inference.preprocess、inference.cache_lookup。不要用预处理或step1这种命名——Span 名称是 Trace Viewer 中链路展示的核心信息必须可读、可搜索、可区分。Span 生命周期管理。span.End()必须在操作完成后立即调用不要延迟到函数返回。延迟 End 的 Span 在 Trace Viewer 中显示的持续时间会不准确。如果操作是异步的如 goroutine必须在 goroutine 内 Endgo func(ctx context.Context) { ctx, span : tracer.Start(ctx, inference.async_postprocess) defer span.End() // 异步处理 }(ctx)注意ctx必须传进 goroutine不能用外部的ctx外层函数返回后 ctx 会被 cancel。批量操作的 Span 策略。批量推理请求包含多个样本不要为每个样本创建子 SpanSpan 爆炸。在父 Span 上标注batch_size和avg_latency_ms子 Span 只在异常样本延迟 P99 的样本上创建。五、总结手动 Span 和自动插桩的配合不是选一种是两者各司其职自动插桩覆盖框架层HTTP/gRPC手动 Span 覆盖业务关键路径。两者的嵌套关系通过 context 传递保证链路完整。三个关键原则职责不重叠自动插桩覆盖的层级不要手动重复创建 Span属性标注也一样——每个属性只在最合适的 Span 上出现一次。嵌套靠 context手动 Span 必须使用从自动插桩传入的ctx创建不用context.Background()否则链路断裂。中间件顺序otelhttp 必须在最外层确保所有中间件的执行都在追踪范围内。基础设施不需要漂亮话。追踪数据的价值不在于 Span 多少在于链路是否完整、层级是否清晰、属性是否准确。自动和手动配合得当链路完整可读配合不当数据要么冗余要么断裂还不如没有。