最稳妥方案是直接用opentelemetry + otelgin中间件,三步完成:初始化tracerprovider、注册otelgin.middleware、http出站时手动inject context;禁用手写trace-id或私有pkg/trace,因其不支持跨服务传播与标准traceparent。

直接用 OpenTelemetry + otelgin 中间件,是当前 Gin 项目最稳妥、可跨语言、也最容易对接 Jaeger/Zipkin/OTLP Collector 的方案。别再手写 TRACE-ID 注入或自己拼 Span,兼容性差、传播易断、升级成本高。
为什么不用 Gin 自带的 pkg/trace?
Go-Gin-API 框架里那个 pkg/trace 是项目私有封装,只记录本地日志,不生成标准 traceparent 头,也不支持跨服务上下文传递。它本质是「带 ID 的结构化日志」,不是链路追踪 —— 你没法把它和下游 Java 服务或 Python 服务串起来,Jaeger 界面里也看不到任何 Span 关系。
- 它不会自动从
traceparent或uber-trace-id头中提取父 Span Context - 所有
AppendSQL/AppendRedis生成的子 Span 都是孤立的,没有父子关系,无法构成 Trace 树 - 日志输出格式与 OpenTelemetry Collector 不兼容,导出到 Jaeger 需额外转换脚本
如何正确集成 otelgin 中间件?
核心就三步:初始化全局 TracerProvider → 注册 otelgin.Middleware → 确保 HTTP 客户端传播 context。不需要改业务逻辑,gin.Context 会自动携带 context.Context。
- 导入:
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin - 初始化时调用:
otel.SetTracerProvider(tp)(tp来自sdktrace.NewTracerProvider) - 在
*gin.Engine上注册:r.Use(otelgin.Middleware("my-order-service")),服务名必须填,否则 Jaeger 里显示为 unknown_service:gin - 注意:中间件要放在所有业务中间件之前,否则后续中间件拿不到注入后的
ctx
示例片段:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
func initTracer() {
exporter := otlptracehttp.NewClient(
otlptracehttp.WithEndpoint("localhost:4318"),
otlptracehttp.WithInsecure(),
)
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithSampler(trace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
}
func main() {
r := gin.Default()
initTracer()
r.Use(otelgin.Middleware("my-order-service")) // 必须在最前
r.GET("/order/:id", getOrderHandler)
r.Run(":8080")
}
HTTP 调用下游服务时怎么传 context?
这是最容易断链的地方:Gin 中间件自动做了入向(request → span),但出向(span → downstream)必须你手动做。不处理,下游服务就只能生成新 Trace,链路就断成两截。
- 别用
http.Get这类无 context 的快捷函数,一律改用http.DefaultClient.Do(req) - 构造
*http.Request后,用otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))注入头 -
ctx来自c.Request.Context(),Gin 的otelgin已经把它塞进去了 - 常见漏点:数据库驱动、Redis 客户端、gRPC 调用 —— 它们各自有对应的 OTel 插件(如
go.opentelemetry.io/contrib/instrumentation/database/sql),不能靠手动 Inject
错误示范:http.Get("http://user-svc/users/123") → 新 Trace;正确写法:
req, _ := http.NewRequestWithContext(c.Request.Context(), "GET", "http://user-svc/users/123", nil) otel.GetTextMapPropagator().Inject(req.Context(), propagation.HeaderCarrier(req.Header)) resp, _ := http.DefaultClient.Do(req)
采样率和性能开销怎么平衡?
全量采集在压测或故障期有用,但线上默认开 trace.AlwaysSample() 会导致显著 CPU 和网络开销(尤其高频小请求)。生产环境建议用 trace.ParentBased(trace.TraceIDRatioBased(0.01)),即仅对 1% 的入口请求采样,但保留其全部子 Span。
- 不要用
trace.NeverSample()测试,它会让整个链路静默,连otelgin的入口 Span 都不生成 - 如果依赖日志关联 TraceID,记得在日志库(如
zerolog或zap)里加trace.SpanContextFromContext(ctx)提取TraceID字段 - Jaeger UI 默认只查最近 1 小时数据,查历史需确认后端存储(Elasticsearch/Cassandra)配置是否覆盖所需时间范围
真正难的从来不是埋点,而是让每个出站调用都带着 context、让每个 SDK 都用对应 OTel 插件、让采样策略不干扰业务 SLA —— 这些细节没对齐,Jaeger 里看到的永远是散点,不是链路。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











