目录
0.1 先记住这张图
0.2 四类对象分别是什么
A. 信号(Signals)——数据长什么样
B. 规范(Specification)——大家必须遵守的契约
C. 实现(Implementations)——规范的落地代码
D. 后端产品(Backends)—— 存和查
0.3 每个信号的「四件套」(官方结构)
0.4 数据走完一整条路,OTLP 只负责其中一跳
0.5 用一个具体场景把对象钉死
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 是数据种类(信号),不是产品,也不是三套协议
必看文档
- 编辑Instrumentation
- 编辑Collector
- 按你工作语言选一篇 Getting Started:
编辑Dev 入门 → 点进 Java / Go / Python / Node- OpenObserve:编辑Ingestion
- 中文长文(选读):编辑OpenTelemetry 2026 深度解析
操作
- 编辑LFS148 Getting Started with OpenTelemetry
约 10 小时,含自动/手动埋点、Collector lab,Linux Foundation 平台,免费。- B 站可搜「OpenTelemetry 入门」「OTel Collector」,当辅助
- 七米 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=acme、user.id=42、region=cn。
跨服务时通常走 HTTP 头:
traceparent: 00-<trace_id>-<span_id>-01
baggage: tenant=acme,user.id=42
traceparent负责把链路拼成同一棵树;baggage只是把这些键值原样传到下一跳。
和另外两个容易混的东西对比:
| 干什么 | 后端能不能直接看见 | |
|---|---|---|
Trace Context | 身份: | 能,用来拼树 |
Span Attribute | 记在某个 span 上,给观测用 | 能,在该 span 上 |
Baggage | 给下游代码读(路由、计费、采样决策) | 默认不能,除非业务显式写进 Attribute |
所以:网关往 Baggage 里塞了tenant=acme,订单服务能读到并按租户查库;OpenObserve 里却不一定有这个字段。要在 Trace 里看见,需要再span.set_attribute("tenant", ...)。
拼链路只靠traceparent(外加可选的tracestate)。Baggage 不是固定字段表,只传「下游代码真的会读」的业务标签。
传给下一跳的,一共就这几类:
| HTTP 头 | 要不要带 | 干什么 |
|---|---|---|
| 要拼成一棵树就必须带 | 身份:同一 |
| 可选 | 厂商私货,例如某 APM 的采样决策;W3C Trace Context 的第二部分 |
| 可选 | 业务键值,给下游代码用,不是给 OpenObserve 拼树用 |
这三样是服务 A → 服务 B 的传播(Propagator 写入 HTTP/gRPC metadata)。
不会随下一跳服务请求传过去的:
- Span 的
http.method、db.statement等 Attributeservice.name等 Resource- Span Event / Status
那些走的是另一跳:进程 → 接收端/OpenObserve(OTLP/v1/traces)。下游服务收不到,也不该靠它们来知道「这次请求是哪个租户」。
若环境不是 W3C,可能看到b3、uber-trace-id等替代身份头。双方必须同一套,否则 Extract 失败、下游自己成根。
如果代码用的是TraceContextTextMapPropagator(),即使 Context 里有 Baggage,出站请求也不会写出baggage头。SDK 默认常用组合是 Trace Context + Baggage 两个 Propagator。
Baggage 要传什么键值对?
规范不定死键名。只约定格式:逗号分隔的key=value,值要百分号编码。放什么由业务决定。
该放的原则就一条:下一跳(或再下一跳)的代码要读它,才能把这次请求做对。
常见、合理的例子:
| 键(示例) | 下游拿来干什么 |
|---|---|
| 选库、鉴权、配额 |
| 路由到对应机房 |
| 同一请求整条链走同一套实验 |
| 兼容逻辑 |
| 降级、限流档位 |
不该放:
- 密码、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 上的字段。
注意两点:
- 别放密钥、Token、身份证号。 Baggage 会传到所有下游,日志和中间件都可能打出来。
- 体积要小。 每个出站请求都会带上,太大既浪费带宽,也可能被代理截断。
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 | 进程内函数级 | 订单服务慢在 |
典型用法: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-api:Tracer、Span、Context、set_attribute。
例如 gateway 里这段是 API:
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:
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 |
|
采样 |
|
Resource(谁发出的) |
|
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:哪条 HTTPdb.system/db.operation:哪类数据库操作
例如 order 服务写的db.system=postgresql就是在遵守 DATA 的命名,不是随便起的属性名。
2. OTLP(运输)
包结构固定为resourceSpans → scopeSpans → spans,编码可以是 protobuf 或 JSON。simulate.py不经过 SDK,自己组的就是 DATA:
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 至少要有:traceId、spanId、可选parentSpanId、name、起止时间、status、attributes。Metrics / Logs / Profiles 各有自己的 DATA 形状,但分层关系一样。
4317 和 4318是官方写进 OTLP 规范的默认端口,但不是世界上只能用这两个口,也不是像 80 那样由 IANA 独占。
OpenTelemetry 协议(OTLP)里写明:
| 端口 | 默认干什么 |
|---|---|
4317 | OTLP/gRPC(二进制 protobuf,一条连接可发 traces/metrics/logs) |
4318 | OTLP/HTTP(POST 到 |
Collector、各语言 SDK 不配 endpoint 时,通常就连localhost:4317(gRPC)或localhost:4318(HTTP)。如果走 HTTP、本机没开 gRPC,那么就使用4318。
需要分清三件事:
- 默认,不是唯一。 可以改成任意端口。OpenObserve 云上常见是
https://…:443/api/default/v1/traces,根本不是 4318。- 不是这个端口天生就是 OTLP。 谁先监听谁用。在操作前要先检查 4318 空闲,就是怕被别的程序占了。
- 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)
对应装的三个包:
| 包 | 层 |
|---|---|
| API |
| SDK |
| 把 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 不是给后端画图用的,它是 顺着请求往下游传的键值(走baggageheader 等),下游代码自己读。它不导出到 OpenObserve,所以没有 OTLP、也没有 Collector 管道。
2. 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 上的 | 语义约定(规范 Data) |
服务里的 Java Agent / SDK | 实现 |
| 告诉 SDK 用 OTLP/HTTP 发到下一跳 |
Collector 的 | 实现,中间又走一次 OTLP |
OpenObserve 里 stream | 后端产品 的存储单元 |
日志里的 | 仍是 Logs 信号,用同一个 Context 里的 |
没有「OTel Trace 产品」这种东西。Trace 是信号;OTLP 是运货协议;OpenObserve 是仓库。
0.6 常见混用
| 容易说错 | 更准确 |
|---|---|
「OTel 就是链路追踪」 | OTel 管三类主力信号 + 传播;链路只是其中一种 |
「学 OTel 协议就是学 OpenObserve」 | 产品是查询层;协议是生成和运输层 |
「Logs、Trace、Metric 是 OTel 三个产品」 | 三种信号 |
「OTLP 保证数据一定到得了界面」 | 只保证这一跳 Export;后面每跳、采样、查询都可能让你「看不见」 |
「没数据就是 OTel 坏了」 | 先分:没生成 / 被采样 / 没导出 / 中转丢 / 后端拒 / 查错 |
「Baggage 也是一种监控」 | 它是跨服务传上下文,不进 OTLP,不能当指标用 |
- Logs、Traces、Metrics 是产品、协议,还是信号?
- OTLP 保证的是端到端不丢,还是一跳 client↔server?
- Baggage 为什么没有 OTLP?
- OpenObserve 在 A/B/C/D 里属于哪一类?
- 日志里有 trace_id,界面没有这条 Trace可能停在生命周期的哪几格?