news 2026/10/13 1:38:34

Cortex 中的 OpenTelemetry/OpenTracing 桥接器(bridge/opentracing)实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cortex 中的 OpenTelemetry/OpenTracing 桥接器(bridge/opentracing)实战指南
  • 可观测性
  • 时序数据库
  • 后端
  • 指标监控

【免费下载链接】cortex

A horizontally scalable, highly available, multi-tenant, long term Prometheus.

项目地址:https://gitcode.com/gh_mirrors/cortex6/cortex
点击查看免费下载

导读

本篇文章围绕 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 在同一个 Gocontext.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.goBridgeTracer核心实现:OpenTracingTracer接口翻译为 OpenTelemetry 调用
provider.goTracerProvider:按名称/版本缓存并返回WrapperTracer
wrapper.goWrapperTracer:包装 OpenTelemetry Tracer,负责双向上下文接线
util.goNewTracerPair/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) /* ... */ }

代码中有两个关键产物:

  1. bridgeTracer:一个实现 OpenTracingTracer接口的对象(*BridgeTracer),应通过opentracing.SetGlobalTracer(bridgeTracer)注册为全局 OpenTracing Tracer;
  2. 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,还会执行两步额外动作:

  1. 若返回的 Span 实现了migration.OverrideTracerSpanExtension接口,则调用其OverrideTracer(t)让 Span 返回 WrapperTracer 本身;
  2. 若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)是整座桥的核心:

  1. 收集ot.StartSpanOption到StartSpanOptions;
  2. otSpanReferencesToParentAndLinks解析 References:第一个ChildOfRef作为父 SpanContext,其余引用转换为trace.Link,并在 link 属性ot-span-reference-type中标注extra-child-of或follows-from-ref(bridge.go);
  3. otTagsToOTelAttributesKindAndError把 Tag 翻译为 OTel Attributes、SpanKind 和 error 标志;
  4. 以migration.WithDeferredSetup(context.Background())构造临时 context 调用底层 OTel Tracer 的Start();
  5. 若返回的 context 中已有活跃 Span(说明 OTel Tracer 没有遵守“延迟设置”约定),通过warnOnce发出一次警告;
  6. 若 Tags 中ext.Error=true,将 Span 状态置为codes.Error;
  7. 基于 OTel SpanContext 构造bridgeSpanContext与bridgeSpan返回给 OpenTracing 调用方。

Tag 与 Log 的翻译规则

otTagToOTelAttr(bridge.go)定义了 Tag 值的类型映射,需要注意这些“不直观”的转换:

OpenTracing Tag 值类型OpenTelemetry Attribute 值类型
intint64(Int64)
uintstring(十进制字符串)
int32int64
uint32int64
uint64string(十进制字符串)
float32float64
float64float64
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.

项目地址:https://gitcode.com/gh_mirrors/cortex6/cortex
点击查看免费下载
上一篇:em-proxy: 一个轻量级的网络代理服务器
下一篇:PoshC2项目维护与更新策略:确保长期稳定运行的实用建议

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:38:32

java项目实战滑雪场管理系统[文档+开题+PPT

🍅选题推荐——以防找不到我们,点击上方订阅专栏✌✌\ Java毕设实战项目 Python毕设项目源代码 asp.net毕业设计项目 Uniapp安卓毕业设计项目 node.js毕业设计项目 python毕业设计 微信小程序毕业设计项目 php毕业设计 👇🏻&#…

作者头像 李华
网站建设 2026/10/13 1:38:29

AI 时代的:一个老程序员的转型、困境与自白

一、我曾经的"黄金时代"作为一个写了二十多年代码的传统程序员,我至今怀念以前的工作模式。那时候,一个性能问题、一个 bug、甚至一个莫名其妙的坑,常常让我在代码里兜兜转转几天、几个星期。但解决之后那种满满的成就感&#xff0…

作者头像 李华
网站建设 2026/10/13 1:37:41

图分析引擎技术方案调研:需求拆解、选型维度与落地路线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:37:34

Spring AOP拦截器链与proceed()递归调用原理深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:37:18

PyTorch官方指南翻译实践:从术语表到代码验证的完整复盘

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:36:06

带权并查集入门:从A Bug‘s Life理解关系维护与路径压缩

刷POJ的并查集题单,有一道题绕不开:A Bugs Life。我第一次做它的时候,用普通并查集硬怼了半天,WA了快十发才反应过来——这题根本不是“把虫子分成几堆”那么简单,它需要给并查集的每条边加上权值,去表示集…

作者头像 李华