Beego 可完整集成 OpenTelemetry,但需手动处理 trace context 注入、span 命名、ORM 埋点、日志关联及 span 生命周期管理,否则导致链路断裂、指标失真。

Beego 框架本身不内置 OpenTelemetry 支持,但可以完整集成——关键不是“能不能”,而是“怎么绕过 Beego 的中间件机制盲区,把 trace context 正确注入和传播出去”。
Beego 的 HTTP 中间件不自动处理 trace header
Beego 的 Controller 生命周期里没有默认的 context 透传逻辑,otlptrace 或 otelhttp 这类标准中间件无法直接套用。你写 beego.InsertFilter("/.*", beego.BeeAppHandler, yourMiddleware),但若中间件没手动从 http.Request.Header 提取 traceparent 并塞进 context.Context,后续所有 span 都会断链。
- 常见错误现象:Jaeger 里看到单个服务内有多个孤立 span,跨请求无 TraceID 关联
- 必须在
Prepare()方法里手动解析并注入:func (c *BaseController) Prepare() { ctx := c.Ctx.Request.Context() // 从 header 提取 traceparent spanCtx := propagation.Extract(ctx, http.HeaderCarrier(c.Ctx.Request.Header)) ctx = trace.ContextWithSpanContext(ctx, spanCtx.SpanContext()) c.Ctx.Request = c.Ctx.Request.WithContext(ctx) } - Beego 的
c.Ctx.Request是只读副本,必须用WithContext()显式替换,否则下游调用(如 ORM、HTTP client)拿不到上下文
Beego 路由参数无法被 otelhttp 自动识别
otelhttp 依赖 http.ServeMux 的路由匹配逻辑,而 Beego 使用自定义路由表(beego.Router("/api/users/:id", &UserCtrl{}, "get:Get")),导致 otelhttp 记录的 span 名称固定为 HTTP GET,丢失真实路径模板信息。
- 解决方法:在
Prepare()中手动设置 span 名称:span := trace.SpanFromContext(c.Ctx.Request.Context()) span.SetName(fmt.Sprintf("GET /api/users/{id}")) - 更稳妥做法是用 Beego 的
c.Ctx.Input.Param(":id")动态构造,避免硬编码;但注意别把敏感参数(如 token、手机号)打进去 - 如果不改 span 名,Prometheus 的
http_server_duration_seconds_bucket{route="HTTP GET"}就完全失去聚合价值
Beego ORM 调用需手动创建子 span
Beego 的 o.QueryTable("user").Filter("id", 123).One(&u) 是同步阻塞调用,OpenTelemetry Go SDK 不会自动 instrument 它——不像 gorm 或 sqlx 有官方插件。
- 必须包裹在显式 span 内:
ctx := c.Ctx.Request.Context() _, span := trace.StartSpan(ctx, "beego.orm.query.one") defer span.End() err := o.QueryTable("user").Filter("id", 123).One(&u) - 建议封装成带 trace 的 wrapper 函数,统一处理 span name、error 标记、duration 记录,避免每个 DAO 层都重复写
- 注意:Beego ORM 底层用的是
database/sql,如果你已启用otelmysql或otelpostgresql,那 SQL 执行本身会被捕获,但上层 Beego ORM 的抽象层仍需手动埋点
Beego 日志与 trace 关联要靠字段注入
Beego 默认日志(beego.Info())不携带 trace_id,即使你用了 zap 或 zerolog 作为底层 logger,也得主动把 trace.SpanContext().TraceID().String() 注入日志字段。
- 推荐在
Prepare()初始化一个带 trace 字段的 logger 实例:traceID := trace.SpanFromContext(c.Ctx.Request.Context()).SpanContext().TraceID().String() c.Data["zlog"] = zap.With(zap.String("trace_id", traceID)) - 后续所有
c.Ctx.Input.Log()或自定义日志调用都基于这个实例,确保 trace_id 和 span 同源 - 如果跳过这步,日志系统和追踪系统就彻底脱节,查问题时还得靠时间戳硬对齐
最易被忽略的是 Beego 的 Finish() 钩子——它在 response 写完后触发,但此时 http.Request.Context() 可能已被 cancel 或 timeout,span end 操作必须在 Prepare() 后立即配对,不能拖到 Finish() 里做。否则你会看到大量 “ended after context done” 的 warning,且 span duration 不准。


















