Go微服务可观测性需在main()开头同步注入trace_id、metrics、结构化日志,否则链路断裂、指标失真、日志无法关联;HTTP/gRPC入口须透传context生成trace,禁用context.Background();Metrics须区分运行时与业务维度并带label;日志必须结构化且显式绑定trace context;OTel SDK初始化必须早于任何业务逻辑并配置采样对齐。

Go微服务的可观测性不是“加个SDK就完事”,而是从服务启动那一刻起,trace_id、metrics、structured logs必须三位一体同步注入上下文,否则链路断裂、指标失真、日志无法关联——排查时你面对的仍是三座孤岛。
Trace ID 必须在请求入口就生成并透传
很多团队在中间件里才生成 trace_id,结果上游网关已超时或重试,导致同一次用户请求被记录为多个独立 trace;更糟的是,gRPC 调用中若没显式携带 context,span 会断在第一跳。
- HTTP 入口:用
http.Request.Context()初始化,优先读取X-Trace-IDheader,缺失时立即调用otel.GetTracerProvider().Tracer(...).Start()生成新 trace - gRPC 入口:必须用
grpc.UnaryInterceptor拦截,从metadata.MD提取traceparent或自定义 key,并通过grpc.ExtractTraceID()(或手动解析)注入context - 禁止用
context.Background()启动 span —— 这会让所有 span 脱离父链,变成孤立根节点
Metrics 暴露必须区分业务维度与运行时维度
go_goroutines 和 http_request_duration_seconds 都是 gauge/histogram,但它们的采集意图完全不同:前者反映系统负载水位,后者用于 SLO 计算。混在一起注册、不设 label,等于把油表和转速表焊死在一块仪表盘上。
- 运行时指标(如
go_memstats_alloc_bytes)直接用prometheus.DefaultRegisterer,无需 label,靠 Prometheus 自动打标 - 业务指标(如
payment_success_total)必须带label:至少包含service、method、status,否则无法下钻到具体接口或错误类型 - 避免在 handler 内频繁调用
Inc()或Observe()—— 高并发下锁竞争严重,应改用promauto.With(registry).NewCounterVec()获取线程安全实例
日志必须结构化且自动绑定 trace context
用 fmt.Printf 或 log.Println 打日志,等于主动放弃可观测性。没有 trace_id 的日志,在 Jaeger 或 Grafana 中根本无法和 span 关联;没有结构化的字段,Loki 就没法做 {job="user-service"} |= "timeout" 这类过滤。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
立即学习“go语言免费学习笔记(深入)”;
- Go 1.21+ 强制用
slog,初始化时传入slog.HandlerOptions.AddSource: true,确保输出含文件名和行号 - 每条日志必须显式传入
ctx,再用slog.With("trace_id", slog.StringValue(GetTraceID(ctx)))注入(GetTraceID()需从 context 提取,不能靠全局变量) - 禁止在日志中拼接字符串:
slog.Info("failed to call payment api: " + err.Error())→ 正确写法是slog.Error("call payment api failed", "error", err.Error(), "trace_id", GetTraceID(ctx))
OTel SDK 初始化必须早于任何业务逻辑
常见错误是把 otel.InitTracerProvider() 放在 main() 末尾,甚至塞进某个 handler 里——这时已有 goroutine 在跑,otel.Tracer("xxx").Start() 创建的 span 会默认 fallback 到 noop 实现,全程静默丢弃数据。
- 必须在
main()开头、flag.Parse()之后立即初始化:先建resource(含service.name),再配otlptracegrpc.New()导出器,最后otel.SetTracerProvider() - 导出器地址不能硬编码:用环境变量
OTEL_EXPORTER_OTLP_ENDPOINT,K8s 下配ConfigMap或Secret动态注入 - 务必设置
otel.SetErrorHandler(),否则 SDK 内部错误(如连接 OTLP collector 失败)完全不可见
最易被忽略的点:trace 和 metrics 的采样策略必须对齐。比如 trace 设置了 tail-based 采样(只保留 error 或 slow request),但 metrics 却全量上报,会导致 Prometheus 看到的错误率远高于 Jaeger 中实际 trace 出错的比例——这不是数据不准,是观测视角错位。

















