beego中间件中创建span后必须用opentracing.contextwithspan绑定并显式调用req.withcontext()覆盖c.ctx.request.context(),否则controller内spanfromcontext()返回nil;同时需在中间件末尾defer span.finish(),不可依赖finishrouter阶段。

Beego 中间件里 Span 创建后必须重写 request.Context
Beego 的 Controller 实例不自动继承 HTTP 请求的 trace 上下文,哪怕你在中间件里调用了 tracer.StartSpan,后续 c.Ctx.Request 里的 Context() 仍是原始空 context。结果就是业务逻辑里调用 opentracing.SpanFromContext(c.Ctx.Request.Context()) 拿到 nil,子 span 全部断开。
正确做法是:创建 span 后,用 opentracing.ContextWithSpan 绑定,并通过 c.Ctx.Request = c.Ctx.Request.WithContext(...) 显式覆盖:
func JaegerMiddleware() beego.FilterFunc {
return func(ctx *context.Context) {
tracer := opentracing.GlobalTracer()
req := ctx.Request
// 从 header 提取上游上下文
wireCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
var span opentracing.Span
if wireCtx != nil {
span = tracer.StartSpan("http-server", opentracing.ChildOf(wireCtx))
} else {
span = tracer.StartSpan("http-server")
}
// ✅ 关键:注入 span 到 request.Context
ctx.Request = req.WithContext(opentracing.ContextWithSpan(req.Context(), span))
defer span.Finish()
}
}
漏掉这行 ctx.Request = ...,整个 Beego 链路就只剩第一个 span。
Beego 的 FinishRouter 阶段无法访问完整 Span 上下文
Beego 的执行流程是:Prepare → Filter(中间件)→ Controller.Run → FinishRouter。很多人想在 FinishRouter 里统一调用 span.Finish(),但这时 c.Ctx.Request.Context() 已被重置或失效 —— Beego 不保证该阶段 context 可达,且 span 可能已被提前结束。
务必把 defer span.Finish() 放在中间件函数末尾,或在 Prepare 里创建并存入 c.Data,再在 FinishRouter 中取出,但要注意:
-
c.Data是 map[string]interface{},存span本身不安全(可能被 GC);推荐只存span.Context()或 traceID 字符串 - 若用
FinishRouter统一结束,需确保中间件和Prepare严格配对,否则易 panic - 更稳妥的方式:中间件里
defer span.Finish()+ 手动设置w.WriteHeader()前不触发任何 response 写入(避免某些 Beego 日志中间件提前关闭 writer)
HTTP Header 提取必须用 opentracing.HTTPHeaders,别手动读 X-B3-TraceId
Jaeger 默认支持 B3、W3C TraceContext 等多种传播格式,但 Beego 的 req.Header 是 http.Header 类型,直接取 req.Header.Get("X-B3-TraceId") 只能拿到 B3 格式,且无法还原完整的 SpanContext(缺少 parentSpanID、flags 等),导致下游调用链断裂。
必须走标准提取流程:
wireCtx, err := tracer.Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header),
)
这个调用会自动识别 uber-trace-id、traceparent、X-B3-TraceId 等 header,并构造出可参与 ChildOf() 的完整上下文。手动解析只适用于调试验证,不能用于生产透传。
Beego 日志与 TraceId 关联要用 c.Ctx.Input.Data 注入,而非全局 MDC
Go 没有 Java 那样的线程级 MDC,Beego 也不提供类似 MDC.put() 的机制。想让每行日志带 traceId,不能依赖全局变量或 goroutine-local 存储(Beego 多个 controller 可能复用 goroutine)。
正确方式是:在中间件里从 span 提取 traceID,存入 c.Ctx.Input.Data["trace_id"],然后在日志封装层(如自定义 beego.BeeLogger 的 Output 方法)中读取并拼接:
// 中间件中
traceID := span.Context().(opentracing.SpanContext).String()
c.Ctx.Input.Data["trace_id"] = traceID
// 自定义 logger 输出时
if tid, ok := c.Ctx.Input.Data["trace_id"]; ok {
fmt.Printf("[traceId=%s] %s\n", tid, msg)
}
注意:Beego 的 Input.Data 生命周期仅限当前请求,不会跨请求污染,比用 context.Value 更贴合 Beego 的设计习惯。但别在异步 goroutine 里读它 —— 请求上下文已销毁。
最常被忽略的是中间件顺序和 Request.WithContext 覆盖动作:Beego 的 filter 是链式执行,如果 Jaeger 中间件排在 session 或 auth 之后,那些中间件就拿不到 span;而一旦忘记重写 ctx.Request,所有后续逻辑都运行在无 trace 的真空里。











