OTEL 协议入门

目录

[0.1 先记住这张图](#0.1 先记住这张图)

[0.2 四类对象分别是什么](#0.2 四类对象分别是什么)

[A. 信号(Signals)------数据长什么样](#A. 信号(Signals)——数据长什么样)

[B. 规范(Specification)------大家必须遵守的契约](#B. 规范(Specification)——大家必须遵守的契约)

[C. 实现(Implementations)------规范的落地代码](#C. 实现(Implementations)——规范的落地代码)

[D. 后端产品(Backends)------ 存和查](#D. 后端产品(Backends)—— 存和查)

[0.3 每个信号的「四件套」(官方结构)](#0.3 每个信号的「四件套」(官方结构))

[0.4 数据走完一整条路,OTLP 只负责其中一跳](#0.4 数据走完一整条路,OTLP 只负责其中一跳)

[0.5 用一个具体场景把对象钉死](#0.5 用一个具体场景把对象钉死)

[0.6 常见混用](#0.6 常见混用)


0.1 先记住这张图

人们说「OTEL 协议」时,嘴里通常混了四样东西:

┌─────────────────────────────────────┐

│ OpenTelemetry(OTel)= 一整套开源标准与实现 │

│ │

│ ① 信号:Traces / Metrics / Logs / Baggage / Profiles │

│ ② 规范:API + SDK + 语义约定 + OTLP 怎么编码 │

│ ③ 实现:各语言 SDK、Collector、Operator... │

└──────────┬──────────────────────────┘

│ 其中真正「在网上跑」的那一段

④ OTLP(传输协议)

OpenObserve / Jaeger / Prometheus ...

(后端产品,不是 OTel 的一部分)

一句话:

  • OTel 是标准 + 工具箱
  • OTLP 是工具箱里负责「把数据从 A 送到 B」的那份传输协议
  • OpenObserve 是把数据存起来给你查的产品
  • Logs / Traces / Metrics 是数据种类(信号),不是产品,也不是三套协议

必看文档

  1. ​编辑Instrumentation
  2. ​编辑Collector
  3. 按你工作语言选一篇 Getting Started:
    ​编辑Dev 入门 → 点进 Java / Go / Python / Node
  4. OpenObserve:​编辑Ingestion
  5. 中文长文(选读):​编辑OpenTelemetry 2026 深度解析

操作

  1. ​编辑LFS148 Getting Started with OpenTelemetry
    约 10 小时,含自动/手动埋点、Collector lab,Linux Foundation 平台,免费。
  2. B 站可搜「OpenTelemetry 入门」「OTel Collector」,当辅助
  3. 七米 Go 云原生课里有 OTel 几集(偏 Go,语言对口再看)

OpenTelemetry(OTEL)是 CNCF 的开源项目,官方源码都在 GitHub 组织 open-telemetry 下,许可证一般为 Apache 2.0。

常用官方仓库:

用途 仓库
官网 / 文档 opentelemetry.io · opentelemetry.io
规范 opentelemetry-specification
协议(OTLP protobuf) opentelemetry-proto
Collector 核心 opentelemetry-collector
Collector 社区组件 opentelemetry-collector-contrib
语言 SDK Go · Java · Python · JS · .NET

0.2 四类对象分别是什么

A. 信号(Signals)------数据长什么样

信号 = 遥测数据的种类。系统对外「说」的内容分这几类:

信号 回答的问题 是不是「一次请求」
Traces 这一次请求走过哪些服务、哪一段慢/错
Metrics 一段时间里 QPS、错误率、延迟分布 否(聚合)
Logs 某个时刻发生了什么、细节是什么 不一定绑在某次请求上
Baggage 沿请求传递的业务键值(租户、实验组) 跟着请求走,但不是观测数据
Profiles 代码级 CPU/内存占用(较新) 另一类信号

它们在 OTel 出现之前就有。OTel 没有发明日志或指标,只规定:怎么生成、怎么命名、怎么互相关联、怎么运走。

所以不能说Logs 是 OTel 的产品。正确说法是:Logs 是 OTel 支持的一种信号


Baggage 是随这次请求一起"捎带"的键值上下文,不是 Trace 本身,也不会自动出现在 OpenObserve 的 Span 树上。

名字就这个意思:请求像一次旅行,traceparent 是身份证(这次调用是谁、父子怎么接),Baggage 是随身行李------下游服务可能用得上的业务信息,例如 tenant=acmeuser.id=42region=cn

跨服务时通常走 HTTP 头:

traceparent: 00-<trace_id>-<span_id>-01

baggage: tenant=acme,user.id=42

traceparent 负责把链路拼成同一棵树;baggage 只是把这些键值原样传到下一跳。

和另外两个容易混的东西对比:

干什么 后端能不能直接看见
Trace Context 身份:trace_id / span_id 能,用来拼树
Span Attribute 记在某个 span 上,给观测用 能,在该 span 上
Baggage 给下游代码读(路由、计费、采样决策) 默认不能,除非业务显式写进 Attribute

所以:网关往 Baggage 里塞了 tenant=acme,订单服务能读到并按租户查库;OpenObserve 里却不一定有这个字段。要在 Trace 里看见,需要再 span.set_attribute("tenant", ...)


拼链路只靠 traceparent(外加可选的 tracestate)。Baggage 不是固定字段表,只传「下游代码真的会读」的业务标签。

传给下一跳的,一共就这几类:

HTTP 头 要不要带 干什么
traceparent 要拼成一棵树就必须带 身份:同一 trace_id、本段 span_id、是否采样
tracestate 可选 厂商私货,例如某 APM 的采样决策;W3C Trace Context 的第二部分
baggage 可选 业务键值,给下游代码用,不是给 OpenObserve 拼树用

这三样是 服务 A → 服务 B 的传播(Propagator 写入 HTTP/gRPC metadata)。

不会随下一跳服务请求传过去的:

  • Span 的 http.methoddb.statement 等 Attribute
  • service.name 等 Resource
  • Span Event / Status

那些走的是另一跳:进程 → 接收端/OpenObserve(OTLP /v1/traces。下游服务收不到,也不该靠它们来知道「这次请求是哪个租户」。

若环境不是 W3C,可能看到 b3uber-trace-id 等替代身份头。双方必须同一套,否则 Extract 失败、下游自己成根。

如果代码用的是 TraceContextTextMapPropagator(),即使 Context 里有 Baggage,出站请求也不会写出 baggage 头。SDK 默认常用组合是 Trace Context + Baggage 两个 Propagator。


Baggage 要传什么键值对?

规范不定死键名。 只约定格式:逗号分隔的 key=value,值要百分号编码。放什么由业务决定。

该放的原则就一条:下一跳(或再下一跳)的代码要读它,才能把这次请求做对。

常见、合理的例子:

键(示例) 下游拿来干什么
tenant / tenant.id 选库、鉴权、配额
region / cell 路由到对应机房
experiment / feature_flag 同一请求整条链走同一套实验
client.version 兼容逻辑
priority 降级、限流档位

不该放:

  • 密码、Token、Cookie、身份证号(会传到所有下游,还可能进日志)
  • 已经能从 traceparent 得到的东西(trace_id 再塞一遍没有意义)
  • 只为了在 OpenObserve 里好看的字段(应写成 Span Attribute)
  • 又大又长的 JSON(每个出站请求都带,代理还可能截断)

头看起来像这样:

traceparent: 00-d333b8c15d39b13483cdb2a07c550203-87bedaf081d1c67b-01

tracestate: congo=t61rcWkgMzE

baggage: tenant=acme,region=cn-east,experiment=checkout-v2

订单服务 Extract 后可以 baggage.get("tenant") 去查对应租户库。若还想在 Trace 里看见 tenant,必须再 span.set_attribute("tenant", "acme")------Baggage 不会自动变成 OpenObserve 上的字段。

注意两点:

  1. 别放密钥、Token、身份证号。 Baggage 会传到所有下游,日志和中间件都可能打出来。
  2. 体积要小。 每个出站请求都会带上,太大既浪费带宽,也可能被代理截断。

Trace 回答"这次调用怎么走";Baggage 回答"这次调用随身带了哪些业务标签"。

Baggage 不是协议必选项;没有租户/实验/路由这类跨服务业务上下文,就可以不传。有了,再选上面那些「下游真会读」的键,而不是把整个用户对象塞进去。


**Profiles(性能剖析)**是"代码在忙什么"的采样快照,和 Trace / Metrics / Logs 并列,是遥测里的第四类信号。

它不回答这次请求怎么走(那是 Trace),也不回答每秒多少次(那是 QPS 这类 Metrics)。它回答:CPU / 内存花在哪些函数上。

它长什么样:

运行时按固定频率(例如每秒几百次)打断正在执行的线程,记下当时的调用栈:

main

└ handle_checkout

└ query_inventory

└ pg_query ← 采样时经常停在这里

把成千上万次这样的栈叠在一起,就得到一份 Profile:哪个函数出现次数多,就说明它占的 CPU(或分配的内存)多。常见形态是火焰图(Flame Graph):横轴是占比,纵轴是调用深度。

常见类型:

类型 看什么
CPU 时间花在哪些函数
Heap / 内存 谁在分配、泄漏嫌疑
Allocations 分配次数/大小
Goroutine / 线程 阻塞、等待

Go 的 pprof、Java 的 JFR、Linux 的 perf、eBPF 连续剖析,导出的都是这类数据。OpenTelemetry 也在把 Profiles 做成和 Traces 一样可上报的信号(OTLP Profiles)。

和另外三类怎么配合:

信号 粒度 典型问题
Metrics 整服务、一段时间 QPS 高了、CPU 90%
Logs 一条事件 报错写了什么
Traces 一次请求、跨服务 checkout 慢在订单服务
Profiles 进程内函数级 订单服务慢在 pg_query 还是 JSON 序列化

典型用法:Metrics 发现 CPU 高 → Trace 定位到某个 span 很慢 → Profile 指出是哪一个函数。有的实现还能把 Profile 和 trace_id / span 对齐,叫做Trace-associated profiling

和 Baggage / traceparent 无关:

Profiles 不是传给下一跳的 HTTP 头。它是本进程自己采的栈,经 OTLP 或 pprof 发到 OpenObserve 这类后端。链路传播仍是 traceparent + 可选 tracestate / baggage

Trace 告诉你慢在哪一跳,Profile 告诉你那一跳里慢在哪几行代码。


B. 规范(Specification)------大家必须遵守的契约

规范分三块,排障时对应不同问题:

规范块 管什么 遇到的典型问题
API 代码里怎么打点(start span、记 attribute) 根本没生成数据
SDK 怎么采样、批处理、重试、加 Resource 生成了但没发出去 / 被采样丢掉
Data 语义约定(字段名)+ OTLP(怎么编码传输) 发出去了但对不上字段 / 对端收不下

API 是「插座形状」,SDK 是「插头实现」,OTLP 是「电怎么从这根线送到对面」。


API 是打点口令,SDK 是落地实现,DATA 是网上真正跑的那份遥测。 这三层就是 OpenTelemetry 规范的骨架。

业务代码

│ 只调用 API(Tracer.start_span ...)

SDK(采样、批处理、Processor、Exporter)

│ 把内存里的 Span 编成约定格式

DATA ── OTLP JSON/protobuf ──► 接收端 / OpenObserve

字段名走语义约定(service.name、http.route ...)

跨服务的 traceparent 不在这三层里,它是另一条线:把 Context 身份复制到下一跳。三层管的是「本进程怎么产生、处理、交出去」。

(1)API:业务只该依赖这一层

API 规定怎么说话,不规定数据去哪。 Python 里就是 opentelemetry-apiTracerSpanContextset_attribute

例如 gateway 里这段是 API:

python 复制代码
with tracer.start_as_current_span("GET /checkout", kind=SpanKind.SERVER) as span:
    span.set_attribute("http.request.method", "GET")
    span.set_attribute("http.route", "/checkout")
    headers = {"Content-Type": "application/json"}
    PROPAGATOR.inject(headers)
    req = urllib.request.Request(
        "http://127.0.0.1:18081/orders",
        data=b"{}",
        headers=headers,
        method="POST",
    )

特点:

  • 没装 SDK 时,这些调用是 No-Op(空操作),线上零开销、不会报错。
  • 框架(Django、FastAPI、HTTP 客户端)只依赖 API,换导出后端不用改业务。
  • API 不采样、不组包、不联网。

类比:插座标准。设备只认插孔形状,不管后面接的是哪家电厂。

(2)SDK:把口令变成真的 Span

SDK 是 API 的实现:opentelemetry-sdk。例如 _provider() 整段都是 SDK:

python 复制代码
def _provider(service: str, sampler=ALWAYS_ON) -> TracerProvider:
    provider = TracerProvider(
        resource=Resource.create({"service.name": service, "lab.source": "official-sdk"}),
        sampler=sampler,
    )
    exporter = OTLPSpanExporter(endpoint=OTLP_ENDPOINT, timeout=5)
    provider.add_span_processor(SimpleSpanProcessor(exporter))

它负责四件业务不该自己写的事:

职责 你们实验里的对应
生成 ID、维护当前 Context start_as_current_span,子 span 自动挂父
采样 ALWAYS_ON / 步骤里的 ALWAYS_OFF
Resource(谁发出的) service.name=gateway
Processor + Exporter Span End 后立刻走 OTLP HTTP

没有 SDK,API 再怎么 start_span,接收端也收不到任何东西。

生产里还常加批处理(BatchSpanProcessor)、尾采样、多导出器。那都是换 SDK 配置,不是改 API。


API 是菜单,SDK 是后厨。 点菜时只看菜单;菜从哪炒、油多油少、怎么装盒送走,是后厨的事。

a. API:在代码里喊的那几句话

就是「开始记一笔」「给这笔加个标签」「结束」。

比如:开始记 GET /checkout,记下这是 GET、路径是 /checkout

不管后面有没有人真的记下来。没装后厨时,这几句话等于对空气说------不报错,也不产生数据。业务、框架只该跟菜单打交道,这样换观测后端不用改业务代码。

b. SDK:真去干活的那套程序

听到开始记,它才:编一个 ID、决定采不采、记开始 / 结束时间、请求一结束就打包,通过网线发给 OpenObserve。

没装 SDK,再怎么喊,接收端也是空的。采样开还是关、批量发还是立刻发、发到哪,都是调 SDK,不是改菜单上的那几句。

例如写 start_span 是在用 API;能把这次 checkout 真的变成 OpenObserve 里的一条链路,靠的是 SDK。


(3)DATA:字段叫什么 + 网上怎么运

规范把 语义约定OTLP合称 DATA:后端只认这份契约,不认 用的是 Python SDK 还是手写 JSON。

1. 语义约定(名字)

同一件事必须用同一个键,OpenObserve 才能按服务、按路由聚合:

  • service.name:谁
  • http.request.method / http.route:哪条 HTTP
  • db.system / db.operation:哪类数据库操作

例如 order 服务写的 db.system=postgresql 就是在遵守 DATA 的命名,不是随便起的属性名。

2. OTLP(运输)

包结构固定为 resourceSpans → scopeSpans → spans,编码可以是 protobuf 或 JSON。simulate.py 不经过 SDK,自己组的就是 DATA:

python 复制代码
payload = {
    "resourceSpans": [
        {
            "resource": {
                "attributes": [
                    {"key": "service.name", "value": {"stringValue": service}},
                ]
            },
            "scopeSpans": [
                {
                    "scope": {"name": "otel-lab.protocol", "version": "1.0.0"},
                    "spans": [span],
                }
            ],
        }
    ]
}

所以两条实验线能打进同一个 :4318 并拼成一棵树:接收端只解析 DATA,不管上游是 SDK 还是手写。

DATA 里一份 Span 至少要有:traceIdspanId、可选 parentSpanIdname、起止时间、status、attributes。Metrics / Logs / Profiles 各有自己的 DATA 形状,但分层关系一样。


4317 和 4318 是官方写进 OTLP 规范的默认端口,但不是世界上只能用这两个口,也不是像 80 那样由 IANA 独占。

OpenTelemetry 协议(OTLP)里写明:

端口 默认干什么
4317 OTLP/gRPC(二进制 protobuf,一条连接可发 traces/metrics/logs)
4318 OTLP/HTTP(POST 到 /v1/traces/v1/metrics 等)

Collector、各语言 SDK 不配 endpoint 时,通常就连 localhost:4317(gRPC)或 localhost:4318(HTTP)。如果走 HTTP、本机没开 gRPC,那么就使用4318。

需要分清三件事:

  1. 默认,不是唯一。 可以改成任意端口。OpenObserve 云上常见是 https://...:443/api/default/v1/traces,根本不是 4318。
  2. 不是这个端口天生就是 OTLP。 谁先监听谁用。在操作前要先检查 4318 空闲,就是怕被别的程序占了。
  3. URL 里不写端口时,不会自动变 4318。 普通 HTTP 仍是 80、HTTPS 仍是 443。SDK 只有在用 OTLP 默认 endpoint 时才会带上 4317/4318。

4317/4318 属于 DATA 怎么运出去 的约定。业务调的是 API,SDK 组包后往这个默认门口送;门口换了,只要路径还是 /v1/traces、格式还是 OTLP,协议本身不变。


(4)三层怎么叠在一次 checkout 上

GET /checkout

API start_span("GET /checkout") + set_attribute(...)

SDK 采样通过 → End → SimpleSpanProcessor → OTLPSpanExporter

DATA protobuf POST /v1/traces

resource.service.name = gateway

span.name = GET /checkout

span.http.route = /checkout

另:Inject 写出 traceparent(身份,不是 DATA 包体)

↓ HTTP

order Extract → 又一套 API/SDK → 再一份 DATA(service.name=order-service)

对应装的三个包:

opentelemetry-api API
opentelemetry-sdk SDK
opentelemetry-exporter-otlp-proto-http 把 SDK 内存对象编成 DATA(OTLP)

simulate.py 跳过前两层,直接写 DATA,用来证明:拼树只认 DATA + traceparent,不认某一家 SDK。


(5)和前面几个词的关系

  • 遥测数据 = DATA 交出去的那批 Metrics / Logs / Traces / Profiles
  • traceparent / Baggage = 传给下一跳服务的头,不是 OTLP 包
  • QPS = DATA 里 Metrics 的一种
  • Profiles = DATA 的第四种信号,不是 API 里多了一个函数那么简单,要另有 Profiling API/SDK

业务写 API,进程里跑 SDK,OpenObserve 只吃 DATA。换语言、换 SDK、甚至手写 JSON,只要 DATA 合规,后端看到的是同一类遥测。


C. 实现(Implementations)------规范的落地代码

  • 各语言 SDK(Java / Go / Python ...)
  • Collector(接收、处理、再导出)
  • Operator / Helm(K8s 里管 Collector 和自动插桩)
  • 各种插桩库(HTTP、DB、gRPC 自动打点)

这些是 OTel 项目产出的软件,仍然不是「Logs / Traces / Metrics 三个产品」。

D. 后端产品(Backends)------ 存和查

Jaeger、Prometheus、Zipkin、OpenObserve、各类商业 APM。

它们 消费 OTLP(或兼容格式) ,负责存储和界面。

OTel 不规定 必须用哪家后端;换产品通常不用改业务打点,只改 exporter 的地址。


0.3 每个信号的「四件套」(官方结构)

官方把每个信号拆成四层(见 Specification Status):

API → SDK → OTLP → Collector

打点 处理导出 传输 中转(可选但生产常用)

对照:

信号 API SDK OTLP Collector
Tracing Stable Stable Stable 同协议,Stable
Metrics Stable mixed Stable 同协议
Logging Bridge API Stable Stable Stable 同协议
Baggage Stable Stable 没有 没有
Profiles 演进中 演进中 Development 同协议

这里有两个容易错的点:

  1. Baggage 为什么没有 OTLP?

Baggage 不是给后端画图用的,它是 顺着请求往下游传的键值(走 baggage header 等),下游代码自己读。它不导出到 OpenObserve,所以没有 OTLP、也没有 Collector 管道。

  1. Collector 的稳定度和 OTLP 绑在一起

Collector 能稳定收某种信号,前提是这种信号的 OTLP 已经稳定。Profiles 的协议还是 Development,生产上不要当主力。


0.4 数据走完一整条路,OTLP 只负责其中一跳

排障时最有用的是这张生命周期,请能默画:

① 生成 代码 / 自动插桩调用 API,造出 Span / Metric / Log

② 处理 SDK:加 Resource、采样、批量、队列

③ 导出 SDK 用 OTLP 发给下一跳(本机 Collector 或直接后端)

④ 中转(可选) Collector 再处理,再 OTLP 发给后端

⑤ 存储 OpenObserve 等收下、建索引

⑥ 查询 在界面 / SQL 里搜 trace_id

OTLP 规范写得很明确:它只保证 ③ 或 ④ 里某一对 client↔server 之间 交割清楚 (成功、部分成功、或明确失败)。

它不保证从应用一直到你点开瀑布图端到端不丢。多一跳 Collector,就多一次独立的 Export 确认

所以:

  • 应用日志里已经有 trace_id,OpenObserve 没有这条 Trace
    → 不一定是产品坏了,可能停在 ② 采样、③ 导出失败、④ Collector 丢掉、⑤ 写失败、⑥ 查错 stream/时间。
  • 这正是要建立的习惯:先问停在第几格,再打开对应的那一层,而不是先猜界面。

0.5 用一个具体场景把对象钉死

假设:checkout 服务处理一笔下单,数据进 OpenObserve 的 test_otel

看到的东西 属于哪一类对象
瀑布图上的一条链路 信号:一条 Trace(许多 Span)
Span 上的 http.route 语义约定(规范 Data)
服务里的 Java Agent / SDK 实现
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 告诉 SDK 用 OTLP/HTTP 发到下一跳
Collector 的 otlp receiver + exporter 实现,中间又走一次 OTLP
OpenObserve 里 stream test_otel 后端产品 的存储单元
日志里的 trace=... 仍是 Logs 信号,用同一个 Context 里的 trace_id 对齐

没有「OTel Trace 产品」这种东西。Trace 是信号;OTLP 是运货协议;OpenObserve 是仓库。


0.6 常见混用

容易说错 更准确
「OTel 就是链路追踪」 OTel 管三类主力信号 + 传播;链路只是其中一种
「学 OTel 协议就是学 OpenObserve」 产品是查询层;协议是生成和运输层
「Logs、Trace、Metric 是 OTel 三个产品」 三种信号
「OTLP 保证数据一定到得了界面」 只保证这一跳 Export;后面每跳、采样、查询都可能让你「看不见」
「没数据就是 OTel 坏了」 先分:没生成 / 被采样 / 没导出 / 中转丢 / 后端拒 / 查错
「Baggage 也是一种监控」 它是跨服务传上下文,不进 OTLP,不能当指标用

  1. Logs、Traces、Metrics 是产品、协议,还是信号?
  2. OTLP 保证的是端到端不丢,还是一跳 client↔server?
  3. Baggage 为什么没有 OTLP?
  4. OpenObserve 在 A/B/C/D 里属于哪一类?
  5. 日志里有 trace_id,界面没有这条 Trace可能停在生命周期的哪几格?
相关推荐
泡沫冰@6 天前
OTEL的组件介绍
openobserve·otel
不懂的浪漫4 个月前
OpenTelemetry 和 SkyWalking Agent 怎么选?一次讲清 OTel、SkyWalking Agent 的相同点与区别
wpf·skywalking·链路追踪·opentelemetry·otel
不会飞的小龙人2 年前
Docker安装Quickwit搜索引擎
搜索引擎·docker·日志存储·链路跟踪·otel·portainer容器管理·quickwit
SRETalk2 年前
OpenTelemetry 101:面向 IT 领导者和爱好者的非技术指南
可观测性·opentelemetry·otel
不会飞的小龙人2 年前
遥测数据采集工具Grafana Alloy
spring boot·grafana·日志采集·链路跟踪·alloy·otel