- 可观测性
- 时序数据库
- 后端
- 指标监控
【免费下载链接】cortex
A horizontally scalable, highly available, multi-tenant, long term Prometheus.
导读
本篇文章围绕 OpenTelemetry Go SDK 提供的go.opentelemetry.io/otel/bridge/opentracing桥接包展开,讲解如何在不改动既有 OpenTracing 埋点代码的前提下,将 OpenTracing API 的调用无缝转发到 OpenTelemetry TracerProvider,实现两套追踪体系在同一进程内的共存与上下文互通。文章不仅完整覆盖桥接器的初始化、Span 上下文互操作与扩展接口用法,还结合 Cortex 仓库中pkg/tracing、pkg/tracing/migration及pkg/frontend/v1的实际调用链,说明桥接器在真实分布式系统(Cortex 查询前端 gRPC 透传)中的落地方式。读完本文,你将掌握桥接器的安装接线、双向上下文传播、Tag/Log 翻译规则,以及延迟上下文设置(Deferred Context Setup)这一关键机制。
为什么需要 OpenTracing → OpenTelemetry 桥接器
OpenTracing 与 OpenTelemetry 是两代追踪规范。OpenTelemetry 已取代 OpenTracing 成为事实标准,但大量存量代码仍使用github.com/opentracing/opentracing-go的Tracer、Span、SpanContext接口埋点,全部重写成本极高。桥接器的价值在于:
- 让 OpenTracing 埋点继续工作,同时把产生的 Span 数据送入 OpenTelemetry SDK/导出器(如 OTLP);
- 让 OpenTelemetry 与 OpenTracing 两种 API 创建的 Span 在同一个 Go
context.Context中互相可见、互为父子; - 让传播(Propagation)走 OpenTelemetry 的
TextMapPropagator体系,保证跨服务链路完整。
Cortex 正是这样做的:官方文档 docs/guides/tracing.md 明确说明“Cortex 尚未完全从 OpenTracing 迁移到 OpenTelemetry,当前使用 OpenTracing bridge”,其 go.mod 中直接依赖go.opentelemetry.io/otel/bridge/opentracing v1.45.0。
快速开始:安装与初始化桥接器
安装依赖
原文档给出的安装方式:
go get go.opentelemetry.io/otel/bridge/opentracing在 Cortex 这类已 vendoring 的仓库中,该桥接包位于 vendor/go.opentelemetry.io/otel/bridge/opentracing,包内主要源文件包括:
| 文件 | 职责 |
|---|---|
| bridge.go | BridgeTracer核心实现:OpenTracingTracer接口翻译为 OpenTelemetry 调用 |
| provider.go | TracerProvider:按名称/版本缓存并返回WrapperTracer |
| wrapper.go | WrapperTracer:包装 OpenTelemetry Tracer,负责双向上下文接线 |
| util.go | NewTracerPair/NewTracerPairWithContext便捷函数 |
| mock.go | 测试用 mock Tracer/Span |
| migration | 延迟上下文设置相关接口与函数 |
标准接线代码
假设你已经配置好一个 OpenTelemetryTracerProvider,原文档给出的完整接线步骤如下:
import ( "go.opentelemetry.io/otel" otelBridge "go.opentelemetry.io/otel/bridge/opentracing" ) func main() { /* Create tracerProvider and configure OpenTelemetry ... */ otelTracer := tracerProvider.Tracer("tracer_name") // Use the bridgeTracer as your OpenTracing tracer. bridgeTracer, wrapperTracerProvider := otelBridge.NewTracerPair(otelTracer) // Set the wrapperTracerProvider as the global OpenTelemetry // TracerProvider so instrumentation will use it by default. otel.SetTracerProvider(wrapperTracerProvider) /* ... */ }代码中有两个关键产物:
bridgeTracer:一个实现 OpenTracingTracer接口的对象(*BridgeTracer),应通过opentracing.SetGlobalTracer(bridgeTracer)注册为全局 OpenTracing Tracer;wrapperTracerProvider:一个实现 OpenTelemetryTracerProvider接口的对象,应通过otel.SetTracerProvider(wrapperTracerProvider)注册为全局 OpenTelemetry TracerProvider。
两个对象通过NewTracerPair在内部相互引用(见 util.go):NewBridgeTracer()创建桥接器,NewWrappedTracerProvider创建包装 Provider,随后bridgeTracer.SetOpenTelemetryTracer(wrapperProvider.Tracer(""))让桥接器转发到底层 OTel Tracer。
从源码结构看,NewTracerPairWithContext是带context.Context的变体(util.go),它会额外调用bridgeTracer.NewHookedContext(ctx),把 baggage 的 Set/Get 钩子挂进 context,适合在程序启动早期就获得一个“接线完成”的根 context。
深入理解 TracerPair 的内部结构
BridgeTracer:OpenTracing 侧的翻译器
BridgeTracer实现 OpenTracing 的ot.Tracer接口,同时实现ot.TracerContextWithSpanExtension扩展接口(bridge.go)。它的核心成员是:
type BridgeTracer struct { setTracer bridgeSetTracer warningHandler BridgeWarningHandler warnOnce sync.Once propagator propagation.TextMapPropagator }setTracer内部持有真正被转发的trace.Tracer。如果尚未调用SetOpenTelemetryTracer,则默认使用 no-op Tracer,并仅在首次使用时发出一次警告(warnOnce保证只警告一次):“The OpenTelemetry tracer is not set, default no-op tracer is used! Call SetOpenTelemetryTracer to set it up.”(bridge.go);warningHandler用于接收各类协作异常提示,默认空操作,可用SetWarningHandler覆盖(bridge.go);propagator为空时,Inject/Extract 会回退到otel.GetTextMapPropagator()获取全局传播器(bridge.go),也可以通过SetTextMapPropagator显式指定。
WrapperTracerProvider:OpenTelemetry 侧的桥
TracerProvider(provider.go)为每个 Tracer 名称/版本/schema/属性组合创建并缓存一个WrapperTracer;WrapperTracer(wrapper.go)在Start()时除了调用底层 OTel Tracer 创建 Span,还会执行两步额外动作:
- 若返回的 Span 实现了
migration.OverrideTracerSpanExtension接口,则调用其OverrideTracer(t)让 Span 返回 WrapperTracer 本身; - 若
migration.SkipContextSetup(ctx)返回 false,则调用bridge.ContextWithBridgeSpan(ctx, span),把新建的 OpenTelemetry Span 同时安装为 context 中的活跃 OpenTracing Span。
这样一来,无论 Span 由哪一侧 API 创建,另一侧 API 都能从同一个context.Context中取到它。
上下文互操作:让 OpenTracing Span 进入 OpenTelemetry 上下文
为何必须显式挂钩
原文档明确指出:要让 OpenTracing Span 正确进入 OpenTelemetry 上下文并被传播(内部与外部),必须使用BridgeTracer(而不是裸 OpenTracingTracer)创建 Span,并在创建后显式调用ContextWithSpanHook。原因在于 OpenTracing 的StartSpan不接收 Go context,Span 创建后需要手动把它“插入”context,桥接器才有机会触发延迟上下文设置。
标准做法
原文档给出的代码:
ctxWithOTSpan := opentracing.ContextWithSpan(ctx, otSpan) ctxWithOTAndOTelSpan := bridgeTracer.ContextWithSpanHook(ctxWithOTSpan, otSpan) // Propagate the otSpan to both OpenTracing and OpenTelemetry // instrumentation by using the ctxWithOTAndOTelSpan context.其中ContextWithSpanHook的实现逻辑(bridge.go):
- 若传入的 Span 不是
*bridgeSpan(即“外来 Span”),发出警告并原样返回 context,不做任何设置; - 若该 Span 设置了
skipDeferHook(例如它本身就是由 OpenTelemetry 侧WrapperTracer.Start创建的,已通过ContextWithBridgeSpan装好),直接返回; - 否则,若底层 OTel Tracer 实现了
migration.DeferredContextSetupTracerExtension接口,就调用其DeferredContextSetupHook(ctx, otSpan),让 OTel Tracer 完成它本该在Start()里做的 context 设置。
推荐的使用模式是:先opentracing.ContextWithSpan建立 OpenTracing 关联,再调用ContextWithSpanHook触发 OpenTelemetry 侧接线,最终把返回的ctxWithOTAndOTelSpan向下游传递。
扩展功能:访问 SpanContext 的 OpenTelemetry 方法
桥接器内部用bridgeSpanContext(bridge.go)实现 OpenTracing 的ot.SpanContext,它内嵌了 OpenTelemetry 的trace.SpanContext,同时维护一份 OpenTelemetry Baggage。因此,从 OpenTracing API 拿到的SpanContext实际上是一个“双面孔”对象——只要做一次类型断言,就能取到 OpenTelemetry 的 TraceID、SpanID、采样标志等信息。
原文档给出的扩展模式:
type spanContextProvider interface { IsSampled() bool TraceID() trace.TraceID SpanID() trace.SpanID TraceFlags() trace.TraceFlags ... // any other available method can be added here to access it } var sc opentracing.SpanContext = ... if s, ok := sc.(spanContextProvider); ok { // Use TraceID by s.TraceID() // Use SpanID by s.SpanID() // Use TraceFlags by s.TraceFlags() ... }由于bridgeSpanContext内嵌了trace.SpanContext,上述方法(IsSampled、TraceID、SpanID、TraceFlags等)全部可用。注意该接口定义中的...表示可根据需要扩展,例如补充IsRemote()、HasTraceID()、HasSpanID()等方法,只要这些方法存在于trace.SpanContext上即可。
另外,bridgeSpanContext还实现了ForeachBaggageItem(bridge.go),会把内部 OpenTelemetry Baggage 的成员逐项回传给 OpenTracing 侧,从而保证SpanContext.BaggageItem()/SetBaggageItem()的语义完整。
底层翻译机制:从 OpenTracing 调用到 OpenTelemetry 调用
StartSpan 的处理流程
BridgeTracer.StartSpan(bridge.go)是整座桥的核心:
- 收集
ot.StartSpanOption到StartSpanOptions; otSpanReferencesToParentAndLinks解析 References:第一个ChildOfRef作为父 SpanContext,其余引用转换为trace.Link,并在 link 属性ot-span-reference-type中标注extra-child-of或follows-from-ref(bridge.go);otTagsToOTelAttributesKindAndError把 Tag 翻译为 OTel Attributes、SpanKind 和 error 标志;- 以
migration.WithDeferredSetup(context.Background())构造临时 context 调用底层 OTel Tracer 的Start(); - 若返回的 context 中已有活跃 Span(说明 OTel Tracer 没有遵守“延迟设置”约定),通过
warnOnce发出一次警告; - 若 Tags 中
ext.Error=true,将 Span 状态置为codes.Error; - 基于 OTel SpanContext 构造
bridgeSpanContext与bridgeSpan返回给 OpenTracing 调用方。
Tag 与 Log 的翻译规则
otTagToOTelAttr(bridge.go)定义了 Tag 值的类型映射,需要注意这些“不直观”的转换:
| OpenTracing Tag 值类型 | OpenTelemetry Attribute 值类型 |
|---|---|
int | int64(Int64) |
uint | string(十进制字符串) |
int32 | int64 |
uint32 | int64 |
uint64 | string(十进制字符串) |
float32 | float64 |
float64 | float64 |
bool/string | 原样对应 |
特殊 Tag 的处理(见SetTag与otTagsToOTelAttributesKindAndError,bridge.go):
ext.SpanKind:映射为 OpenTelemetry 的SpanKindClient/SpanKindServer/SpanKindProducer/SpanKindConsumer,其余值回退为SpanKindInternal;ext.Error=true:调用SetStatus(codes.Error, "")标记错误状态,而不是作为普通属性写入;- 其他 Tag:全部转为普通 Attribute。
Log 方面,LogFields/LogKV/Log统一翻译为 OpenTelemetry 的AddEvent(bridge.go):通过bridgeFieldEncoder把otlog.Field编码为attribute.KeyValue列表,事件名使用空字符串,时间戳来自FinishOptions或LogRecord。
传播(Propagation):Inject 与 Extract
BridgeTracer的 Inject/Extract(bridge.go)目前支持 OpenTracing 内置格式HTTPHeaders与TextMap,其他格式返回ot.ErrUnsupportedFormat。实现要点:
- 要求传入的 SpanContext 必须是
*bridgeSpanContext,否则返回ot.ErrInvalidSpanContext; - 底层通过
textMapWrapper在 OpenTracing 的TextMapWriter/TextMapReader与 OpenTelemetry 的TextMapCarrier之间做兼容适配:当 carrier 只实现了读写接口之一时,自动补一个空实现(textMapWriter/textMapReader)以避免 panic(bridge.go); - Inject 时构造一个
fakeSpan携带bridgeSC.SpanContext,连同 Baggage 一起交给getPropagator().Inject; - Extract 时调用
getPropagator().Extract解析出 SpanContext 与 Baggage,若结果无效返回ot.ErrSpanContextNotFound。
值得注意:Inject对HTTPHeaders格式要求 carrier 是ot.HTTPHeadersCarrier,而 Cortex 的 gRPC 场景使用的是httpgrpcutil.HttpgrpcHeadersCarrier,直接调用会遇到不兼容问题——这正是 Cortex 自行包装BridgeTracer的原因(见下文)。
延迟上下文设置机制(migration 子包)
bridge/opentracing/migration 是让两类 API 在 Go context 语义差异下协作的关键:
WithDeferredSetup(ctx)在 context 中放入一个内部标记;SkipContextSetup(ctx)检测该标记——OpenTelemetry Tracer 在Start()里应据此决定“跳过 context 设置”;DeferredContextSetupTracerExtension接口要求 Tracer 实现DeferredContextSetupHook(ctx, span),在 OpenTracing 调用ContextWithSpan时补做本应在Start()完成的 context 设置;OverrideTracerSpanExtension接口允许 Span 重写其Tracer()返回值,保证 OpenTracing 侧看到的 Tracer 是 WrapperTracer。
为什么需要延迟?因为 OpenTracing 的StartSpan没有 context 参数,BridgeTracer只能先传一个临时 context 调用 OTelStart();若 OTel Tracer 此时擅自修改 context,改动会在StartSpan返回时丢失。约定是:OTel Tracer 检测到SkipContextSetup为 true 时应原样返回 context,等用户调用opentracing.ContextWithSpan时再由DeferredContextSetupHook完成接线(wrapper.go 展示了 WrapperTracer 的转发实现)。反之,若 Span 由 OpenTelemetry 侧Start()创建(SkipContextSetup为 false),Tracer 应额外调用ContextWithBridgeSpan把活跃 OpenTracing Span 装进 context。
Cortex 中的实际应用:从全局 Tracer 到查询前端
启动时的桥接器装配
Cortex 在 pkg/tracing/tracing.go 的SetupTracing中完成了整套桥接装配(type: otel分支):
bridge, wrappedProvider := migration.NewCortexBridgeTracerWrapper(tracerProvider.Tracer("github.com/cortexproject/cortex/cmd/cortex")) bridge.SetTextMapPropagator(propagator) opentracing.SetGlobalTracer(bridge) otel.SetTracerProvider(wrappedProvider)即:先用NewCortexBridgeTracerWrapper得到 Cortex 专属的桥接器包装与 WrapperTracerProvider,再分别注册为全局 OpenTracing Tracer 与全局 OpenTelemetry TracerProvider。这里的propagator由 newTraceProvider 决定:默认propagation.TraceContext{},当exporter_type: awsxray时切换为xray.Propagator{}并配合 X-Ray ID Generator。
CortexBridgeTracerWrapper:为何要再包一层
pkg/tracing/migration/bridge_wrapper.go 的CortexBridgeTracerWrapper内嵌了*bridge.BridgeTracer,主要动机在注释与测试中写得很清楚:上游 BridgeTracer 的Inject/Extract对HTTPHeaders格式要求 carrier 必须是opentracing.HTTPHeadersCarrier,而 Cortex 在 pkg/frontend/v1/frontend.go 中通过 gRPC 转发查询请求时使用httpgrpcutil.HttpgrpcHeadersCarrier(一种基于[]*httpgrpc.Header的 carrier),直接调用会报错。因此包装层在HTTPHeaders格式下先把 carrier 桥接为opentracing.HTTPHeadersCarrier,再调用底层桥接器,随后把写好的键值回填到原 carrier(或从原 carrier 读入后再 Extract)。
对应的单元测试 pkg/tracing/migration/bridge_wrapper_test.go 覆盖了三种组合:HTTPHeadersCarrier、HttpgrpcHeadersCarrier与HeaderCarrier,验证注入的 traceID 键值能同时被原 carrier 与自定义 mock propagator 读到。
查询前端中的真实调用
在 pkg/frontend/v1/frontend.go,Cortex 查询前端通过全局 OpenTracing Tracer 完成跨 gRPC 的 trace 注入:
tracer, span := opentracing.GlobalTracer(), opentracing.SpanFromContext(ctx) if tracer != nil && span != nil { carrier := (*httpgrpcutil.HttpgrpcHeadersCarrier)(req) err := tracer.Inject(span.Context(), opentracing.HTTPHeaders, carrier) ... }由于全局 Tracer 已被注册为 Cortex 的桥接包装器,这里的Inject最终走的是 BridgeTracer 的翻译逻辑,把 OpenTracing Span 的 trace 信息通过 OTel Propagator 注入到 gRPC 请求头中,从而让下游组件(如 query-frontend 队列中的 Span,见 frontend.go 的opentracing.StartSpanFromContext(ctx, "queued"))维持同一条 trace。这印证了桥接器在 Cortex 内部“查询链路追踪”上的实际价值:既有 OpenTracing 埋点(StartSpanFromContext、Inject)无需重写,Span 数据即汇入 OpenTelemetry 采集体系。
使用注意事项与最佳实践
- 必须成对注册:
bridgeTracer与wrapperTracerProvider需要分别注册为全局 OpenTracing Tracer 与全局 OpenTelemetry TracerProvider,缺一不可,否则会出现一侧创建 Span、另一侧 context 里取不到的情况。 - 先设置 OTel Tracer 再使用:未调用
SetOpenTelemetryTracer时 BridgeTracer 转发到 no-op Tracer,Span 不会真正上报(仅发出一次性警告)。 - 使用 Hook 保持上下文同步:OpenTracing 侧创建 Span 后,务必调用
ContextWithSpanHook(并配合opentracing.ContextWithSpan),否则 OpenTelemetry 侧看不到该 Span。 - 传播格式有限制:Inject/Extract 目前只支持
HTTPHeaders与TextMap两种内置格式;非标准 carrier(如 gRPC Header carrier)需要像 Cortex 那样自行包装适配(参考 bridge_wrapper.go)。 - 类型转换差异:
uint/uint64Tag 会被转为字符串、int被转为int64,依赖数值类型的消费者需留意。 - 监控协作异常:通过
SetWarningHandler捕获桥接器发出的警告(如 foreign span、未设置 OTel tracer、SDK 未延迟 context 设置等),有助于排查链路断裂问题。
- 可观测性
- 时序数据库
- 后端
- 指标监控
【免费下载链接】cortex
A horizontally scalable, highly available, multi-tenant, long term Prometheus.
相关推荐
OpenTelemetry Go 与 OpenTracing 桥接指南:使用 bridge/opentracing 无缝迁移与双向互操作
OpenTelemetry Go 与 OpenTracing 桥接指南:使用 bridge/opentracing 无缝迁移与双向互操作 导读 :在存量系统中,
可观测性godebug命令行详解:掌握n/step/c/print等调试指令的使用技巧
godebug命令行详解:掌握n/step/c/print等调试指令的使用技巧 godebug是一款基于源代码生成的Go语言调试工具,通过在代码中插入调试调用实
Go kit 分布式请求追踪指南:Zipkin、OpenCensus、OpenTracing 与 OpenTelemetry 集成实战
Go kit 分布式请求追踪指南:Zipkin、OpenCensus、OpenTracing 与 OpenTelemetry 集成实战 Go kit 作为微服务
微服务后端RPC框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考