库只认 API,SDK 由应用来装
一句话总结
OpenTelemetry 客户端按信号(signal)分成 API、SDK、语义约定(semantic conventions)和 Contrib 四类包。埋点代码只依赖 API,由应用所有者安装 SDK,组装出 provider、processor 和 exporter。本文依据规范概览和客户端设计原则,说明为什么需要这种分离(composability),SDK 管道是如何串起来的,以及无需修改代码的代理(agent)如何装入同样的 API 与 SDK。
为什么需要它
埋点正如文档所说,是“横切关注点(cross-cutting concern)”。观测代码会混进 Web 框架、数据库客户端、消息队列库里。然而,使用这些库的应用要把遥测发往哪个后端,或者根本不发送,库的作者无从得知。如果库拖入某个具体实现,连不想要它的应用也会变得臃肿。
设计原则文档因此提出三点要求:API 必须与实现明确分离;第三方库只能依赖 API;最终的应用开发者要能决定如何配置 SDK,甚至完全不使用。为此文档写明,API 与 SDK 必须作为独立的 artifact 提供(MUST)。
工作原理
API 的最小实现
API 包自身是完整的。没有 SDK,应用也必须能构建并运行,所以 API 里内置了最小实现(no-op)。文档强调这个最小实现的返回值必须有效:createSpan() 不能失败,必须返回非 null 的 Span;调用方不必关心此刻运行的是不是最小实现;并且性能开销应几乎为零。这就是满足“已埋点的库也能用在不使用 OpenTelemetry 的应用中”这一要求的方法,也让框架不必再分别发布“已埋点版”和“未埋点版”。
规则中针对埋点作者的一条很明确:“埋点作者不得直接引用任何种类的 SDK 包(MUST NOT),只能引用 API。”
SDK 内部:从 provider 到 exporter
安装 SDK 后,它会取代最小实现。SDK 又分为两部分:与协议无关的通用逻辑(批处理、附加进程信息等),以及与协议绑定的 exporter。exporter 只具备最基本的功能,便于厂商接入自己的协议。规范要求 SDK 提供的默认 exporter 有 OTLP(日志、指标、跟踪)、标准输出、内存(用于测试),指标还要加上 Prometheus,跟踪还要加上 Zipkin。厂商专用的 exporter 不放进客户端。
组装的骨架在每种信号中形状相同。
| 信号 | Provider | 创建的对象 | 记录单位 | 处理器 → exporter |
|---|---|---|---|---|
| 跟踪 | TracerProvider | Tracer | Span | SpanProcessor → SpanExporter |
| 指标 | MeterProvider | Meter → Instrument | Measurement | 聚合状态 → MetricReader/Exporter |
| 日志 | LoggerProvider | Logger | LogRecord | LogRecordProcessor → LogRecordExporter |
根据跟踪 SDK 规范,创建 TracerProvider 时要配置若干 SpanProcessor、IdGenerator、SpanLimits 和 Sampler。默认采样器是 ParentBased(root=AlwaysOn)。TracerProvider 的 Shutdown 会调用所有已注册处理器的 Shutdown,ForceFlush 也同样会传播。
处理器链
SpanProcessor 是跨度(span)生命周期的钩子:跨度开始时调用 OnStart,结束前一刻调用 OnEnding,结束后调用 OnEnd,此外还有 Shutdown 和 ForceFlush。它们按注册顺序调用,只有所有处理器的 OnEnding 都结束后,OnEnd 才会开始。内置处理器有两种。
- Simple processor:span 一结束就立刻交给 exporter。
- Batching processor:把结束的 span 收集到队列里,再打包发送。参数有
maxQueueSize(默认 2048,超出则丢弃 span)、scheduledDelayMillis(默认 5000)、exportTimeoutMillis(默认 30000)、maxExportBatchSize(默认 512,必须不大于maxQueueSize)。队列达到批次大小、延迟时间已过或调用ForceFlush时就会导出,exporter 的Export调用会被串行化,不会同时重叠。
Tracer ──► Span(끝) ──► [SpanProcessor 1] ──► [SpanProcessor 2: Batching] ──► SpanExporter(OTLP)
│ OnStart/OnEnding/OnEnd 훅
日志 SDK的结构也相同。在 LoggerProvider 中注册 LogRecordProcessor,由 Simple 或 Batching 处理器交给 LogRecordExporter(例如 OTLP)。规范说明,内置处理器负责“批处理和转换”。
代理(agent):不改代码,装入同样的东西
零代码埋点概念把 agent 的作用定义为“把 OpenTelemetry API 与 SDK 的能力添加到应用中”。具体方法因语言而异:字节码操作、猴子补丁(monkey patching)、eBPF。被埋点的是你所使用的库(请求与响应、数据库调用、消息队列),而不是你自己的代码;想给自己的代码埋点,需要基于代码的埋点。配置通过环境变量和各语言自己的方式完成,要启动只需要一个服务名称。
Java agent就是一个 opentelemetry-javaagent.jar。给 Java 8 及以上的 JVM 加上 -javaagent:path/to/opentelemetry-javaagent.jar,它会动态注入字节码,从许多库中获取遥测。配置可以通过 -Dotel.service.name=... 这样的系统属性、OTEL_SERVICE_NAME、OTEL_TRACES_EXPORTER 这样的环境变量、JAVA_TOOL_OPTIONS,或者用 otel.javaagent.configuration-file 指定的属性文件中的任意一种来完成。
Python 采用的是猴子补丁。用 pip install opentelemetry-distro opentelemetry-exporter-otlp 安装 API、SDK 和两个工具,opentelemetry-bootstrap -a install 会扫描 site-packages,为已安装的包选择并安装对应的埋点库(例如 flask 对应 opentelemetry-instrumentation-flask)。运行命令是 opentelemetry-instrument python myapp.py,通过 --traces_exporter console,otlp 这样的参数或 OTEL_* 环境变量来配置。文档明确指出,自动埋点要生效,必须安装 distro 包。
在现场相遇的样子
一个团队的内部 HTTP 客户端库直接依赖了 SDK。使用这个库的批处理任务明明没有地方可以发送遥测,OTLP exporter 却照样启动,退出时还要等待 Shutdown,导致结束得很晚。把库改为依赖 API 后,批处理任务中它变成 no-op,而在 Web 服务中则直接使用应用组装的 SDK。
另一个团队遇到 span 间歇性消失的问题。流量集中时,Batching processor 的队列(默认 2048)溢出,丢弃了 span。他们需要在调大队列,与调整 maxExportBatchSize、scheduledDelayMillis 以加快导出速度之间权衡。
接下来的文章要讲什么
后续文章将讲解七种指标 Instrument 与 temporality、日志记录的字段与桥接 API,以及 schema URL。之后的测验会确认 API 与 SDK 的分离原因、处理器链、Batching processor 的默认值,以及 agent 的工作方式。