Golang Logrus集成OpenTelemetry 日志TraceID/SpanID为0问题排查
问题根因
otelhttp中间件生成的链路Span是绑定在HTTP请求对象自带的Context上的,你在业务代码里直接调用log.Info()时,Logrus默认使用空的背景上下文,根本读不到请求绑定的Span信息,所以提取出的TraceID、SpanID始终为全0的无效值。你看到控制台输出的Span信息是OpenTelemetry导出器自己打印的,它持有请求上下文,和你打日志用的上下文完全隔离。- 服务启动阶段打印的
Listening...日志本身不在任何请求链路内,出现全0ID是正常现象,不需要特殊处理。
无侵入实现方案(无需全量手动透传Context)
核心思路是通过Goroutine本地存储(GLS)自动绑定请求上下文,配合自定义日志格式化逻辑自动提取链路ID,业务代码不需要修改原有日志调用方式。
具体修改步骤
- 安装GLS依赖,用来在当前处理请求的Goroutine内存储上下文,避免跨函数透传:
go get github.com/jtolio/gls - 新增上下文注入中间件,在请求入口把OpenTelemetry注入了Span的请求上下文绑定到当前Goroutine,请求结束后自动清理:
func contextMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() gls.Set("req_ctx", ctx) defer gls.Del("req_ctx") next.ServeHTTP(w, r) }) } - 修改自定义日志格式化逻辑,优先读取Goroutine绑定的上下文,自动校验Span有效性再注入ID:
func (l customLogger) Format(entry *log.Entry) ([]byte, error) { ctx := entry.Context // 日志条目未携带Context时,从当前Goroutine取绑定的请求上下文 if ctx == nil { if val, ok := gls.Get("req_ctx"); ok { ctx = val.(context.Context) } } span := trace.SpanFromContext(ctx) sc := span.SpanContext() // 仅当Span有效时注入链路ID,避免非链路场景打印无意义的全0值 if sc.IsValid() { entry.Data["trace_id"] = sc.TraceID().String() entry.Data["span_id"] = sc.SpanID().String() } return l.formatter.Format(entry) } - 调整路由注册的中间件顺序,上下文中间件需要包裹在
otelhttp外层,保证能拿到生成好的Span上下文:otelHandler := otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { // 这里不需要任何修改,直接调用原有日志方法即可 log.Info("About End Point Printning...") fmt.Fprintln(w, "about page") }),"About") // 包裹上下文中间件 http.Handle("/about", contextMiddleware(otelHandler))
注意事项
- 如果你在请求处理过程中启动新的Goroutine执行异步任务,GLS不会自动继承上下文,这类场景只需要在启动Goroutine时把请求Context传入,用
log.WithContext(ctx)打印日志即可,核心同步链路的代码不需要任何改动。 - 原代码中
log.Error("Error shutting down tracer provider: %v", err)写法有误,Logrus的Error方法不支持格式化占位符,需要改为log.Errorf或者使用log.WithError(err).Error("Error shutting down tracer provider")。
内容的提问来源于stack exchange,提问作者Arjun Kanti
相关产品推荐
相关产品推荐

