news 2026/9/19 17:08:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 Protocol(OTLP)OpenTelemetry 官方协议标准 OTLP 通道
ThriftBatchJaeger IDL 中定义的 Thrift 模型UDP 或 HTTP
ProtobufBatchJaeger 模型 v2 的 Protobuf 定义gRPC

本文档定义的是OTLP 与 Jaeger Span 之间的转换规则。OpenTelemetry 规范中"非 OTLP 映射"章节给出的是通用映射规则;当通用规则与本文规则冲突时,必须优先采用本文(文档)中的规则。这意味着 Jaeger 翻译器是一份"特殊优先于一般"的映射实现,任何对该实现的修改都必须遵循这一优先级原则。

在 Tempo 仓库中,这份文档与实现位于 translator/jaeger/ 目录下,包含四个核心 Go 文件:traces_to_jaegerproto.go(OTLP → Jaeger Proto)、jaegerproto_to_traces.go(Jaeger Proto → OTLP)、jaegerthrift_to_traces.go(Jaeger 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 映射为 Logs
Span.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 位整数)表示 ID;128 位的 Trace ID 则拆成两个i64字段traceIdLowtraceIdHigh

转换规则有两个硬性要求:

  1. 字节必须按 Big Endian 字节序与无符号整数互转,例如[0x10, 0x00, 0x00, 0x00] == 268435456
  2. 无符号整数必须通过"重新解释"转换为有符号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 ID(parentSpanId);
  • Jaeger Proto:不支持顶层 parent ID 字段,父 Span 必须记录为一条SpanReference,其ref_typeCHILD_OF,并且这条引用必须是引用列表中的第一条

Python 示意(对应 OpenTracing 语义约定):

SpanReference( ref_type=opentracing.CHILD_OF, trace_id=span.context.trace_id, span_id=parent_id, )

源码侧的印证:在jaegerthrift_to_traces.go中,jThriftSpanParentID从 Thrift Span 的顶层字段取父 ID;而在jaegerproto_to_traces.gojReferencesToSpanLinks中,会显式排除 parent ID(excludeParentID)后再把其余引用转换为 OTLP 的 SpanLink。这正体现了"Proto 的父关系编码在引用里,Thrift 的父关系编码在顶层字段里"这一不对称设计。

SpanKind 映射:编码为 span.kind 标签

OpenTelemetry 的SpanKind必须编码为 Jaeger Span 上的span.kind标签,唯一的例外是SpanKind.INTERNAL——它不应被翻译为任何标签(内部 Span 在 Jaeger 语义中没有对应的 kind 概念)。

OpenTelemetryJaeger 值
SpanKind.CLIENT"client"
SpanKind.SERVER"server"
SpanKind.CONSUMER"consumer"
SpanKind.PRODUCER"producer"
SpanKind.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.Timestampgoogle.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时,必须添加一个值为布尔trueerror标签,且该标签可以覆盖此前已有的任何同名值。

jaegerproto_to_traces.go中,setInternalSpanStatus负责从 Jaeger 标签反解 OTLP Status:它会识别error标签、status.codeotel.status_code等约定键,并能从 HTTP 状态属性推导状态码(getStatusCodeFromHTTPStatusAttr依据 Span kind 与 HTTP 状态码决定是 Error 还是 Ok)。这套"标签双向传递状态"的机制,保证了跨系统时错误状态不会丢失。

Attributes 映射为 tags 标签

OTLP Span 的 Attribute(s) 必须作为 tags 上报给 Jaeger:

  • 原始类型(字符串、整数、浮点、布尔)直接对应 Jaeger 标签的相应类型;
  • 数组值必须按语义约定序列化为 JSON 风格的字符串(例如[1, 2, 3])。

从实现看,attributeToJaegerProtoTagappendTagsFromAttributes完成了从pcommon.Valuemodel.KeyValue的类型映射;反向的jTagsToInternalAttributes则把 Jaeger 标签还原为 OTLP 属性。对于无法直接表达的数组与 Map 类型,翻译器统一走字符串序列化通道,这是两种模型属性体系差异最小的平滑点,也是实践中最常用的映射路径。

Links 映射为 SpanReference 引用

OTLP 的 Link(s) 必须转换为 Jaeger 的SpanReference,引用类型使用FOLLOWS_FROM。由于 Jaeger 无法显式表示 Link 的属性,导出器可以额外把 Link 转换为 Span 的 Log,转换约定如下:

  • 使用 Span 的开始时间作为该 Log 的时间戳;
  • 设置 Log 标签event=link
  • 从对应 SpanContext 的字段中设置trace_idspan_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 Log:

  • Event 的time_unix_nano直接映射为 Log 的timestamp
  • Event 的attributes直接映射为 Log 的fields
  • Event 的name字段在 Jaeger Log 中没有直接对应物,但 OpenTracing 语义约定定义了特殊属性名,因此 Eventname应作为 Log 的fields项写入,键为event
OpenTelemetry 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 Proto
jaegerproto_to_traces.goProtoToTraces(batches []*model.Batch) (ptrace.Traces, error)Jaeger Proto → OTLP
jaegerthrift_to_traces.goThriftToTraces(batches *jaeger.Batch) (ptrace.Traces, error)Jaeger Thrift → OTLP
constants.gostatusError/statusOk/eventNameAttr等常量共享映射常量

其中ProtoFromTraces内部通过resourceSpansToJaegerProtoresourceToJaegerProtoProcess+spanToJaegerProto完成整条资源与 Span 的转换链路;ProtoToTraces则先通过regroup/batchForProcess按进程(含对 Process 的哈希归类)重新组织 Batch,再由jSpanToInternaljTagsToInternalAttributesjLogsToSpanEvents逐层还原为 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 已携带 Process,plugin.go 再逐 Span 回填s.Process = batch.Process,并依据Process.ServiceName构建ProcessMap返回给 Jaeger Query——这一步恰好印证了本文 Resource 映射 一节中"ServiceName 是 Jaeger 识别服务的关键字段"的结论。

此外,该插件同时实现了SpanReaderPluginServerDependenciesReaderPluginServerSpanWriterPluginServer三个 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),仅供参考

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

免费 OSINT 工具 Blackbird:快速完成 600+ 平台账号搜索

免费 OSINT 工具 Blackbird:快速完成 600 平台账号搜索 【免费下载链接】blackbird An OSINT tool to search for accounts by username and email in social networks. 项目地址: https://gitcode.com/GitHub_Trending/bl/blackbird Blackbird 是一款免费的…

作者头像 李华
网站建设 2026/9/19 17:04:42

2023数据中台建设方案:DataOps驱动的MVP落地实践

简介:本资源为一份面向企业数字化转型实践者、数据架构师与中台建设团队的2023年数据中台项目建设方案,聚焦解决多源数据分散、治理低效、指标口径不一、资产价值难量化等典型痛点。方案全文以Word文档(.docx)形式呈现&#xff0c…

作者头像 李华
网站建设 2026/9/19 17:02:19

DeepSeek 降重版测评,论文改写的 Base URL 填 TaoToken 的 API 地址

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

作者头像 李华