Gin框架需通过OpenTelemetry中间件显式注入上下文并创建Span,且otelgin.Middleware必须在其他中间件前注册,同时正确配置propagator、Exporter及HTTP客户端traceparent注入,否则链路将在首跳中断。

Gin 框架本身不带链路追踪能力,必须通过 OpenTelemetry 中间件显式注入上下文、创建 Span,并确保 traceparent Header 能跨服务透传——漏掉 propagator 配置或中间件注册顺序错误,整个链路就断在第一跳。
otlpgin.Middleware 必须在其他业务中间件之前注册
OpenTelemetry 的 Gin 中间件(otelgin.Middleware)需要在请求进入时立即解析 traceparent 并生成根 Span,否则后续中间件(如日志、鉴权)拿到的 context.Context 就不含 trace 信息。
- ✅ 正确顺序:
r.Use(otelgin.Middleware("my-service"))→r.Use(loggerMiddleware)→r.Use(authMiddleware) - ❌ 错误顺序:把
otelgin.Middleware放在authMiddleware后面,会导致 auth 处理时 Context 无 trace,其内部发起的 HTTP/gRPC 调用无法延续链路 - 注意:Gin 的
c.Request.Context()是只读副本,中间件必须用c.Set("ctx", ctx)或自定义*gin.Context扩展来传递新 Context,不能直接改写原 Context
HTTP 客户端调用必须手动 Inject traceparent
Gin 处理完请求后,若要调用下游服务(比如用 http.Client 请求订单服务),必须显式将当前 Span 的上下文注入到请求 Header,否则下游服务收不到 traceparent,链路就此中断。
- 用
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))注入 - 别依赖
req.Header.Set("traceparent", ...)手动拼,W3C 标准格式(含 version、trace-id、span-id、flags)容易出错 - 如果下游是 gRPC 服务,要用
metadata.MD+grpc.Inject替代 Header 注入 - 常见坑:忘记在
http.NewRequest后再 Inject,或在重试逻辑里重复 Inject 导致 Header 冗余
全局 TracerProvider 初始化不可少,且 Exporter 地址必须可连通
otel.TracerProvider 是所有 Span 的源头,没初始化或配置失败,otelgin.Middleware 创建的 Span 实际不会上报,本地日志里也看不到 trace_id。
- 必须在
main()开头调用otel.SetTracerProvider(tp),且tp由sdktrace.NewTracerProvider构建 - OTLP Exporter 的 endpoint(如
http://localhost:4318/v1/traces)需提前确认 collector 进程已运行、端口未被防火墙拦截 - 采样策略建议初期用
sdktrace.AlwaysSample(),上线后再切TraceIDRatioBased(0.01)避免漏关键链路 - 不配置 Propagator(如
otel.SetTextMapPropagator(propagation.TraceContext{}))会导致跨服务 context 丢失,即使 Exporter 正常也断链
日志中打印 trace_id 要从 Context 提取,而非中间件局部变量
很多开发者在中间件里用 span.SpanContext().TraceID().String() 获取 trace_id 并塞进日志字段,但这个值只在当前中间件生命周期有效;一旦进到 handler 或 goroutine,Span 可能已结束或 Context 已变更。
- 正确做法:在日志封装层(如
log.WithValues("trace_id", traceIDFromCtx(c.Request.Context())))统一从context.Context提取 - 提取函数应使用
otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))后再取trace.SpanFromContext(ctx).SpanContext().TraceID() - 避免在 handler 里用
c.Get("trace_id")这类字符串 key 取值,类型不安全,且易与自定义 header 冲突 - 如果用结构化日志库(如 zerolog、zap),务必注册
traceID为context.Context的字段,而不是每次手动传参
真正卡住链路落地的,往往不是埋点代码写得对不对,而是 propagator 配置漏了、Exporter 连不上、或者 HTTP 客户端忘了 Inject——这些地方没有报错,但 trace 就是断在 service A 到 service B 的那一跳,查日志也只看到单个 span,得挨个检查中间件顺序、context 传递路径和网络连通性。


















