必须在main()开头注册tracerprovider,否则所有span为nil且静默失效;需依次执行sdktrace.newtracerprovider()、otel.settracerprovider()和otel.settextmappropagator(propagation.tracecontext{}),并显式传入含service.name的resource。

必须在 main() 开头注册 TracerProvider,否则所有 span 都是 nil,Jaeger / Tempo 里完全看不到 trace —— 程序照跑、日志无报错、链路静默失效。
为什么 otelgin.Middleware 启动后 Jaeger 里还是空的
常见现象:加了 otelgin.Middleware("my-service"),启动服务、发请求,但 Jaeger UI 里查不到任何 trace。根本原因不是中间件没生效,而是 OpenTelemetry SDK 根本没初始化。
OpenTelemetry Go SDK 的设计是“零 panic、零报错、静默降级”:如果没调用 otel.SetTracerProvider(),otel.Tracer().Start() 返回的全是 noopSpan,后续所有操作(记录属性、结束 span、传播 context)全部被丢弃。
实操建议:
- 把
sdktrace.NewTracerProvider()+otel.SetTracerProvider()+otel.SetTextMapPropagator(propagation.TraceContext{})这三行放在main()函数第一行(或紧随init()后),不要包在 if 或 defer 里 - Resource 必须显式传入
sdktrace.NewTracerProvider(),例如resource.WithAttributes(semconv.ServiceNameKey.String("my-service")),否则 Jaeger 里 service.name 为空,无法按服务筛选 - 别在单元测试中多次调用
NewTracerProvider(),并发时可能 panic;也别在 handler 或中间件里 new,会导致多个 provider 冲突、采样逻辑错乱、exporter 连接泄漏
HTTP 入口必须用 otelgin.Middleware,不能手写 header 解析
自己从 req.Header.Get("traceparent") 提取并手动 start span,看似能生成 trace_id,但漏掉了 HTTP server 的语义约定:不会自动记录 http.status_code、net.peer.ip、http.route(如 GET /api/users/:id),span name 固定为 HTTP GET,丢失路由参数上下文。
更严重的是跨 goroutine 传播失败:handler 里起 go doWork(),子协程里的 span 就变成 root,trace_id 全 0。
实操建议:
- 直接用
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin包的otelgin.Middleware("my-service") - 确保它在
r.Use()中排在 logger、recovery 等中间件之后(不影响 span 属性采集),但必须在业务路由注册前 - 不要试图用
otelhttp.NewHandler替代 —— 它和 Gin 路由系统不兼容,会破坏c.Param()、c.FullPath()等能力,span name 退化为固定字符串
出向调用和数据库必须用 instrumented client
在 handler 里手动 span := tracer.Start(ctx); defer span.End() 包裹 db.Query() 或 http.Get(),只覆盖业务代码执行时间,完全忽略连接池等待、驱动解析、DNS 查询、TLS 握手、网络往返等真实瓶颈点。
实操建议:
- 数据库:用
go.opentelemetry.io/contrib/instrumentation/database/sql,注册时调sql.OpenDB(otelsql.InjectDriver("mysql", driver)),不是sql.Open();MySQL 驱动必须是github.com/go-sql-driver/mysql,modernc.org/sqlite等纯 Go 驱动不兼容 - 出向 HTTP:用
otelhttp.NewTransport(http.DefaultTransport)构造http.Client,再传给业务逻辑;绝不能直接改http.DefaultClient或 patchhttp.Transport - 异步任务(如
go func() { ... }())必须显式传递带 span 的context.Context,否则子 goroutine 的 span 自动 fallback 到 noop
Exporter 配置容易忽略的关键点
本地开发常用 jaeger exporter,但生产环境强烈建议走 OTLP + otel-collector:一是避免应用直连后端导致连接数爆炸,二是 collector 可做采样、过滤、丰富资源属性、多路导出(同时发给 Jaeger 和 Prometheus)。
实操建议:
- Jaeger exporter endpoint 默认是
localhost:6831(UDP),但新版 Jaeger Agent 已默认关闭 UDP,应改用http://localhost:14268/api/traces(HTTP)或更推荐 OTLP gRPClocalhost:4317 - 用 OTLP 时,务必设置
WithEndpoint("localhost:4317")和WithInsecure()(开发环境),生产环境必须配 TLS 和认证 - Collector 配置中,
resource_to_telemetry_conversion.enabled: true很关键 —— 它能把service.name等资源属性自动转为指标标签,否则 Prometheus 里指标没法按服务聚合
最常被跳过的一步:忘记在 TracerProvider 初始化时传入 resource。没有 service.name,所有 trace 在 Jaeger 里都归到 “unknown_service:go”,排查时根本分不清是哪个服务出的问题 —— 这不是数据没上报,是上报了但丢了关键标识。











