OpenTelemetry(简称 OTel)是 CNCF 的可观测性标准,统一了 Traces、Metrics、Logs 等遥测数据的采集、处理和导出方式。它不是一个新的监控后端,而是一套 API、SDK、协议和 Collector 生态,让应用只埋点一次,就能把数据送到任意兼容的后端。
本文基于 OpenTelemetry 官方最新文档整理,先讲清概念和架构,再以 Java 为例,完整说明 SDK 初始化、Tracing、Metrics、Logs、Context、Sampling、Collector、Spring Boot 集成和生产实践。
版本说明:文中 Java SDK 示例以 OpenTelemetry Java 1.67.x 为主线,Java Agent/Instrumentation 2.x 为例。实际项目请以 Maven Central 和官方文档的最新稳定版为准。
- OpenTelemetry 是什么
- 核心概念
- 架构与数据流
- Java 生态与依赖
- 快速开始:Java Agent
- 手动初始化 SDK
- Tracing 实战
- Metrics 实战
- Logs 与日志关联
- Context 与 Baggage
- Sampling
- Resource 与 Semantic Conventions
- Collector
- Spring Boot 实战
- 生产实践
- 测试
- 常见坑
- 一次请求的完整链路
- 参考资料
OpenTelemetry 是什么
定义
OpenTelemetry 是一套开源的、厂商中立的可观测性框架,提供:
- 统一的 API:应用和库只依赖 API,不绑定具体厂商或后端。
- 多语言 SDK:负责采样、批处理、资源、导出等实现。
- 自动埋点:Java Agent、Spring Boot Starter、各类库的 instrumentation。
- OTLP 协议:统一的遥测数据传输协议(gRPC/HTTP + Protobuf)。
- Collector:接收、处理、路由、导出遥测数据的代理/网关。
它解决的核心问题是:应用埋点与后端解耦。今天用 Jaeger,明天换 Tempo;今天只看 Trace,明天加 Metrics/Logs,都不需要重写业务埋点。
三大信号
| 信号 | 回答的问题 | 典型数据 |
|---|---|---|
| Traces | 一次请求经过了哪些服务、每一步耗时多少 | Span、TraceID、SpanID、父子关系、事件、状态 |
| Metrics | 系统整体趋势如何、是否超过阈值 | Counter、Gauge、Histogram、属性 |
| Logs | 某个时刻具体发生了什么 | 时间戳、级别、消息、属性、Trace 关联 |
| Baggage | 跨服务传递业务上下文 | 键值对,随请求传播 |
| Profiles | 性能瓶颈在哪个函数/栈 | 采样剖析数据(仍在演进) |
OpenTelemetry 不是什么
- 不是监控后端:不存储、不展示、不告警;这些由 Jaeger、Tempo、Prometheus、Loki、Grafana、商业 APM 等负责。
- 不是日志框架:不替代 SLF4J/Logback/Log4j,而是把日志桥接到 OTel。
- 不是只做 Trace:Metrics 和 Logs 也是一等公民。
- 不是零成本:采样、属性基数、导出批量、Collector 资源都需要设计。
与 Prometheus、Grafana、Jaeger 的关系
应用(OTel SDK/Agent) |
- Prometheus 是指标存储/查询系统;OTel 可以通过 Collector 或 exporter 把指标送到 Prometheus。
- Grafana 是可视化层;可以查询 Prometheus、Tempo、Loki 等。
- Jaeger/Tempo 是追踪后端;接收 OTLP 或 Jaeger 格式。
- OTel 负责“产生和传输”,后端负责“存储和展示”。
核心概念
Signals
OpenTelemetry 用 Signal 表示一类遥测数据:
- Traces:分布式追踪。
- Metrics:指标。
- Logs:日志。
- Baggage:跨服务上下文,不是独立的存储信号。
- Profiles:性能剖析,仍在发展阶段。
API、SDK、Instrumentation
| 层 | 作用 | 谁使用 |
|---|---|---|
| API | 定义 Tracer、Meter、Logger、Span、Context 等接口 | 业务代码、库作者 |
| SDK | API 的具体实现:采样、处理、导出、资源 | 应用启动时配置 |
| Instrumentation | 自动/手动埋点 | Agent、Starter、库、业务代码 |
| Collector | 接收、处理、导出遥测数据 | 运维/平台团队 |
关键原则:
- 库和框架只依赖 API,不要打包 SDK。
- 应用负责初始化 SDK,并且只初始化一次。
- Agent 和手动 SDK 不要重复初始化。
- 只引入
opentelemetry-api而没有 SDK/Agent 时,API 是 no-op,不会产生任何数据。
Resource
Resource 描述“遥测数据来自哪里”,是所有信号共享的元数据:
service.name = order-service |
Resource 应该统一配置到 TracerProvider、MeterProvider、LoggerProvider。
Context
Context 是不可变的键值容器,用来在函数调用和线程之间隐式传递:
- 当前 Span 存在 Context 中,新建 Span 默认以当前 Span 为父。
- Baggage 存在 Context 中,随请求传播。
- Metrics 的 exemplar、Logs 的 trace 关联都依赖 Context。
Semantic Conventions
Semantic Conventions(语义约定)是 OTel 定义的标准属性名,例如:
| 场景 | 属性 |
|---|---|
| 服务 | service.name、service.version、service.instance.id |
| 环境 | deployment.environment.name |
| HTTP 服务端 | http.request.method、url.path、http.response.status_code、server.address |
| HTTP 客户端 | http.request.method、server.address、server.port |
| 数据库 | db.system.name、db.namespace、db.operation.name |
| 消息 | messaging.system、messaging.destination.name、messaging.operation |
| 异常 | exception.type、exception.message、exception.stacktrace |
使用语义约定可以让 Dashboard、告警和后端自动识别数据,不要随意发明同名属性。
OTLP
OTLP(OpenTelemetry Protocol)是 OTel 的标准传输协议:
- gRPC:默认端口
4317。 - HTTP/Protobuf:默认端口
4318,路径为/v1/traces、/v1/metrics、/v1/logs。 - 数据使用 Protobuf 编码,支持 Traces、Metrics、Logs。
- Collector 的
otlpreceiver 同时支持 gRPC 和 HTTP。
Java Agent 2.x 和 Spring Boot Starter 2.x 默认使用 http/protobuf,不是 gRPC。连接 4317 时要显式设置 grpc。
架构与数据流
典型链路
┌─────────────────────────────────────────────────────┐ |
三种部署形态
| 形态 | 说明 | 适用 |
|---|---|---|
| 无 Collector | 应用直接导出到后端 | 测试、小规模 |
| Agent 模式 | 每台主机/每个 Pod 一个 Collector | 边缘采集、缓冲、批处理 |
| Gateway 模式 | 中心 Collector 集群 | 多集群、统一处理、tail sampling |
数据流的关键点
- 应用只负责产生数据,不关心后端地址和协议细节。
- Collector 负责批处理、重试、限流、采样、脱敏、路由。
- 后端负责存储、查询、可视化和告警。
- Trace 的 Context 必须跨 HTTP、RPC、消息队列传播,否则链路会断。
Java 生态与依赖
依赖分层
| 依赖 | 用途 | 是否应该进业务代码 |
|---|---|---|
opentelemetry-api |
API 接口 | 是 |
opentelemetry-sdk |
SDK 实现 | 应用启动模块 |
opentelemetry-exporter-otlp |
OTLP 导出 | 应用启动模块 |
opentelemetry-sdk-extension-autoconfigure |
环境变量/配置自动装配 | 可选 |
opentelemetry-instrumentation-annotations |
@WithSpan、@SpanAttribute |
可选 |
opentelemetry-spring-boot-starter |
Spring Boot 自动埋点 | Spring Boot 应用 |
opentelemetry-javaagent |
零代码 Agent | 部署时挂载 |
opentelemetry-sdk-testing |
单元测试 | 测试作用域 |
Maven BOM
<properties> |
opentelemetry-bom管理 SDK/API/Exporter 版本;opentelemetry-instrumentation-bom管理 instrumentation 版本。不要混用不兼容的版本。
依赖示例
<dependencies> |
选择哪种埋点方式
| 场景 | 推荐 |
|---|---|
| 已有 Spring Boot 服务,想快速接入 | Java Agent(零代码) |
不想加 -javaagent,希望依赖化 |
Spring Boot Starter |
| 需要完全控制 SDK、采样、导出 | 手动初始化 SDK |
| 开发公共库 | 只依赖 opentelemetry-api |
| 需要业务级 Span | 手动埋点 + Agent/Starter 自动埋点 |
| 测试断言 | opentelemetry-sdk-testing |
不要同时使用 Java Agent 和 Spring Boot Starter,否则可能重复埋点。
快速开始:Java Agent
下载 Agent
从 OpenTelemetry Java Instrumentation 的 GitHub Releases 下载:
curl -L -o opentelemetry-javaagent.jar \ |
校验 checksum,并固定版本,不要在生产使用 latest。
启动应用
java -javaagent:/opt/otel/opentelemetry-javaagent.jar \ |
环境变量方式更适合容器:
export OTEL_SERVICE_NAME=order-service |
常用环境变量:
| 变量 | 示例 | 说明 |
|---|---|---|
OTEL_SERVICE_NAME |
order-service |
服务名,写入 service.name |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://collector:4317 |
OTLP 地址 |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc / http/protobuf |
4317 用 grpc,4318 用 http/protobuf |
OTEL_EXPORTER_OTLP_HEADERS |
api-key=xxx |
认证 Header |
OTEL_TRACES_SAMPLER |
parentbased_traceidratio |
采样器 |
OTEL_TRACES_SAMPLER_ARG |
0.1 |
采样比例 |
OTEL_RESOURCE_ATTRIBUTES |
service.version=1.2.0,deployment.environment.name=prod |
资源属性 |
OTEL_PROPAGATORS |
tracecontext,baggage |
传播器 |
OTEL_TRACES_EXPORTER |
otlp / none |
Trace 导出 |
OTEL_METRICS_EXPORTER |
otlp / prometheus / none |
指标导出 |
OTEL_LOGS_EXPORTER |
otlp / none |
日志导出 |
OTEL_SDK_DISABLED |
true |
临时关闭 SDK |
OTEL_JAVAAGENT_DEBUG |
true |
Agent 调试日志 |
Java Agent 2.x 默认协议是 http/protobuf;如果你的 Collector 暴露的是 4317 gRPC 端口,必须显式设置 OTEL_EXPORTER_OTLP_PROTOCOL=grpc。
Docker
FROM eclipse-temurin:21-jre |
Kubernetes
env: |
验证
# 查看 Agent 日志(默认输出到 stderr) |
常见问题:
- Agent 没生效:检查
-javaagent路径、JVM 参数是否被覆盖。 - 没有数据:检查 endpoint、协议、网络、TLS、Collector 日志。
- 4317 用了
http/protobuf:先确认协议,4317 通常配grpc,4318 配http/protobuf。 - Agent 日志太多:设置
OTEL_JAVAAGENT_LOGGING=simple或none。
手动初始化 SDK
完整初始化示例
package com.example.otel; |
关键点:
- SDK 只初始化一次,不要每个请求新建。
Resource要同时设置到三个 Provider。BatchSpanProcessor和PeriodicMetricReader是异步批量导出,避免阻塞业务。- 注册 shutdown hook,确保退出前 flush。
- 生产环境设置队列、超时、批量大小,避免 OOM 或丢数据。
使用自动配置
如果希望用环境变量/系统属性配置 SDK,可以用:
import io.opentelemetry.api.OpenTelemetry; |
它读取 OTEL_* 配置,适合不想手写 SDK 初始化、又不想挂 Agent 的场景。
GlobalOpenTelemetry
库或框架通常通过全局实例获取:
import io.opentelemetry.api.GlobalOpenTelemetry; |
- Java Agent 会自动设置
GlobalOpenTelemetry。 - 手动初始化时,如果库依赖全局实例,需要用
GlobalOpenTelemetry.set(sdk)。 - 不要重复
set,否则会告警或覆盖。
Tracing 实战
获取 Tracer
import io.opentelemetry.api.trace.Tracer; |
getTracer(scopeName, scopeVersion) 的 scope 用于标识埋点来源,便于后端按库/模块过滤。
创建 Span 的标准写法
import io.opentelemetry.api.common.AttributeKey; |
要点:
startSpan()之后必须end(),放在finally。makeCurrent()让子 Span 自动挂到当前 Span 下。- 异常要
recordException+setStatus(ERROR)。 - 成功可以设置
StatusCode.OK,但不是必须。 - 不要在
finally里重复记录异常。
SpanKind
| 类型 | 含义 | 示例 |
|---|---|---|
SERVER |
服务端接收请求 | HTTP 入口、gRPC 服务端 |
CLIENT |
客户端发起请求 | HTTP 客户端、数据库客户端 |
PRODUCER |
消息生产者 | Kafka producer |
CONSUMER |
消息消费者 | Kafka consumer |
INTERNAL |
内部操作 | 业务方法、缓存、计算 |
SpanKind 影响后端拓扑图和指标聚合,尽量使用语义正确的类型。
嵌套 Span
Span parent = tracer.spanBuilder("order.create").startSpan(); |
如果忘记 makeCurrent(),子 Span 不会自动挂到父 Span 下,需要显式 setParent(Context)。
事件
Span Event 适合记录“某个时间点发生了什么”:
span.addEvent("cache.miss", Attributes.of( |
不要把大量日志或大对象塞进 Event,事件会随 Span 一起存储。
Link
Link 用于关联“不是父子关系”的 Span,例如批处理、异步消费:
Context producerContext = ...; // 从消息头 extract 得到的 Context |
手动注入与提取
跨进程调用需要传播 Trace Context:
import io.opentelemetry.context.Context; |
服务端提取:
import io.opentelemetry.context.propagation.TextMapGetter; |
使用 Agent/Starter 时,HTTP 服务端和客户端通常会自动注入/提取;手动埋点主要用于自定义协议或未被自动埋点的客户端。
异步与线程池
Context 不会自动跨线程传播,必须显式包装:
Context context = Context.current(); |
常见错误:
- 直接
executor.submit(() -> ...),Context 丢失,子 Span 变成新的根。 - 在线程池中复用 Span 对象,导致生命周期混乱。
- 在异步回调中忘记
makeCurrent()。
一次请求的 Span 结构
HTTP POST /orders SERVER (Agent/Starter 自动) |
原则:
- 自动埋点负责框架边界(HTTP、DB、消息)。
- 手动埋点负责业务语义(订单创建、库存锁定、风控决策)。
- 不要给每个 getter/setter 都加 Span,Span 太细会增加开销和噪音。
Metrics 实战
获取 Meter
import io.opentelemetry.api.metrics.Meter; |
Counter
import io.opentelemetry.api.common.Attributes; |
Counter 只增不减,适合请求数、错误数、处理条数。
Histogram
import io.opentelemetry.api.metrics.DoubleHistogram; |
Histogram 适合延迟、响应大小等分布数据。不要用平均值代替分位数。
Gauge
meter.gaugeBuilder("queue.depth") |
异步 Gauge 在回调中读取当前值,适合队列长度、连接数等。
UpDownCounter
LongUpDownCounter inFlight = meter.upDownCounterBuilder("orders.inflight").build(); |
适合可增可减的当前值,如正在处理的请求数。
属性与基数
Metrics 的每个属性组合都会生成一条时间序列:
ordersCreated.add(1, Attributes.of( |
高基数属性会导致内存和存储爆炸:
- 不要用
user.id、order.id、request.id做属性。 - 不要用完整 URL、原始 IP、错误堆栈做属性。
- 使用有限的枚举值,如
channel=web|app|api。 - 用 View 丢弃不需要的指标或限制基数。
SDK 默认对每个 instrument 的每个 collection cycle 限制约 2000 个唯一属性组合,可以通过 CardinalityLimitSelector 或 View 调整。
Exemplars
Exemplar 把指标测量关联到具体 Trace:
- Histogram 记录延迟时,可以附带当前 TraceID。
- 后端可以从慢指标直接跳到对应 Trace。
- 需要后端和 exporter 支持。
业务代码通常不需要手动创建 exemplar,SDK 会从当前 Context 自动关联;确保记录指标时 Context 中有 Span。
Logs 与日志关联
两种日志接入方式
| 方式 | 说明 | 推荐 |
|---|---|---|
| Log Bridge API | 用 OTel Logger 直接写日志 |
适合写 appender/桥接,不推荐业务直接用 |
| Log Appender | 把 Logback/Log4j 日志桥接到 OTel | 应用最常用 |
| Agent 注入 | Agent 把 trace_id/span_id 注入日志 MDC | 与现有日志框架配合 |
官方明确:LoggerProvider / Logger 是 Log Bridge API,主要给日志框架的 appender 使用,不是用来替代 SLF4J/Logback/Log4j。
Logback Appender
<dependency> |
在 logback.xml 中配置 OpenTelemetryAppender:
<configuration> |
应用启动时把 SDK 安装到 appender:
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; |
关键设置:
| 设置 | 作用 |
|---|---|
captureExperimentalAttributes |
捕获 thread.name、thread.id |
captureCodeAttributes |
捕获代码位置,可能有性能开销 |
captureArguments |
捕获日志参数 |
captureTemplate |
捕获消息模板 |
mdcAttributesIncluded / mdcAttributesExcluded |
控制哪些 MDC 字段进入日志属性 |
structuredAttributesIncluded / structuredAttributesExcluded |
控制 SLF4J KeyValuePair/Logstash 结构化属性 |
numLogsCapturedBeforeOtelInstall |
SDK 安装前缓存并回放的日志条数,默认 1000 |
注意:
- MDC 默认不会全部捕获,需要显式配置 include/exclude,避免把敏感字段带出去。
OpenTelemetryAppender.install(sdk)要在 SDK 初始化后尽早调用。- 日志量大时,建议同时配置 Collector 过滤/采样,不要在应用里全部同步导出。
- 使用 Agent 时,Agent 也可能注入 trace 上下文;避免和自定义 appender 重复产生同一份日志。
日志关联 Trace
理想情况下,每条日志都带:
trace_id=4bf92f3577b34da6a3ce929d0e0e4736 |
这样在日志平台点击 trace_id 就能跳到 Trace。
如果使用 Agent + Logback,Agent 会把 trace 上下文注入 MDC;日志格式中加:
<pattern>%d{ISO8601} %-5level [%X{trace_id:-},%X{span_id:-}] %logger{36} - %msg%n</pattern> |
日志最佳实践
- 使用结构化日志,关键字段放 MDC/KeyValuePair。
- 不要把整个对象序列化进日志。
- 不要在日志中记录密码、token、身份证、银行卡等敏感信息。
- 采样日志时保留 ERROR/WARN,避免把故障日志采掉。
- 日志量大的服务优先在 Collector 做过滤/采样,而不是全部上报。
Context 与 Baggage
Context 的作用
Context 是 OTel 的“隐式传递容器”:
- 当前 Span 放在 Context 中。
- Baggage 放在 Context 中。
- Metrics exemplar、Logs trace 关联都从 Context 读取。
Scope
Span span = tracer.spanBuilder("operation").startSpan(); |
makeCurrent() 返回 Scope,必须在 finally 中关闭,否则 Context 会泄漏到后续代码。
Baggage
Baggage 用于跨服务传递业务上下文:
import io.opentelemetry.api.baggage.Baggage; |
读取:
String tenantId = Baggage.current().getEntryValue("tenant.id"); |
注意事项:
- Baggage 会随请求传播到所有下游,可能被第三方看到。
- 不要放密码、token、身份证等敏感信息。
- Baggage 不宜过大,避免 HTTP Header 膨胀。
- Baggage 不是 Span Attribute,不会自动出现在 Span 上;需要显式读取并
setAttribute。
跨线程传递
Context context = Context.current(); |
Context.wrap 支持 Runnable、Callable、ExecutorService、Consumer、Supplier、Function 等。
跨进程传播
默认使用:
- W3C Trace Context:
traceparent、tracestate - W3C Baggage:
baggage
配置:
OTEL_PROPAGATORS=tracecontext,baggage |
如果对接旧系统,可加 b3、jaeger 等,但不要同时启用过多导致 Header 冲突。
Sampling
为什么采样
Trace 数据量可能非常大,全量采集成本高。采样在“保留可观测性”和“控制成本”之间做取舍。
Head Sampling
在 Trace 开始时决定是否采样:
- 基于 TraceID 的概率采样。
- 决策会通过
traceparent的 sampled flag 传给下游。 - 保证整条 Trace 要么全采、要么全不采。
- 实现简单、开销低。
Java SDK 配置:
.setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(0.1))) |
环境变量:
OTEL_TRACES_SAMPLER=parentbased_traceidratio |
parentBased 表示:
- 有父 Span 时跟随父采样决定。
- 根 Span 按 10% 比例采样。
Tail Sampling
在 Trace 结束后,根据整条 Trace 的信息决定是否保留:
- 保留所有错误 Trace。
- 保留慢 Trace。
- 按服务/属性设置不同采样率。
- 需要 Collector 的
tail_samplingprocessor。 - 需要等待
decision_wait,并保证同一 Trace 的所有 Span 到达同一 Collector 实例。
适合大规模系统,但实现和运维复杂度更高。
Head + Tail 组合
常见策略:
- 应用侧 Head Sampling 采 10%-30%,降低传输量。
- Collector 侧 Tail Sampling 保留全部错误和慢请求。
- 后端按保留策略存储。
注意:Head Sampling 已经丢弃的 Trace,Tail Sampling 无法恢复。
采样不会减少 Metrics/Logs
采样只作用于 Traces。Metrics 和 Logs 需要单独做基数治理、过滤和采样。
Resource 与 Semantic Conventions
Resource 配置
Resource resource = Resource.getDefault().toBuilder() |
环境变量:
OTEL_SERVICE_NAME=order-service |
OTEL_SERVICE_NAME 会覆盖 service.name。
语义约定
优先使用官方属性名:
import static io.opentelemetry.semconv.SemanticAttributes.*; |
不同 semconv 版本的常量类/包名可能变化;以项目引入的
opentelemetry-semconv版本为准。
Span 命名
Span 名称要低基数、可聚合:
| 推荐 | 不推荐 |
|---|---|
GET /orders/{id} |
GET /orders/123456 |
order.create |
createOrder-123456 |
inventory.reserve |
reserve inventory for user 42 |
kafka.consume order.created |
consume message 8f3a... |
高基数 Span 名称会让后端无法聚合,直接拖垮追踪系统。
属性基数与 PII
- 属性值应是有限枚举或低基数标识。
- 不要把用户 ID、订单 ID、请求 ID 作为 Metric 属性。
- Span Attribute 可以放少量高基数 ID,但要注意存储成本和隐私。
- 不要记录密码、token、身份证、银行卡、手机号等敏感信息。
- 需要排障的 ID 可以放日志,并做脱敏。
Collector
Collector 的作用
Collector 是遥测数据的代理/网关:
- 接收 OTLP/gRPC/HTTP、Jaeger、Zipkin、Prometheus 等数据。
- 批处理、重试、限流、内存保护。
- 属性处理、过滤、脱敏、采样。
- 路由到不同后端。
- 统一 TLS、认证、多租户。
最小配置
receivers: |
常用 Processor
| Processor | 作用 |
|---|---|
memory_limiter |
防止 Collector OOM,必须放 pipeline 第一位 |
batch |
批量发送,降低网络开销 |
attributes |
增加/删除/重命名属性 |
filter |
过滤不需要的 Span/Metric/Log |
tail_sampling |
按 Trace 结果采样 |
resource |
补充资源属性 |
transform |
使用 OTTL 转换数据 |
Tail Sampling 注意事项
Tail Sampling 需要看到整条 Trace:
- 同一 Trace 的所有 Span 必须路由到同一 Collector 实例。
- 多实例部署时使用
loadbalancingexporter,按 TraceID 路由。 decision_wait太短会误判,太长会增加内存。- 错误和慢请求优先保留,普通请求按比例采样。
示例:
processors: |
这个策略表示:保留所有错误 Trace、所有超过 500ms 的 Trace,其余按 10% 采样。
Agent 模式与 Gateway 模式
| 模式 | 优点 | 缺点 |
|---|---|---|
| Agent(DaemonSet/Sidecar) | 就近采集、低延迟、缓冲 | 资源分散、配置管理复杂 |
| Gateway(中心集群) | 统一处理、集中采样、易扩展 | 网络跳数增加、单点风险 |
| 混合 | Agent 采集 + Gateway 处理 | 架构复杂,但生产常用 |
Spring Boot 实战
方式一:Java Agent
最简单,零代码:
java -javaagent:/opt/otel/opentelemetry-javaagent.jar \ |
Agent 会自动埋点:
- Spring MVC / WebFlux 入口
- RestTemplate / WebClient / OkHttp / Apache HttpClient
- JDBC / HikariCP / MyBatis
- Redis / Kafka / RabbitMQ
- 日志 trace 上下文注入
- 常见的线程池/异步框架
方式二:Spring Boot Starter
<dependency> |
配置(等价环境变量):
otel: |
采样比例建议用标准环境变量 OTEL_TRACES_SAMPLER_ARG=0.1,避免不同版本 Starter 的属性绑定差异。
不要同时使用 Agent 和 Starter,否则可能重复埋点。
业务 Span
import io.opentelemetry.instrumentation.annotations.SpanAttribute; |
注意:
@WithSpan依赖 AOP/代理,自调用(同一个类内部调用)不会生效。- 注解方式适合业务方法;复杂逻辑仍建议手动
Span以便控制属性和异常。 @SpanAttribute的值会作为 Span 属性,避免放高基数或敏感数据。
WebClient / RestTemplate
Agent/Starter 会自动注入 Trace Context。若使用自定义 HTTP 客户端,需要手动注入:
openTelemetry.getPropagators() |
Kafka
- Producer:自动创建
PRODUCERSpan,并把 Trace Context 写入消息 Header。 - Consumer:自动提取 Header,创建
CONSUMERSpan。 - 批量消费时,一条消息可能对应多个上游 Trace,使用 Span Link 关联。
异步任务
Spring 的 @Async 线程池通常会被 Agent/Starter 包装;自定义线程池需要手动 Context.wrap:
executor.submit(Context.current().wrap(() -> { |
Actuator 与健康检查
- 不要给健康检查、Prometheus 抓取等高频低价值请求创建过多 Span。
- 可以用 Agent 的 suppression 配置或 Collector 过滤规则排除。
- 确保
/actuator/health不被采样成噪音。
生产实践
性能
- 使用批量处理器,不要用
SimpleSpanProcessor在生产同步导出。 - 设置合理的队列大小、批量大小、导出超时。
- 导出失败要能降级,不能阻塞业务线程。
- 避免在热路径创建大量属性对象,能复用就复用。
- 控制 Span 数量,不要把每个方法都包一层。
采样与成本
- 全量 Trace 成本高,按流量设置 head sampling。
- 错误和慢请求用 tail sampling 保留。
- Metrics 关注基数和保留时间。
- Logs 关注量和敏感信息。
- 定期评估“采样后是否还能定位问题”。
安全
- OTLP 使用 TLS,生产不要明文传输。
- 使用 API Key/mTLS 保护 Collector。
- 不要把 Collector 暴露到公网。
- 不要在属性/日志中记录密码、token、身份证、银行卡等。
- 对 PII 做脱敏或哈希。
- 限制 Collector 的接收大小和速率,防止滥用。
版本与升级
- 使用 BOM 统一 SDK/Exporter 版本。
- Agent、Instrumentation、Collector 分开升级,先测试。
- 关注 Semantic Conventions 的破坏性变更(如属性重命名)。
- 升级后检查 Dashboard、告警、采样配置是否仍然有效。
- 固定 Agent 版本,不要用
latest。
可观测性管道自身
监控 Collector:
- 接收/导出速率
- 队列长度、丢弃数
- 导出失败、重试
- 内存使用、CPU
otelcol_*指标
监控应用侧:
- Span 导出失败
- 队列满
- Agent 日志错误
- 采样率是否符合预期
排错清单
| 现象 | 排查 |
|---|---|
| 没有 Trace | Agent 是否挂载、endpoint/协议、Collector 日志 |
| 只有部分服务有 Trace | Context 传播是否断开、Header 是否被网关丢弃 |
| 子 Span 不关联 | 是否 makeCurrent()、异步是否 Context.wrap |
| 4317 连不上 | 协议用 grpc,端口 4317;HTTP 用 4318 |
| TLS 失败 | 证书、CA、SNI、Collector TLS 配置 |
| 数据量过大 | 采样、属性基数、日志量、Collector 限流 |
| 日志没有 trace_id | appender/MDC 配置、Agent 是否注入 |
| 指标缺失 | MeterProvider、MetricReader、导出间隔、后端 |
| 应用启动变慢 | Agent 版本、类加载、排除不需要的 instrumentation |
测试
使用 OpenTelemetryExtension
import io.opentelemetry.api.GlobalOpenTelemetry; |
OpenTelemetryExtension 会自动:
- 创建带内存 exporter 的 SDK。
- 注册
GlobalOpenTelemetry。 - 每个测试前清空已捕获数据。
使用 InMemorySpanExporter
InMemorySpanExporter exporter = InMemorySpanExporter.create(); |
测试重点:
- Span 名称、父子关系、SpanKind。
- 关键属性是否存在、值是否正确。
- 异常是否记录、状态是否为 ERROR。
- 异步代码是否保留了 Context。
- 日志是否带 trace_id/span_id。
常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
忘记 span.end() |
Span 不完整、内存泄漏 | finally 中 end() |
忘记 makeCurrent() |
子 Span 不关联 | try (Scope ...) |
| 线程池丢失 Context | 链路断裂 | Context.current().wrap(...) |
| 每个请求新建 SDK | 资源泄漏、重复导出 | 全局初始化一次 |
| Agent + Starter 同时使用 | 重复 Span/指标 | 二选一 |
| 高基数属性 | 内存/存储爆炸 | 用有限枚举、View、采样 |
| 把用户 ID 放进 Metric | 基数失控 | 放日志或 Span,且脱敏 |
| Span 名称含 ID | 后端无法聚合 | 低基数名称 |
| 同步导出 Span | 阻塞业务 | 批量处理器 |
| 没有 shutdown hook | 退出丢数据 | sdk.close() |
| 认为采样会减少 Metrics | 成本预估错误 | Metrics/Logs 单独治理 |
| endpoint 用了 4317 但协议是 http | 连不上 | 4317=grpc,4318=http/protobuf |
| 关闭了 TLS 校验 | 中间人风险 | 配置正确 CA |
| 把密码写进 attribute | 数据泄露 | 脱敏、禁止记录 |
| 只埋点不建 Dashboard | 数据无法使用 | 同步建设查询和告警 |
自调用 @WithSpan |
不生效 | 拆分 Bean 或手动 Span |
| 忽略 Collector 队列 | 丢数据 | 监控队列/丢弃/重试指标 |
一次请求的完整链路
以「创建订单」为例,完整链路如下:
Client |
这条链路里:
- 自动埋点负责框架边界:HTTP、JDBC、Kafka、Redis。
- 手动埋点负责业务语义:
order.create、inventory.reserve。 - Context 通过 HTTP Header 和 Kafka Header 传播。
- Baggage 可以带
tenant.id,但不要带敏感信息。 - Metrics 记录订单数、失败数、处理延迟。
- Logs 带
trace_id/span_id,可从日志跳到 Trace。 - Sampling 在根 Span 决定是否采样,下游继承。
- Collector 负责批处理、限流、tail sampling 和导出。
- 后端 负责存储、查询、Dashboard 和告警。
参考资料
- OpenTelemetry 官方文档
- What is OpenTelemetry
- Traces
- Metrics
- Logs
- Context propagation
- Sampling
- Resources
- Semantic Conventions
- Java
- Java API
- Java SDK
- Java Agent
- Java Agent Configuration
- Instrumentation ecosystem
- Spring Boot Starter
- Collector
- OTLP Specification
- OpenTelemetry Java GitHub
- OpenTelemetry Java Instrumentation GitHub
- OpenTelemetry Java Examples