go微服务链路追踪必须打通指标采集、trace上下文透传、导出通道和前端展示四环节;http入口需用otelhttp.newhandler或otelgin.middleware包裹以自动extract/inject traceparent,下游请求须用otelhttp.newclient或手动注入propagator,otlp endpoint配置错误会导致静默丢数据,异步任务中span必须显式结束并正确传递context。

Go微服务的服务监控与链路可视化,不是“加个SDK就能跑”,而是必须同时打通指标采集、trace上下文透传、导出通道和前端展示四个环节。漏掉任一环,都会出现“有日志无链路”“有指标无调用图”“本地能看线上全黑”这类典型失效现象。
HTTP handler 必须用 otelhttp.NewHandler() 包裹
不包裹就收不到 inbound 请求的 traceparent,root span 直接丢失。OpenTelemetry 的自动注入只发生在中间件层,http.ServeMux 或 gin.Engine 原生 handler 不会触发 span 创建。
-
otelhttp.NewHandler()会自动 extracttraceparent、创建 root span、注入 context 到 request,并在 response 中写回 trace headers - Gin 用户应优先用
otelgin.Middleware(),它内部已封装该行为;但注意它默认不 inject response header,需确认是否启用WithPropagator() - 自定义
http.ServeMux时,每个 route 都要显式 wrap:http.Handle("/api", otelhttp.NewHandler(http.HandlerFunc(handler), "api")) - 别在 handler 里手动调用
tracer.Start(ctx, ...)——这样生成的是孤立 span,parent 关系断裂
下游 HTTP 请求必须用 otelhttp.NewClient() 或手动 inject
Go SDK 不会自动把当前 span 注入 outbound 请求。req.WithContext(ctx) 只传 context,不传 trace 上下文——这是最常被忽略的断链原因。
- 用
otelhttp.NewClient()替代http.DefaultClient是最省事方案,它自动完成 inject + extract - 若用自定义 client(比如带 retry 的),务必封装
DoWithContext(ctx, req)工具函数,避免漏掉otel.GetTextMapPropagator().Inject(req.Context(), propagation.HeaderCarrier(req.Header)) - 跨语言调用时,检查对方 propagator 类型:Go 默认用 W3C
traceparent,但旧 Python/Java 服务可能只认 B3,需配置复合 propagator:propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.B3{})
OTLP 导出器 endpoint 配置错 = 静默丢数据
SDK 默认走 localhost:4317 的 OTLP gRPC,但生产环境 collector 往往监听 0.0.0.0:4317 或 TLS 端口,配错就等于没上报——且无 error 日志,span 直接消失。
- 本地调试建议先切到
stdoutexporter,看到 JSON 输出再切回 OTLP,避免盲调 - 若 collector 启用了 TLS 或认证,需配
otlptracehttp.WithTLSClientConfig()或otlptracehttp.WithHeaders(map[string]string{}) - 使用
otlphttp.NewClient()时,默认端口是4318(HTTP),不是4317(gRPC),别混用
goroutine 和定时任务里的 span 必须显式结束
异步任务里 start 的 span 如果没 defer span.End(),或没在回调中正确获取 parent context,会导致 span 悬空、trace 断裂、内存泄漏。
- 跨 goroutine 必须显式传入
req.Context(),写成go doWork(ctx)而非go doWork() - 异步定时任务(如
time.AfterFunc)中,原 context 可能已被 cancel,应在回调内重新获取 span 或使用otel.WithSpanFromContext - DB 调用需 wrap:
MySQL用otelmysql.Wrap,PostgreSQL用otelpostgresql.Wrap,否则 SQL 执行不计入 span
真正卡住人的从来不是“怎么加 tracer”,而是 context 在哪断、header 在哪漏、exporter 在哪静默失败——这些点不逐个验证,链路永远只显示单个服务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











