OpenTelemetry:trace、log 和 metric 的统一标准
1. 概述
当系统拆分为多个微服务时,一个请求可能会经过五个、十个不同的进程。当出现故障时,你需要知道请求去了哪里、在哪一步变慢了,以及哪些日志属于这个请求。OpenTelemetry (OTel) 是一个开放标准,正好解决了这个问题。它统一了如何收集 trace、metric 和 log 的方式,然后将它们发送到任何后端。
读完这篇文章后,你将理解 OTel 的核心概念以及它为什么受关注。你还会学到如何将 OTel 集成到 Spring Boot 3 应用中、运行 Collector、为 instrumentation 部分编写测试,以及何时不应该使用它。
2. OpenTelemetry 是什么?
OpenTelemetry 是一个属于 CNCF 的开源项目。它定义了如何创建、描述和传输遥测数据。OTel 不是一个数据存储或可视化界面。那部分由 Jaeger、Prometheus、Grafana Tempo/Loki 或商业服务等后端来处理。
2.1. 三种信号 (signals)
- Trace:一个请求的完整路径。每个 trace 由多个 span 组成,每个 span 代表一个工作单元,例如“调用 PaymentService”或“查询 orders 表”。Span 包含开始时间、结束时间和各种属性。
- Metric:随时间汇总的数值,比如每秒请求数、p99 延迟或失败订单数。
- Log:离散的事件记录。OTel 会在日志中加入
traceId和spanId,这样你可以从一条日志跳转到它的完整 trace。
最大的价值在于相关性。三种信号共享相同的上下文和资源描述,例如 service.name。
2.2. 主要组件
| 组件 | 作用 |
|---|---|
| API | 供代码创建 span 和 metric 的接口,不依赖特定供应商 |
| SDK | API 的具体实现:采样、批处理、导出 |
| OTLP | 数据传输协议 (gRPC 或 HTTP/protobuf) |
| Collector | 独立进程,接收、处理和转发遥测数据 |
| Semantic conventions | 属性命名约定,如 http.request.method、db.system… |
| Instrumentation | 为流行框架自动生成遥测数据的库或 agent |
2.3. 上下文传播
要让不同服务中的 span 连接成一个 trace,上下文必须随请求传播。对于 HTTP,OTel 默认使用 W3C Trace Context 标准,通过 traceparent 头传递:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
这个头有四部分:version、traceId、父 span 的 spanId 和采样标志。接收请求的服务读取这个头,并创建属于同一 trace 的子 span。
3. 为什么 OpenTelemetry 正在受关注?
- 厂商中立:你只需 instrument 一次代码。想更换后端时,只需修改 exporter 配置或 Collector,无需改代码。
- 统一旧标准:OTel 是通过合并 OpenTracing 和 OpenCensus 而来。社区不再需要在两个方向之间选择。
- 三种信号的统一标准:以前每种数据类型通常有自己的 agent 和格式。
- 广泛的生态系统:许多框架、云提供商和 observability 工具都支持 OTLP。Spring Boot 3 也通过 Micrometer 集成了它。
- 灵活的 Collector:你可以在基础设施层过滤敏感数据、采样和路由到多个后端,无需修改应用。
4. 在 Spring Boot 3 中应用 OpenTelemetry
Spring Boot 3 使用 Micrometer Observation 作为抽象层。你编写一个 observation,Micrometer 会同时生成 metric(计时器)和 span。通过 OTel 的 bridge 来处理导出部分。
4.1. 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Tracing 到 OpenTelemetry 的桥接 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- 通过 OTLP 导出 trace -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<!-- 通过 OTLP 导出 metric -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-otlp</artifactId>
</dependency>
<!-- 用于测试 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
你不需要手动指定版本。Spring Boot 通过 BOM 管理 Micrometer 和 OpenTelemetry 库的版本。
4.2. 配置
spring:
application:
name: order-service
management:
tracing:
sampling:
probability: 1.0 # 开发:采样 100%;生产环境应该降低
otlp:
tracing:
endpoint: http://localhost:4318/v1/traces
metrics:
export:
url: http://localhost:4318/v1/metrics
step: 30s
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
各配置组的含义:
spring.application.name用作service.name。这是后端用来区分不同服务的最重要属性。management.tracing.sampling.probability决定了记录 trace 的比例。- 两个
otlp端点指向通过 OTLP/HTTP 的 Collector,默认端口是 4318。 logging.pattern.level将 MDC 中的traceId和spanId注入到每一行日志中。这是将日志与 trace 关联的最简单方式。
4.3. 使用 Observation API 对业务逻辑进行 instrumentation
进入 controller 的 HTTP 请求和通过 RestClient/WebClient 的调用已由 Spring 自动 instrument。你只需手动添加重要的业务步骤:
public record Order(String id, String paymentMethod, long amount) {}
@FunctionalInterface
public interface PaymentService {
String charge(Order order);
}
@Service
public class CheckoutService {
private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);
private final ObservationRegistry registry;
private final PaymentService paymentService;
public CheckoutService(ObservationRegistry registry, PaymentService paymentService) {
this.registry = registry;
this.paymentService = paymentService;
}
public String checkout(Order order) {
return Observation.createNotStarted("order.checkout", registry)
.lowCardinalityKeyValue("payment.method", order.paymentMethod())
.highCardinalityKeyValue("order.id", order.id())
.observe(() -> {
log.info("Processing order {}", order.id());
return paymentService.charge(order);
});
}
}
代码中需要注意的几点:
Observation.createNotStarted("order.checkout", registry)创建一个名为order.checkout的 observation。lowCardinalityKeyValue添加一个基数较低的属性(CARD、CASH、WALLET)。这个属性出现在 metric 和 span 中。highCardinalityKeyValue添加一个基数非常高的属性,如订单号。它仅附加到 span,不会膨胀 metric。observe(...)打开 span、测量时间、记录异常(如果有)、然后当 supplier 返回时关闭 span。
运行时,日志会像这样:
INFO [order-service,4bf92f3577b34da6a3ce929d0e0e4736,a1b2c3d4e5f60718] ... Processing order A-1001
后端会收到两样东西:
- 一个
order.checkoutspan,嵌套在请求的 HTTP span 下面。 - 一个
order.checkouttimer metric,按payment.method分类。从这个 metric 你可以计算调用次数、总时间和最大时间。
提示:在 Spring 代码中优先使用 Observation API,而不是直接调用
io.opentelemetry.api.trace.Tracer。这样你的代码就不会依赖特定的 tracing 库。一次测量同时产生 metric 和 trace。
4.4. 替代方案:Java agent
如果你不想修改代码或添加依赖,可以使用 OpenTelemetry Java agent。Agent 会自动 instrument 许多流行的库,如 JDBC、HTTP client 和 Kafka:
export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
java -javaagent:./opentelemetry-javaagent.jar -jar order-service.jar
Agent 适合用于旧应用或当运维团队想集中控制 instrumentation 的情况。折衷之处是你对细节的控制会变少,启动时间增加,agent 像一个“黑盒子”修改字节码。
提示:不要同时运行 Java agent 和 Micrometer bridge 来收集相同类型的信号。你会收到重复的 span 或上下文混乱。
5. 运行 OpenTelemetry Collector
应用应该将数据发送到 Collector 而不是直接发送到后端。Collector 是批处理、过滤和路由数据的地方。以下是一个最小化配置:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: {}
attributes/strip-pii:
actions:
- key: customer.email
action: delete
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [attributes/strip-pii, batch]
exporters: [debug]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [debug]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug]
这个配置有四个部分:
receivers.otlp通过 gRPC (4317) 和 HTTP (4318) 接收数据。attributes/strip-pii在数据离开你的基础设施之前删除customer.email属性。batch在发送前聚合数据,以减少请求数量。debugexporter 将数据打印到控制台供你检查。在生产环境中,用你所使用后端的 exporter 替换它。较旧版本的 Collector 用logging作为这个 exporter 的名称。
用 Docker 启动 Collector:
docker run --rm -p 4317:4317 -p 4318:4318 \
-v "$(pwd)/otel-collector.yaml:/etc/otelcol-contrib/config.yaml" \
otel/opentelemetry-collector-contrib
调用一次 checkout API,然后查看 Collector 的控制台。你会看到类似这样的 span:
Span #0
Trace ID : 4bf92f3577b34da6a3ce929d0e0e4736
Parent ID : 9a8b7c6d5e4f3a2b
Name : order.checkout
Attributes:
-> payment.method: Str(CARD)
-> order.id: Str(A-1001)
6. 测试 instrumentation 部分
Instrumentation 也是代码,所以也需要测试。TestObservationRegistry 让你在不需要启动 Spring context 或 Collector 的情况下验证 observation:
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CheckoutServiceTest {
@Test
void whenCheckout_thenObservationIsRecordedWithPaymentMethod() {
TestObservationRegistry registry = TestObservationRegistry.create();
PaymentService paymentService = order -> "PAID-" + order.id();
CheckoutService service = new CheckoutService(registry, paymentService);
String result = service.checkout(new Order("A-1001", "CARD", 250_000));
assertThat(result).isEqualTo("PAID-A-1001");
TestObservationRegistryAssert.assertThat(registry)
.hasObservationWithNameEqualTo("order.checkout")
.that()
.hasLowCardinalityKeyValue("payment.method", "CARD")
.hasHighCardinalityKeyValue("order.id", "A-1001")
.hasBeenStarted()
.hasBeenStopped();
}
}
这个测试验证了三件事:
- 业务逻辑结果是正确的。
order.checkoutobservation 已被创建,并带有正确的属性。- Observation 已启动并关闭。一个忘记关闭的 span 是生产环境中很难检测到的错误。
7. 常见错误
- 基数爆炸:将
userId、orderId或包含参数的 URL 放入 metric 标签会导致 metric 字符串数量激增,存储成本随之增加。这些值应该只附加到 span。 - 在生产环境采样 100%:数据量和成本随流量线性增长。降低
probability,或在 Collector 使用尾采样来保留错误或慢速的 trace。 - 异步运行时丢失上下文:使用
@Async、CompletableFuture或自创的 thread pool 时,上下文不会自动传到新线程。你需要用上下文传播机制(如上下文传播库的ContextExecutorService)或使用 Spring 管理的执行器(已预配置)。 - 敏感数据泄露:email、卡号或令牌可能进入属性或日志。在代码中立即过滤它们,再在 Collector 添加一层过滤。
- 任意命名:不遵循 semantic conventions 使得仪表板和告警在服务间难以重用。
- 跳过 Collector:直接从应用发送到后端意味着每次切换后端都要重新部署所有服务。
8. 何时不应该使用(或尚不需要)
- 小型应用、单实例、依赖少:结构化日志加上 Actuator 提供的几个 metric 通常已足够。当没有“分布式”的东西时,分布式 tracing 价值有限。
- 团队还没有后端和运维人员:OTel 只是管道。如果没人看数据,你只是在多花 CPU、网络和存储费用。
- 系统对延迟或资源极度敏感:比如嵌入式应用或计时以微秒计的热路径。在广泛启用 instrumentation 之前,先测量开销。
- 期待 OTel 一次性替代现有所有工具:不同信号和语言的成熟度可能不同。检查你要使用部分的支持状态,并逐步迁移,从 tracing 开始。
9. 总结
OpenTelemetry 通过公共 API、OTLP 协议和 Collector 统一了如何创建和传输 trace、metric 和 log。这样你可以将代码中的 instrumentation 与后端选择分离。
在 Spring Boot 3 中,最简洁的路径包括四步。第一,用 Micrometer Observation 一次测量同时产生 metric 和 span。第二,通过 OTLP 导出到 Collector。第三,在日志中注入 traceId。第四,为重要的 observation 编写测试。同时,从一开始就控制基数、采样和敏感数据。
如果你的系统还很小,不必急着做。当服务数增长、故障排查开始耗费数小时时,OTel 就是值得投入的基础设施。