本文是 Spring Boot 4 系列第 16 篇 | 基于 Spring Boot 4.1.0 仓库(
module/spring-boot-opentelemetry、module/spring-boot-micrometer-tracing-opentelemetry、module/spring-boot-micrometer-metrics)与官方文档actuator/observability.adoc、actuator/tracing.adoc、actuator/loggers.adoc| 预计阅读 25 分钟文末附「三条链路默认行为差异」与「版本对照表」,落地时可直接对照。
写在前面
线上排查一个慢接口,经常遇到三个信号对不上的情况:
- 监控告警:
POST /api/orders/confirmP99 从 200ms 涨到 3.2s; - 看日志:日志里有
traceId字段,但十个请求九个是00000000000000000000000000000000(全零),剩下一个 traceId 对不上号; - 看指标:
http.server.requests只有总和,没有按接口拆分的延迟分布; - 看链路:Jaeger 里只有本服务一段 span,下游 Redis、数据库、第三方支付全是断的。
这类问题的典型根因之一是 @Async 方法里的 ThreadLocal 上下文丢失------traceId 没传进异步线程,下游调用全部成为孤儿 span。
Spring Boot 4.0 把 OpenTelemetry 扶正为官方能力(spring-boot-starter-opentelemetry),4.1 又补齐了 @Async 上下文传播、OTEL 环境变量映射、exemplars、SSL Bundle 等一整套生产级能力。官方 Release Highlights 里可观测性部分的原话:
"Context can be automatically propagated to methods running on a separate thread using
@Async. Also includes OpenTelemetry enhancements and SSL bundle support for OTLP."
本文从三个模块的源码出发,把日志、指标、追踪三条信号链路的自动装配逐层拆开。
内容速览
- 全景图:一个 Starter、四个模块、三条信号链路
- Trace 三层装配:SDK 组装、Tracer/采样器/Span 处理器、OTLP 导出
- 默认采样器是 PARENT_BASED_TRACE_ID_RATIO,概率来自 management.tracing.sampling.probability
- Boot 4 行为变化:trace 导出无默认端点,不配置 endpoint 导出器不创建
- 指标走 Micrometer 不走 OTel API,默认端点是 localhost:4318
- 日志链路:SDK 自动配置 + 第三方 appender,默认完全关闭
- 4.1 关键增强:@Async 上下文传播一行配置
- 4.1 关键增强:OTEL_* 环境变量自动映射成 Spring 属性
- 实战:docker-compose 拉起 Collector + Jaeger + Prometheus + Grafana
- 三条链路默认行为差异与采样策略选择
一、全景图:三层架构 + 三大信号
先看依赖坐标。Spring Boot 4.1 的官方 OpenTelemetry 支持由 2 个直接模块 + 1 个指标模块(经 Starter 间接引入) + 1 个 Starter 构成:
bash
starter/spring-boot-starter-opentelemetry # 唯一需要引入的坐标
├── starter/spring-boot-starter # 基础
├── starter/spring-boot-starter-micrometer-metrics # 指标(Observation + Micrometer)
├── module/spring-boot-micrometer-tracing-opentelemetry # Tracing 桥接
├── module/spring-boot-opentelemetry # OTel SDK 自动配置
└── runtimeOnly:
io.micrometer:micrometer-registry-otlp
io.micrometer:micrometer-tracing-bridge-otel
io.opentelemetry:opentelemetry-exporter-otlp
(以上依赖来自 starter/spring-boot-starter-opentelemetry/build.gradle 原文。)
版本统一由 BOM 管理(platform/spring-boot-dependencies/build.gradle 里的 opentelemetry-bom、micrometer-bom、micrometer-tracing-bom),4.1 升级到了 OpenTelemetry 1.62 + Micrometer 1.17(官方 Release Highlights)。
三条信号各自的装配链路:
less
┌────────────────────────────── 应用层 ──────────────────────────────┐
│ @RestController / @Async / 自定义 Observation API / 业务日志 │
└──────────────────────────────────────────────────────────────────┘
│ 注入 │ │
┌─────────────────▼──────────┐ ┌────▼─────────────┐ ┌▼───────────────┐
│ Traces │ │ Metrics │ │ Logs │
│ OpenTelemetryTracingAuto- │ │ OtlpMetricsExport│ │ OpenTelemetry- │
│ Configuration │ │ AutoConfiguration│ │ LoggingAuto- │
│ SdkTracerProvider │ │ OtlpMeter- │ │ Configuration │
│ Sampler / SpanLimits │ │ Registry │ │ SdkLogger- │
│ BatchSpanProcessor │ │ Exemplar │ │ Provider │
│ Tracer / OtelTracer │ │ JVM Binder │ │ BatchLog- │
└──────────────┬─────────────┘ └────┬─────────────┘ │ Record- │
│ OtlpHttpSpanExporter│ │ Processor │
│ │ └──────┬─────────┘
▼ ▼ ▼
┌── OTel Collector (OTLP: 4317 gRPC / 4318 HTTP) ──────────┐
│ → Jaeger / Tempo(Traces) │
│ → Prometheus(Metrics,或 Collector 直推) │
│ → Loki / ClickHouse(Logs) │
└───────────────────────────────────────────────────────────┘
二、Trace 链路:从 SDK 到 OTLP 的三层装配
2.1 第一层:OpenTelemetrySdkAutoConfiguration------SDK 组装与 Resource
module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java(@since 4.0.0):
java
@AutoConfiguration
@ConditionalOnClass({ OpenTelemetry.class, OpenTelemetrySdk.class })
@EnableConfigurationProperties(OpenTelemetryProperties.class)
public final class OpenTelemetrySdkAutoConfiguration {
@Bean
@ConditionalOnMissingBean(OpenTelemetry.class)
@ConditionalOnEnabledOpenTelemetry
OpenTelemetrySdk openTelemetrySdk(ObjectProvider<SdkTracerProvider> openTelemetrySdkTracerProvider,
ObjectProvider<ContextPropagators> openTelemetryContextPropagators,
ObjectProvider<SdkLoggerProvider> openTelemetrySdkLoggerProvider,
ObjectProvider<SdkMeterProvider> openTelemetrySdkMeterProvider) {
OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder();
openTelemetrySdkTracerProvider.ifAvailable(builder::setTracerProvider);
openTelemetryContextPropagators.ifAvailable(builder::setPropagators);
openTelemetrySdkLoggerProvider.ifAvailable(builder::setLoggerProvider);
openTelemetrySdkMeterProvider.ifAvailable(builder::setMeterProvider);
return builder.build();
}
...
}
这个类设计上有个关键点:SDK 本身不生产任何信号 。它只是把容器里已有的 SdkTracerProvider、ContextPropagators、SdkLoggerProvider、SdkMeterProvider 四个 Bean 组装进 OpenTelemetrySdk。注意:实际 Boot 只自动配置了前三个------SdkTracerProvider 由 Tracing 模块提供、SdkLoggerProvider 由 Logging 模块提供,而 SdkMeterProvider 没有任何自动配置 :官方文档明确说 "Spring Boot doesn't use OpenTelemetry's metrics functionality"------指标走的是 Micrometer 的 OtlpMeterRegistry 直接以 OTLP 导出(见第三节),不经过 OTel 的指标 API。这是 Boot 4.0 模块化自动配置思路的一个缩影:SDK 只管组装,信号各自独立。
另外注意两点:
Resource合并 :openTelemetryResourceBean 用Resource.getDefault().merge(...)合并三类来源(见OpenTelemetryResourceAttributes源码,@since 4.0.0):环境变量OTEL_RESOURCE_ATTRIBUTES/OTEL_SERVICE_NAME→management.opentelemetry.resource-attributes配置(用户值优先)→ 兜底默认:service.name取spring.application.name(没有则为unknown_service)、service.namespace取spring.application.group。这是所有 span / log record / metric 上service.name标签的来源;- 禁用分支 :
disabledOpenTelemetrySdk在management.opentelemetry.enabled=false时兜底,只配置 propagators------也就是关掉 OpenTelemetry 后 W3C 传播仍然生效(ConditionalOnEnabledOpenTelemetry注解是 4.1 新增的,@since 4.1.0,后面第七节细说;注意禁用只影响 traces / logs,指标不受影响,见第七节)。
2.2 第二层:OpenTelemetryTracingAutoConfiguration------Tracer、采样器、Span 处理器
module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfiguration.java(@since 4.0.0):
java
@AutoConfiguration(before = { MicrometerTracingAutoConfiguration.class, NoopTracerAutoConfiguration.class })
@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class })
它排在 Micrometer Tracing 自动配置之前,保证 Tracer 先于观测处理器就绪。核心 Bean:
| Bean | 源码要点 | 默认值 |
|---|---|---|
SdkTracerProvider |
sampler + resource + spanLimits + spanProcessors + 自定义器 | 采样器见下 |
Sampler |
6 种策略 switch,绑定 management.opentelemetry.tracing.sampler |
PARENT_BASED_TRACE_ID_RATIO |
SpanLimits |
绑定 management.opentelemetry.tracing.limits.* |
属性/事件/链接各 128 |
BatchSpanProcessor |
绑定 management.opentelemetry.tracing.export.* |
队列 2048、批量 512、调度 5s、超时 30s、不含未采样 span |
Tracer |
openTelemetry.getTracer("org.springframework.boot", SpringBootVersion.getVersion()) |
--- |
OtelTracer |
Micrometer Tracing 桥接,含 Baggage 管理器 | remote/tag fields 可配 |
采样器这一段值得展开------它是生产环境第一个要调的配置:
java
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledOpenTelemetry
Sampler otelSampler() {
return switch (this.openTelemetryTracingProperties.getSampler()) {
case ALWAYS_ON -> Sampler.alwaysOn();
case ALWAYS_OFF -> Sampler.alwaysOff();
case TRACE_ID_RATIO -> Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability());
case PARENT_BASED_ALWAYS_ON -> Sampler.parentBased(Sampler.alwaysOn());
case PARENT_BASED_ALWAYS_OFF -> Sampler.parentBased(Sampler.alwaysOff());
case PARENT_BASED_TRACE_ID_RATIO ->
Sampler.parentBased(Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability()));
};
}
注意概率的来源:TracingProperties.getSampling().getProbability(),即 management.tracing.sampling.probability,默认 0.10 (10%)。PARENT_BASED_TRACE_ID_RATIO 的意思是:根 span 按 10% 概率采样,子 span 直接继承父 span 的采样决定------网关入口采样了,整条链路才完整;入口没采,后面全丢。这是标准的 head-based 采样。
2.3 第三层:OtlpTracingAutoConfiguration------Span 导出
module/spring-boot-micrometer-tracing-opentelemetry/.../otlp/OtlpTracingAutoConfiguration.java(@since 4.0.0)只做了一件事:组装 OtlpHttpSpanExporter 或 OtlpGrpcSpanExporter。
这里有一个从 Boot 3 升级必须知道的行为变化 :4.x 不再默认导出到 localhost:4318/v1/traces。源码里 OtlpTracingConnectionDetails 的条件是:
java
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint")
OtlpTracingConnectionDetails otlpTracingConnectionDetails(...)
不配置 management.opentelemetry.tracing.export.otlp.endpoint,Span 导出器根本不创建 ------trace 生成本地记录(BatchSpanProcessor 队列里),但不出门。这是 Boot 4 与 Boot 3 的一大差异:Boot 3.x 的端点属性是 management.otlp.tracing.endpoint,默认 http://localhost:4318/v1/traces;Boot 4 改成 management.opentelemetry.tracing.export.otlp.endpoint 且无默认值 (git 历史里该模块最早版本就是 private String endpoint; 无初始值),必须显式声明端点。这是有意的设计:防止本地开发时无意识地把流量打到默认端口。
Exporter 的可配项(OtlpTracingProperties,@ConfigurationProperties("management.opentelemetry.tracing.export.otlp")):
| 属性 | 默认值 | 说明 |
|---|---|---|
endpoint |
无(必填) | Collector 的 HTTP API 地址 |
transport |
http |
http(HTTP/protobuf)或 grpc,类 javadoc 明确说明 HTTP/JSON 不被 OTel Java SDK 支持 |
timeout |
10s | 整次导出调用超时(DNS+连接+发送+服务端处理+读响应) |
connectTimeout |
10s | 连接超时 |
compression |
none |
gzip / none |
headers |
空 | 自定义头,如认证 |
ssl.bundle |
无 | 4.1 新增 :引用 spring.ssl.bundle.* 定义的 SSL 证书包 |
想深度定制有两条路:注册 OtlpHttpSpanExporterBuilderCustomizer / OtlpGrpcSpanExporterBuilderCustomizer Bean(官方文档:优先于自动配置的任何设置),或直接定义自己的 OtlpHttpSpanExporter / OtlpGrpcSpanExporter Bean 让自动配置退避(@ConditionalOnMissingBean)。
三、Metrics 链路:Micrometer + OTLP Registry
指标的装配不在 OTel 模块里,而在 module/spring-boot-micrometer-metrics(spring-boot-starter-micrometer-metrics 的依赖)。Starter 里的 runtimeOnly io.micrometer:micrometer-registry-otlp 把 OtlpMeterRegistry 放上 classpath,OtlpMetricsExportAutoConfiguration 随即生效:
java
@AutoConfiguration(
before = { CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class },
after = MetricsAutoConfiguration.class)
@ConditionalOnBean(Clock.class)
@ConditionalOnClass({ OtlpMeterRegistry.class, OpenTelemetryProperties.class })
@ConditionalOnEnabledMetricsExport("otlp")
@EnableConfigurationProperties({ OtlpMetricsProperties.class, OpenTelemetryProperties.class })
public final class OtlpMetricsExportAutoConfiguration { ... }
几个值得注意的默认值(OtlpMetricsProperties,@ConfigurationProperties("management.otlp.metrics.export"),@since 4.0.0):
| 属性 | 默认值 | 说明 |
|---|---|---|
url |
空(= Micrometer 默认 http://localhost:4318/v1/metrics) |
官方文档 metrics.adoc 原文:"By default, metrics are exported over OTLP to a consumer running on your local machine." |
aggregation-temporality |
CUMULATIVE |
累积 vs 增量,Prometheus 风格后端通常选 cumulative |
histogram-flavor |
EXPLICIT_BUCKET_HISTOGRAM |
显式桶 vs 指数桶(BASE2_EXPONENTIAL_BUCKET_HISTOGRAM,4.1 支持配置) |
max-scale / max-bucket-count |
20 / 160 | 指数直方图参数 |
compression-mode |
NONE |
4.1 支持 GZIP(官方 Release:metrics 导出压缩一行配置开启) |
base-time-unit |
MILLISECONDS |
|
ssl.bundle |
无 | 4.1 新增 |
注意 Metrics 和 Traces/Logs 在"默认端点"上的不对称 :指标默认打到本机 4318/v1/metrics(Micrometer Registry 的行为),而 trace 导出必须有显式 endpoint------所以裸跑一个只加了 spring-boot-starter-opentelemetry 的应用,指标会尝试连接 localhost 的 Collector(连不上就静默排队重试),trace 却一条都不发。这是刚上手时最容易困惑的点。
另外两个和 4.1 强相关的点:
- 虚拟线程 :注册表有
@ConditionalOnThreading(Threading.VIRTUAL)的otlpMeterRegistryVirtualThreads变体------开启虚拟线程时指标上报任务跑在虚拟线程上; - Exemplars(4.1) :
ExemplarContextProviderBean 存在时启用 OTLP exemplar(把采样过的 traceId 附加到 metric 时间序列上,实现"指标 → 链路"跳转)。用 Micrometer Tracing 时自动配置;默认只包含采样过的 trace(Include.SAMPLED_TRACES,源码TracingProperties.Exemplars实证),可用management.tracing.exemplars.include(NONE/ALL/SAMPLED_TRACES)控制。一个小坑:javadoc 里注明ALL在对接 Prometheus 时不受支持------Prometheus 的 exemplar 只认采样过的 trace。
JVM 指标不用管:JvmMetricsAutoConfiguration(同模块 jvm/ 目录,@since 4.0.0)自动注册 JvmGcMetrics、JvmHeapPressureMetrics、JvmMemoryMetrics、JvmThreadMetrics、ClassLoaderMetrics 等 Binder。虚拟线程指标(VirtualThreadMetrics)需要单独引入 io.micrometer:micrometer-java21------它是模块里的 optional 依赖,默认不传递;把它加进 classpath 后,内部的 VirtualThreadMetricsConfiguration(@ConditionalOnClass(name = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics"))才会生效注册。
四、Logs 链路:OTel 日志导出的完整装配
4.1 日志 SDK 自动配置
module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfiguration.java(@since 4.0.0):
java
@AutoConfiguration(after = OpenTelemetrySdkAutoConfiguration.class)
@ConditionalOnClass(SdkLoggerProvider.class)
@ConditionalOnEnabledOpenTelemetry
@EnableConfigurationProperties(OpenTelemetryLoggingProperties.class)
public final class OpenTelemetryLoggingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
BatchLogRecordProcessor openTelemetryBatchLogRecordProcessor(ObjectProvider<LogRecordExporter> logRecordExporters) {
LogRecordExporter exporter = LogRecordExporter.composite(logRecordExporters.orderedStream().toList());
Export export = this.properties.getExport();
return BatchLogRecordProcessor.builder(exporter)
.setExporterTimeout(export.getTimeout())
.setMaxQueueSize(export.getMaxQueueSize())
.setScheduleDelay(export.getScheduleDelay())
.setMaxExportBatchSize(export.getMaxBatchSize())
.build();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(Resource.class)
SdkLoggerProvider openTelemetrySdkLoggerProvider(Resource openTelemetryResource,
ObjectProvider<LogRecordProcessor> logRecordProcessors,
ObjectProvider<SdkLoggerProviderBuilderCustomizer> customizers, LogLimits logLimits) { ... }
}
SdkLoggerProvider 和 trace 的 SdkTracerProvider 结构完全同构:Resource + Processor 链 + Limits。4.1 新增 management.opentelemetry.logging.* 配置类(OpenTelemetryLoggingProperties,@since 4.1.0)把 BatchLogRecordProcessor 行为参数化:export.timeout(30s)、export.max-queue-size(2048)、export.schedule-delay(1s)、export.max-batch-size(512),以及 limits.max-attributes(128)、limits.max-attribute-value-length。
4.2 OTLP 日志导出
.../logging/otlp/OtlpLoggingAutoConfiguration.java(@since 4.0.0)与 trace 导出同构:management.opentelemetry.logging.export.otlp.endpoint 必填 ,transport 默认 http 可切 grpc,导出开关是 management.logging.export.enabled / management.logging.export.otlp.enabled,同样有 OtlpHttpLogRecordExporterBuilderCustomizer / OtlpGrpcLogRecordExporterBuilderCustomizer 扩展点和 4.1 的 ssl.bundle 支持。
官方文档 loggers.adoc 对日志的定位说得很清楚:
"By default, logging via OpenTelemetry is not configured."
三条链路里只有日志默认完全关闭------因为没有 appender 它是无法工作的。
4.3 关键一环:Appender 不属于 Spring Boot
这是最容易踩的坑:OTel 的 Logback / Log4j2 appender 不是 Spring Boot 提供的。官方文档原文:
"The OpenTelemetry Logback appender and Log4j appender are not part of Spring Boot."
你需要自己引入 opentelemetry-java-instrumentation 里的 io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender,在 logback-spring.xml 里配置:
xml
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<captureExperimentalAttributes>true</captureExperimentalAttributes>
<captureCodeAttributes>true</captureCodeAttributes>
</appender>
<root level="INFO">
<appender-ref ref="OTEL"/>
</root>
Appender 需要访问容器里的 OpenTelemetry Bean,官方文档给出了标准的桥接写法(OpenTelemetryAppenderInitializer):
java
@Component
class OpenTelemetryAppenderInitializer implements InitializingBean {
private final OpenTelemetry openTelemetry;
OpenTelemetryAppenderInitializer(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@Override
public void afterPropertiesSet() {
OpenTelemetryAppender.install(this.openTelemetry); // 把 Boot 装配的 SDK 交给 appender
}
}
装上之后,OTel appender 在每条日志事件产生时自动从当前 OTel Context 附加 trace_id / span_id / trace_flags 字段,导出的 log record 天然携带链路标识。这就是日志与链路关联的基础:同一 trace 的所有日志带同一个 trace_id,点日志直接跳链路。
五、4.1 关键增强:@Async 上下文自动传播
@Async 线程里 traceId 全零,是链路追踪最常见的断裂点,也是 4.1 最值得升级的一条能力。
5.1 问题本质
Trace 上下文存放在 OTel Context(基于 ThreadLocal)里。@Async 方法被提交到线程池,池里的工作线程是另一条 ThreadLocal 上下文------不处理的话,异步线程里的 span 就是孤儿 span,甚至干脆不产生。
Boot 4.1 之前怎么办?Boot 3 的文档会让你自己注册 TaskDecorator 做快照与恢复,或者借助 io.micrometer.context 的 ThreadLocalAccessor 手动织入------配置繁琐、极易漏。
5.2 源码:一行配置背后的 Bean
core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java(4.1 新增提交 "Add support for context propagation in task execution"):
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ContextSnapshot.class)
static class TaskExecutorContextPropagationConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.task.execution.propagate-context", havingValue = "true")
ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
return new ContextPropagatingTaskDecorator();
}
}
两个条件:
@ConditionalOnClass(ContextSnapshot.class):io.micrometer.context.ContextSnapshot(micrometer-context 库,随 starter-opentelemetry 自动引入);spring.task.execution.propagate-context=true:显式 opt-in。
用法就是在配置里加一行:
yaml
spring:
task:
execution:
propagate-context: true # 4.1 新增:@Async 自动透传 Micrometer Context(含 trace 上下文)
5.3 它如何作用到 @Async 上
ContextPropagatingTaskDecorator(org.springframework.core.task.support.*,Spring Framework 6.1 起就提供的类,javadoc 实证 "Since: 6.1")内部使用 ContextSnapshotFactory(默认实例)在提交线程捕获上下文快照并包裹任务,任务在目标线程执行时自动恢复快照------一个标准的 decorator 包裹。4.1 的新增点不是这个类,而是 Boot 把它自动接进 applicationTaskExecutor 的装配。Framework 的 javadoc 也提了一个权衡:它会给任务执行带来额外开销,"不适合大量微小任务"的场景。
关键在于它是怎么进到 @Async 执行链路的。看同一个文件里两个 executor builder:
java
@Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@ConditionalOnThreading(Threading.PLATFORM)
ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder builder) {
return threadPoolTaskExecutorBuilder.build(); // 平台线程池
}
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@ConditionalOnThreading(Threading.VIRTUAL)
SimpleAsyncTaskExecutor applicationTaskExecutorVirtualThreads(SimpleAsyncTaskExecutorBuilder builder) {
return builder.build(); // 虚拟线程
}
两个 Builder 创建时都执行 builder.taskDecorator(getTaskDecorator(taskDecorator))------从容器取 TaskDecorator Bean 集合:只有一个就直接使用,有多个则包装成 CompositeTaskDecorator 按序执行。ContextPropagatingTaskDecorator 就是这个被注入的 Bean,你自己注册的 TaskDecorator 会与它组合叠加(注意此处没有 @ConditionalOnMissingBean 退避语义,不是二选一的关系)。所以:
- 虚拟线程模式 (
spring.threads.virtual.enabled=true,系列第五篇讲过)→ 走SimpleAsyncTaskExecutorBuilder→ 同样被 decorator 包裹; - 平台线程模式 (默认)→ 走
ThreadPoolTaskExecutorBuilder→ 同样被包裹。
无论哪种线程模型,@Async 方法都能拿到调用方的 trace 上下文。官方文档 observability.adoc 的表述是:
"If you're working with
@Asyncmethods and theAsyncTaskExecutoris auto-configured, you have to opt-in for context propagation using thespring.task.execution.propagate-contextproperty."
如果你自己定义 AsyncTaskExecutor(不经过自动配置的 builder),官方给出的做法是手动注册 ContextPropagatingTaskDecorator Bean,或在自己的 Executor 上显式 setTaskDecorator(...)。
5.4 附带覆盖:Reactor 链路
同样的上下文传播也覆盖了响应式栈:spring.reactor.context-propagation 属性(ReactorProperties,module/spring-boot-reactor)控制 Micrometer Context 的 ThreadLocal 值是否自动恢复到 Reactor 算子中。注意它默认是 LIMITED 模式------只在 tap / handle 两类算子上生效;想要全算子透传,显式设成 auto 即可。
六、4.1 关键增强:OTEL 环境变量映射
云原生部署(K8s、OpenShift)里,采集端团队习惯用 OTEL_* 环境变量统一配置所有服务------而 Spring Boot 的配置体系是 application.yml。两边对不上,一直是 OTel 落地时的摩擦点。
4.1 的 OpenTelemetryEnvironmentVariableEnvironmentPostProcessor(@since 4.1.0)解决了这个问题:启动时读取系统环境变量中的 OTEL 变量,映射成 Spring Boot 属性 ,作为一个 OriginTrackedMapPropertySource(名为 openTelemetryEnvironmentVariables)插到 PropertySources 最前面:
java
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (!environment.getProperty(ENABLED_PROPERTY, Boolean.class, true)) {
return; // management.opentelemetry.map-environment-variables=false 可关闭
}
Map<String, OriginTrackedValue> map = new HashMap<>();
mapEnabled(map); // OTEL_SDK_DISABLED → management.opentelemetry.enabled(值取反)
mapPropagators(map); // OTEL_PROPAGATORS → management.tracing.propagation.type / baggage.enabled
mapSampler(map); // OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG → sampler / probability
mapMetricsEnvironmentVariables(map);
mapTracesEnvironmentVariables(map);
mapLogsEnvironmentVariables(map);
...
environment.getPropertySources().addFirst(new OriginTrackedMapPropertySource("openTelemetryEnvironmentVariables", map));
}
常用映射速查(源码逐行核实):
| OTEL 环境变量 | 映射到的 Spring Boot 属性 | 备注 |
|---|---|---|
OTEL_SDK_DISABLED |
management.opentelemetry.enabled |
值取反(SDK_DISABLED=true → enabled=false) |
OTEL_TRACES_SAMPLER |
management.opentelemetry.tracing.sampler |
always_on→ALWAYS_ON、traceidratio→TRACE_ID_RATIO、parentbased_traceidratio→PARENT_BASED_TRACE_ID_RATIO 等 6 种 |
OTEL_TRACES_SAMPLER_ARG |
management.tracing.sampling.probability |
仅当 sampler 是 traceidratio 系列时生效,校验 0.0~1.0 |
OTEL_PROPAGATORS |
management.tracing.propagation.type |
tracecontext→W3C、b3→B3、b3multi→B3_MULTI;含 baggage 才开 baggage |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
management.opentelemetry.tracing.export.otlp.endpoint |
也可用通用 OTEL_EXPORTER_OTLP_ENDPOINT,自动拼 /v1/traces |
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL |
management.opentelemetry.tracing.export.otlp.transport |
grpc/http/protobuf |
OTEL_EXPORTER_OTLP_TRACES_HEADERS |
management.opentelemetry.tracing.export.otlp.headers |
W3C 风格 k=v,k2=v2(W3CHeaderParser 解析) |
OTEL_BSP_* |
management.opentelemetry.tracing.export.* |
调度间隔/超时/队列/批量 |
OTEL_SPAN_*_COUNT_LIMIT 等 |
management.opentelemetry.tracing.limits.* |
|
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
management.otlp.metrics.export.url |
自动拼 /v1/metrics |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT |
management.opentelemetry.logging.export.otlp.endpoint |
自动拼 /v1/logs |
OTEL_LOGS_EXPORTER / OTEL_TRACES_EXPORTER / OTEL_METRICS_EXPORTER |
对应 export.enabled |
含 otlp 才为 true |
OTEL_EXPORTER_OTLP_*_CERTIFICATE / *_CLIENT_KEY |
spring.ssl.bundle.pem.opentelemetry-* |
4.1:自动生成 PEM SSL Bundle 并挂到导出器 |
不想让环境变量插手,一行关掉:management.opentelemetry.map-environment-variables=false(这个开关本身就是被 EnvironmentPostProcessor 读取的,所以它必须在 application.yml 里配置,不能来自 OTEL 变量本身)。
这个机制对容器化最大的价值:同一份镜像,在测试环境用环境变量指向测试 Collector、生产环境指向生产 Collector,代码与配置文件零改动------采集端和业务端解耦,符合 OTEL 规范"环境变量优先"的惯例。
七、4.1 其余增强速览
management.opentelemetry.enabled全局开关 (官方 issue #49564):OpenTelemetryProperties的enabled字段自 4.0 起就存在(默认 true),4.1 新增ConditionalOnEnabledOpenTelemetry组合注解(@since 4.1.0,matchIfMissing = true)统一挂在 SDK、Tracing、Logging 各自动配置上。关闭后 traces / logs 走 no-op 实现,仅保留 W3C propagators------链路上下文照常传播、数据不落盘 ,压测/灰度时的标准做法。注意一个官方文档明确提醒的细节:metrics 不受此开关影响 ------因为 Boot 不使用 OTel 的指标 API,指标由 Micrometer 的OtlpMeterRegistry直接导出,即使禁用 OpenTelemetry 也照常上报;- Sampler 属性化 (#49548):
management.opentelemetry.tracing.sampler六种策略任选,替代自定义 Sampler Bean; - BatchLogRecordProcessor 属性化 (#49543):即上文
management.opentelemetry.logging.*; - OTLP Exemplars (#49538、#49572):指标带 traceId 关联,
management.tracing.exemplars.include细粒度控制; - SSL Bundle 支持三条 OTLP 链路 (#49590/#49584/#49583):metrics、traces、logs 导出都能挂
spring.ssl.bundle.pem.*证书包,mTLS 场景不再需要自定义 exporter; - Zipkin OTel 弃用 (#49557):
ZipkinWithOpenTelemetryTracingAutoConfiguration保留但标注弃用,Spring Boot 4.2 移除 。还在用opentelemetry-exporter-zipkin的建议尽快评估两条出路:切 Brave + Zipkin(spring-boot-starter-zipkin),或让 Zipkin 直接接收 OTLP(zipkin-otel 模块); - 依赖升级:OpenTelemetry 1.62、Micrometer 1.17、micrometer-tracing(BOM 统一管理)。
八、实战:一条命令拉起全链路观测平台
8.1 docker-compose:Collector + Jaeger + Prometheus + Grafana
镜像版本为撰写时的示例版本,使用时请替换为当前可用 tag。
yaml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.130.0
command: ["--config=/etc/otelcol-contrib/config.yaml"]
volumes:
- ./otel-collector.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
depends_on: [jaeger, prometheus]
jaeger:
image: jaegertracing/jaeger:2.10
ports:
- "16686:16686" # UI(Jaeger v2 原生接收 OTLP,仅对内网暴露 4317/4318)
prometheus:
image: prom/prometheus:v3.6.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:12.0.0
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_SECURITY_ADMIN_PASSWORD=admin
Collector 配置(接收 OTLP,traces 转 Jaeger、metrics 转 Prometheus、logs 转 Loki):
yaml
receivers:
otlp:
protocols:
grpc: {}
http: {}
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls: { insecure: true }
prometheus:
endpoint: "0.0.0.0:8889"
debug: {}
processors:
batch: {}
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger, debug]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus, debug]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug] # 接 Loki 时换成 otlp/loki 或 loki exporter
8.2 应用侧:三条信号全开
yaml
# application.yml
spring:
application:
name: order-service
task:
execution:
propagate-context: true # 4.1: @Async 透传 trace 上下文
management:
opentelemetry:
resource-attributes:
env: production
version: 1.2.0
tracing:
sampler: PARENT_BASED_TRACE_ID_RATIO
logging:
export:
otlp:
endpoint: http://localhost:4318/v1/logs # 日志端点必填
otlp:
metrics:
export:
url: http://localhost:4318/v1/metrics # 指标默认就是 4318,可省略
tracing:
sampling:
probability: 0.10
exemplars:
include: SAMPLED_TRACES
observations:
annotations:
enabled: true # 开启 @Observed / @Timed / @NewSpan 注解支持
依赖(build.gradle):
groovy
implementation 'org.springframework.boot:spring-boot-starter-opentelemetry'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aspectj' // 注解观测需要 aspectjweaver
runtimeOnly 'io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0' // appender 非 Boot 提供
8.3 业务代码:自定义观测
java
@RestController
public class OrderController {
private final MeterRegistry meterRegistry;
private final ObservationRegistry observationRegistry;
// 1) 注解式:方法级 span + 指标(management.observations.annotations.enabled=true 时生效)
@Observed(name = "order.confirm", contextualName = "confirm-order")
@PostMapping("/api/orders/confirm")
public Order confirm(@RequestBody ConfirmRequest request) {
return doConfirm(request);
}
// 2) 编程式:把指标、链路、日志统一装进一次 Observation
public PaymentResult pay(String orderId) {
return Observation.createNotStarted("order.payment", observationRegistry)
.lowCardinalityKeyValue("orderId", orderId) // 进 metrics 标签 + trace 标签
.observe(() -> paymentService.pay(orderId));
}
// 3) 业务指标:MeterRegistry 直接埋点(自动带 traceId exemplar)
@Timed(value = "order.cache.hit", histogram = true)
public Order getFromCache(String orderId) {
return cache.get(orderId);
}
}
java
// @Async 异步方法------4.1 后无需任何手动 ThreadLocal 处理
@Service
public class NotificationService {
@Async
public CompletableFuture<Void> sendSms(String orderId) {
// 此线程中的 traceId 与调用方一致,span 挂在父 span 下
log.info("send sms for order {}", orderId);
return CompletableFuture.completedFuture(null);
}
}
启动后打几个请求,验证路径:
- Jaeger (localhost:16686):按
service.name=order-service查,能看到 10% 概率采样的完整链路------HTTP span →@Async子 span → 下游; - Prometheus (localhost:9090):
http.server.requests_seconds、order.confirm系列指标; - Grafana:Prometheus 数据源查指标,配合 exemplar 直接从指标点跳 Jaeger trace;
- 日志 :logback appender 发出的 log record 到 Collector,
trace_id字段与 Jaeger 对得上------DEBUG 模式下看 otel-collector 的 debug exporter 输出最快。
九、采样策略怎么选
Head-based(应用侧,默认路径) :PARENT_BASED_TRACE_ID_RATIO + management.tracing.sampling.probability。优点:零成本、无额外组件;缺点:低流量服务 10% 可能一天采不到几条;高流量服务想"保底留痕"也不可控。生产常见组合:入口服务 10%~100%,依赖分析用 Prometheus 侧指标兜底。
环境变量覆盖(云原生做法):同一镜像,部署平台通过 OTEL 变量注入采样率,不用改配置:
bash
# 测试环境
OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.5
# 生产环境
OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.1
Tail-based(Collector 侧) :应用侧 100% 采样(ALWAYS_ON),Collector 用 tail_sampling 处理器按规则(错误 span、慢 span、特定服务)决定保留哪些。完整但费资源,适合对完整链路有强需求的场景。
十、一张表看懂:Boot 3.x → 4.0 → 4.1
| 能力 | Boot 3.x | Boot 4.0 | Boot 4.1 |
|---|---|---|---|
| 官方 OTel Starter | 无(社区方案) | 有 spring-boot-starter-opentelemetry |
有,增强 |
| OTel SDK / Resource / Propagator 自动配置 | 无 | 有 OpenTelemetrySdkAutoConfiguration |
有 |
| Trace OTLP 导出 | 需手动依赖 + 默认 localhost | 有官方支持(endpoint 必填) | 有,+ SSL Bundle + gzip |
| Metrics OTLP 导出 | 有 Micrometer | 有 OtlpMetricsExportAutoConfiguration |
有,+ gzip + exemplars 配置 |
| Logs OTLP 导出 | 无 | 有骨架(endpoint 必填) | 有,+ management.opentelemetry.logging.* 细粒度属性 |
management.opentelemetry.enabled 全局开关 |
无 | 属性字段已存在(OpenTelemetryProperties @since 4.0.0) |
有统一注解 ConditionalOnEnabledOpenTelemetry(官方列为 4.1 新增 #49564) |
| OTEL 环境变量映射 | 无 | 无 | 有 OpenTelemetryEnvironmentVariableEnvironmentPostProcessor |
| @Async 上下文传播 | 无(手写 TaskDecorator) | 无 | 有 spring.task.execution.propagate-context=true |
| 采样器属性化 | 有(management.tracing.sampling.probability) | 有,+ 6 种策略 switch | 有 |
| Zipkin(OTel 桥) | 有 | 有 | 已弃用,4.2 移除 |
| OTel SDK 版本 | 随 micrometer-tracing-bom 管理 | opentelemetry-bom 统一管理 | 1.62(Micrometer 1.17,官方 Release Highlights) |
十一、总结
Spring Boot 4 的可观测性路线很清晰:以 OpenTelemetry 为统一协议,以 Micrometer Observation 为统一模型,SDK 组装(spring-boot-opentelemetry)、信号桥接(spring-boot-micrometer-*-*)分层自治。4.0 解决"有没有",4.1 解决"好不好用"------环境变量映射让容器化部署零配置接入,@Async 上下文传播堵住了最常见的链路断裂点,exemplars 把指标和链路关联在一起。
三个要点:
- trace 导出要显式配 endpoint,metrics 默认打 localhost 4318,logs 要 endpoint + 第三方 appender------三条链路的默认行为完全不一样,这是最常见的三个坑;
spring.task.execution.propagate-context=true是 4.1 升级必加的一行,否则 @Async 里的链路全是断的;- 容器化部署优先用
OTEL_*环境变量,Boot 4.1 会自动映射成自己的属性,代码配置零改动。