多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

OpenTelemetry 与 Jaeger 数据模型转换全解:Grafana Tempo 中 jaeger translator 的字段映射规则与实现原理

OpenTelemetry 与 Jaeger 数据模型转换全解:Grafana Tempo 中 jaeger translator 的字段映射规则与实现原理 OpenTelemetry 与 Jaeger 数据模型转换全解Grafana Tempo 中 jaeger translator 的字段映射规则与实现原理【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo本文系统讲解 OpenTelemetry 与 Jaeger 两种追踪数据模型之间的双向转换规则核心依据为 Grafana Tempo 仓库中 vendored 的 OpenTelemetry Collector Contrib 翻译器文档 translator/jaeger/README.md并以其源码实现为佐证。你将掌握 TraceId/SpanId、ParentId、SpanKind、Status、Attributes、Events、Links 等核心字段在 OTLP、Jaeger Thrift、Jaeger Protobuf 三种格式间的精确映射方法并了解该翻译器在 Tempo 的 tempo-query 插件中如何被真实调用。背景三种可被 Jaeger 接受的 Span 格式Jaeger 后端本身不消费 OTLP 数据它原生接受三种格式的 Span而这正是翻译器需要解决的核心问题格式定义传输方式OpenTelemetry ProtocolOTLPOpenTelemetry 官方协议标准 OTLP 通道ThriftBatchJaeger IDL 中定义的 Thrift 模型UDP 或 HTTPProtobufBatchJaeger 模型 v2 的 Protobuf 定义gRPC本文档定义的是OTLP 与 Jaeger Span 之间的转换规则。OpenTelemetry 规范中非 OTLP 映射章节给出的是通用映射规则当通用规则与本文规则冲突时必须优先采用本文文档中的规则。这意味着 Jaeger 翻译器是一份特殊优先于一般的映射实现任何对该实现的修改都必须遵循这一优先级原则。在 Tempo 仓库中这份文档与实现位于 translator/jaeger/ 目录下包含四个核心 Go 文件traces_to_jaegerproto.goOTLP → Jaeger Proto、jaegerproto_to_traces.goJaeger Proto → OTLP、jaegerthrift_to_traces.goJaeger Thrift → OTLP以及constants.go共享常量。字段映射总览一张表看懂核心对应关系下表概括了 OpenTelemetry Span 与 Jaeger Thrift、Jaeger Proto 之间的主要转换关系是理解整个翻译器行为的入口OpenTelemetryJaeger ThriftJaeger Proto说明Span.TraceIdSpan.traceIdLow/HighSpan.trace_id详见 IDs 转换Span.ParentIdSpan.parentSpanId作为 SpanReference详见 Parent ID 映射Span.SpanIdSpan.spanIdSpan.span_id直接映射Span.TraceStateTBDTBD规范中尚未定稿Span.NameSpan.operationNameSpan.operation_name直接映射Span.KindSpan.tags[span.kind]同上值映射见 SpanKind 映射Span.StartTimeSpan.startTimeSpan.start_time时间单位见 时间单位规则Span.EndTimeSpan.duration同上计算为 EndTime − StartTime单位规则同上Span.AttributesSpan.tags同上数据类型映射见 Attributes 映射Span.DroppedAttributesCount追加到 Span.tags同上标签名遵循 Dropped Attributes Count 约定Span.EventsSpan.logs同上映射格式见 Events 映射为 LogsSpan.DroppedEventsCount追加到 Span.tags同上标签名遵循 Dropped Events Count 约定Span.LinksSpan.references同上见 Links 映射为引用Span.DroppedLinksCount追加到 Span.tags同上标签名遵循 Dropped Links Count 约定Span.Status追加到 Span.tags同上标签名见 Status 与 error 标记值得注意的是Jaeger Thrift 与 Jaeger Proto 在多数字段上是等价的主要差异集中在 ID 表示、ParentId 编码方式与时间精度上——这正是后续小节深入展开的难点。Resource 映射服务身份的来源OpenTelemetry 的Resource 必须映射为 Jaeger 的Span.Process标签。一个进程可以对应多个 Resource导出器需要自行处理这种多对一情况例如将多个 Resource 的标签合并到同一个 Process或拆分到多个 Batch。关键在于Jaeger 后端依赖Span.Process.ServiceName来识别产生 Span 的服务。因此该字段必须从 service Resource 的service.name属性填充如果 Span 的 Resource 中不含service.name则必须从 SDK 提供的默认 Resource 中获取。在 Tempo 的实际场景中这一映射通过resourceToJaegerProtoProcess实现从pcommon.Resource提取属性并生成model.Process随后由 plugin.go 中的调用者把 Batch 的 Process 挂回每个 Span并基于ServiceName构建ProcessMap保证 Jaeger UI 能正确按服务维度聚合。IDs 转换ID 的字节序与符号位处理Trace ID 和 Span ID 在 Jaeger 中是随机字节序列。但 Thrift 模型使用i64有符号 64 位整数表示 ID128 位的 Trace ID 则拆成两个i64字段traceIdLow和traceIdHigh。转换规则有两个硬性要求字节必须按 Big Endian 字节序与无符号整数互转例如[0x10, 0x00, 0x00, 0x00] 268435456无符号整数必须通过重新解释转换为有符号i64——即保持底层 64 位二进制不变仅改变解读方式。文档给出了 Go 语言的参考示例var ( id []byte []byte{0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} unsigned uint64 binary.BigEndian.Uint64(id) signed int64 int64(unsigned) ) fmt.Println(unsigned:, unsigned) fmt.Println( signed:, signed) // Output: // unsigned: 18374686479671623680 // signed: -72057594037927936可以看到同一个 8 字节序列在高位为0xFF时按uint64解读是一个巨大的正数按int64解读则是负数——这就是重新解释的含义。Jaeger 翻译器在进行 Thrift 与 OTLP 互转时必须严格遵循此字节序约定否则跨系统交换的 Trace ID 将无法对应到同一链路。Parent ID 映射Thrift 顶层字段与 Proto 引用两种 Jaeger 模型对父 Span 的表示方式不同这是最容易踩坑的差异点Jaeger Thrift允许在 Span 的顶层字段中直接保存 parent IDparentSpanIdJaeger Proto不支持顶层 parent ID 字段父 Span 必须记录为一条SpanReference其ref_type为CHILD_OF并且这条引用必须是引用列表中的第一条。Python 示意对应 OpenTracing 语义约定SpanReference( ref_typeopentracing.CHILD_OF, trace_idspan.context.trace_id, span_idparent_id, )源码侧的印证在jaegerthrift_to_traces.go中jThriftSpanParentID从 Thrift Span 的顶层字段取父 ID而在jaegerproto_to_traces.go的jReferencesToSpanLinks中会显式排除 parent IDexcludeParentID后再把其余引用转换为 OTLP 的 SpanLink。这正体现了Proto 的父关系编码在引用里Thrift 的父关系编码在顶层字段里这一不对称设计。SpanKind 映射编码为 span.kind 标签OpenTelemetry 的SpanKind必须编码为 Jaeger Span 上的span.kind标签唯一的例外是SpanKind.INTERNAL——它不应被翻译为任何标签内部 Span 在 Jaeger 语义中没有对应的 kind 概念。OpenTelemetryJaeger 值SpanKind.CLIENTclientSpanKind.SERVERserverSpanKind.CONSUMERconsumerSpanKind.PRODUCERproducerSpanKind.INTERNAL不添加span.kind标签反向转换时jSpanKindToInternal会把 Jaeger 的字符串 kind 还原为 OTLP 的SpanKind而对于缺少span.kind标签的 Jaeger Span则默认按内部 Span 处理。这在 Tempo 场景中很常见由 OpenTelemetry SDK 内部探针产生的 Span 通常不带 kind转换后不会被误判为客户端或服务端调用。时间单位规则微秒与纳秒两种 Jaeger 模型对时间精度的要求截然不同Jaeger Thrift时间戳和时长必须使用微秒时间戳为自 epoch 起的微秒数。若 OTLP 原始值是纳秒必须四舍五入或截断到微秒Jaeger Proto时间戳和时长使用纳秒精度通过google.protobuf.Timestamp与google.protobuf.Duration类型表示。这意味着 OTLP → Thrift 是有精度损失的转换纳秒 → 微秒而 OTLP → Proto 则无损。源码中microsecondsToUnixNano在jaegerthrift_to_traces.go正是 Thrift → OTLP 方向的微秒转纳秒实现反过来OTLP → Thrift 时需要将pcommon.Timestamp纳秒换算为微秒。对于需要跨格式比对开始时间与持续时间的场景例如在 tempo-query 中同时服务 Thrift 与 Proto 两种 API务必留意单位换算避免毫秒级误判。Status 与 error 标记OTLP 的 Span Status 同样以 Span 标签的形式记录到 Jaeger标签名遵循非 OTLP 映射规范中 Span Status 的约定。constants.go中明确固化了两个状态常量const ( statusError ERROR statusOk OK )Error 标记规则当 Span Status 为ERROR时必须添加一个值为布尔true的error标签且该标签可以覆盖此前已有的任何同名值。在jaegerproto_to_traces.go中setInternalSpanStatus负责从 Jaeger 标签反解 OTLP Status它会识别error标签、status.code、otel.status_code等约定键并能从 HTTP 状态属性推导状态码getStatusCodeFromHTTPStatusAttr依据 Span kind 与 HTTP 状态码决定是 Error 还是 Ok。这套标签双向传递状态的机制保证了跨系统时错误状态不会丢失。Attributes 映射为 tags 标签OTLP Span 的 Attribute(s) 必须作为 tags 上报给 Jaeger原始类型字符串、整数、浮点、布尔直接对应 Jaeger 标签的相应类型数组值必须按语义约定序列化为 JSON 风格的字符串例如[1, 2, 3]。从实现看attributeToJaegerProtoTag与appendTagsFromAttributes完成了从pcommon.Value到model.KeyValue的类型映射反向的jTagsToInternalAttributes则把 Jaeger 标签还原为 OTLP 属性。对于无法直接表达的数组与 Map 类型翻译器统一走字符串序列化通道这是两种模型属性体系差异最小的平滑点也是实践中最常用的映射路径。Links 映射为 SpanReference 引用OTLP 的 Link(s) 必须转换为 Jaeger 的SpanReference引用类型使用FOLLOWS_FROM。由于 Jaeger 无法显式表示 Link 的属性导出器可以额外把 Link 转换为 Span 的 Log转换约定如下使用 Span 的开始时间作为该 Log 的时间戳设置 Log 标签eventlink从对应 SpanContext 的字段中设置trace_id和span_id两个 Log 标签将 Link 的属性存储为 Log 标签。顺序约束由 Link 生成的 Span 引用必须添加在由 Parent ID 生成的引用即CHILD_OF之后。这保证了 Jaeger UI 渲染时父子关系始终优先、跨进程关联次之。反向转换时jRefTypeToAttribute会把 Jaeger 的CHILD_OF/FOLLOWS_FROM引用类型还原为 OTLP Link 上的语义属性便于后续查询分析。Events 映射为 Logs 日志OTLP Event 必须转换为 Jaeger LogEvent 的time_unix_nano直接映射为 Log 的timestampEvent 的attributes直接映射为 Log 的fieldsEvent 的name字段在 Jaeger Log 中没有直接对应物但 OpenTracing 语义约定定义了特殊属性名因此 Eventname应作为 Log 的fields项写入键为eventOpenTelemetry Event 字段Jaeger 属性nameevent优先级规则如果 Event 自身已包含键为event的属性则该属性优先于Event 的name字段。这条规则在constants.go中以eventNameAttr event常量固化并在 Event/Log 双向转换逻辑中一致使用确保显式属性 隐式 name的语义在转换后不产生歧义。源码结构翻译器的四个核心入口从 translator/jaeger/ 目录的源码结构可以清晰看到该翻译器提供的全部公开能力与上文各小节一一对应文件公开入口方向traces_to_jaegerproto.goProtoFromTraces(td ptrace.Traces) []*model.BatchOTLP → Jaeger Protojaegerproto_to_traces.goProtoToTraces(batches []*model.Batch) (ptrace.Traces, error)Jaeger Proto → OTLPjaegerthrift_to_traces.goThriftToTraces(batches *jaeger.Batch) (ptrace.Traces, error)Jaeger Thrift → OTLPconstants.gostatusError/statusOk/eventNameAttr等常量共享映射常量其中ProtoFromTraces内部通过resourceSpansToJaegerProto→resourceToJaegerProtoProcessspanToJaegerProto完成整条资源与 Span 的转换链路ProtoToTraces则先通过regroup/batchForProcess按进程含对 Process 的哈希归类重新组织 Batch再由jSpanToInternal、jTagsToInternalAttributes、jLogsToSpanEvents逐层还原为 OTLP 结构。这种文件职责单一、函数分层清晰的设计正是文档中每一条映射规则能够被精确落实为代码的原因。实战印证Tempo 的 tempo-query 如何消费该翻译器在 Grafana Tempo 仓库中该翻译器被 tempo-query 插件实际使用用于把 Tempo 的 OTLP 查询结果转换回 Jaeger 模型、响应 Jaeger Query 的 gRPC 存储插件协议。核心调用链位于 plugin.go 的getTrace方法中otTrace, err : (ptrace.ProtoUnmarshaler{}).UnmarshalTraces(body) if err ! nil { return nil, fmt.Errorf(error unmarshalling body to otlp trace %v: %w, traceID, err) } jaegerBatches : ot_jaeger.ProtoFromTraces(otTrace)具体流程为Tempo 返回 OTLP 格式的 trace body → 用ptrace.ProtoUnmarshaler解析为 OTLP Traces → 调用ot_jaeger.ProtoFromTraces得到 Jaeger[]*model.Batch。由于文档规定 Resource 必须落在Span.Process上而ProtoFromTraces生成的 Batch 已携带 Processplugin.go 再逐 Span 回填s.Process batch.Process并依据Process.ServiceName构建ProcessMap返回给 Jaeger Query——这一步恰好印证了本文 Resource 映射 一节中ServiceName 是 Jaeger 识别服务的关键字段的结论。此外该插件同时实现了SpanReaderPluginServer、DependenciesReaderPluginServer、SpanWriterPluginServer三个 Jaeger 存储插件接口把GetTrace/FindTraces/FindTraceIDs/GetServices/GetOperations等 Jaeger 查询语义翻译为 Tempo 的/api/traces/{traceID}、/api/search、/api/search/tag/{tag}/values等 REST 端点构成Jaeger UI → tempo-query → Tempo API的完整查询链路。小结OpenTelemetry 与 Jaeger 的数据模型转换并不只是简单的字段改名而是涉及字节序重解释、父引用编码差异、时间精度取舍、标签语义约定等多层细节。本文基于 Grafana Tempo 仓库内 vendored 的 翻译器文档 与 实现源码完整梳理了 Thrift/Proto/OTLP 三格式间的全部核心映射规则并通过 tempo-query 插件 展示了这些规则在生产链路上的真实落地方式。无论是自研导出器、排查跨系统 Trace 关联丢失问题还是为 Tempo 扩展 Jaeger 兼容查询能力本文的映射表与源码路径都可作为直接的参考依据。【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表