Spring Boot 4 可观测性源码剖析:OpenTelemetry 全链路打通日志、指标、追踪

本文是 Spring Boot 4 系列第 16 篇 | 基于 Spring Boot 4.1.0 仓库(module/spring-boot-opentelemetrymodule/spring-boot-micrometer-tracing-opentelemetrymodule/spring-boot-micrometer-metrics)与官方文档 actuator/observability.adocactuator/tracing.adocactuator/loggers.adoc | 预计阅读 25 分钟

文末附「三条链路默认行为差异」与「版本对照表」,落地时可直接对照。


写在前面

线上排查一个慢接口,经常遇到三个信号对不上的情况:

  1. 监控告警:POST /api/orders/confirm P99 从 200ms 涨到 3.2s;
  2. 看日志:日志里有 traceId 字段,但十个请求九个是 00000000000000000000000000000000(全零),剩下一个 traceId 对不上号;
  3. 看指标:http.server.requests 只有总和,没有按接口拆分的延迟分布;
  4. 看链路: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-bommicrometer-bommicrometer-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 本身不生产任何信号 。它只是把容器里已有的 SdkTracerProviderContextPropagatorsSdkLoggerProviderSdkMeterProvider 四个 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 只管组装,信号各自独立。

另外注意两点:

  1. Resource 合并 :openTelemetryResource Bean 用 Resource.getDefault().merge(...) 合并三类来源(见 OpenTelemetryResourceAttributes 源码,@since 4.0.0):环境变量 OTEL_RESOURCE_ATTRIBUTES / OTEL_SERVICE_NAMEmanagement.opentelemetry.resource-attributes 配置(用户值优先)→ 兜底默认:service.namespring.application.name(没有则为 unknown_service)、service.namespacespring.application.group。这是所有 span / log record / metric 上 service.name 标签的来源;
  2. 禁用分支 :disabledOpenTelemetrySdkmanagement.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)只做了一件事:组装 OtlpHttpSpanExporterOtlpGrpcSpanExporter

这里有一个从 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-otlpOtlpMeterRegistry 放上 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 强相关的点:

  1. 虚拟线程 :注册表有 @ConditionalOnThreading(Threading.VIRTUAL)otlpMeterRegistryVirtualThreads 变体------开启虚拟线程时指标上报任务跑在虚拟线程上;
  2. Exemplars(4.1) :ExemplarContextProvider Bean 存在时启用 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)自动注册 JvmGcMetricsJvmHeapPressureMetricsJvmMemoryMetricsJvmThreadMetricsClassLoaderMetrics 等 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.contextThreadLocalAccessor 手动织入------配置繁琐、极易漏。

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 @Async methods and the AsyncTaskExecutor is auto-configured, you have to opt-in for context propagation using the spring.task.execution.propagate-context property."

如果你自己定义 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_onALWAYS_ONtraceidratioTRACE_ID_RATIOparentbased_traceidratioPARENT_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 tracecontextW3Cb3B3b3multiB3_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 其余增强速览

  1. management.opentelemetry.enabled 全局开关 (官方 issue #49564):OpenTelemetryPropertiesenabled 字段自 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 也照常上报;
  2. Sampler 属性化 (#49548):management.opentelemetry.tracing.sampler 六种策略任选,替代自定义 Sampler Bean;
  3. BatchLogRecordProcessor 属性化 (#49543):即上文 management.opentelemetry.logging.*;
  4. OTLP Exemplars (#49538、#49572):指标带 traceId 关联,management.tracing.exemplars.include 细粒度控制;
  5. SSL Bundle 支持三条 OTLP 链路 (#49590/#49584/#49583):metrics、traces、logs 导出都能挂 spring.ssl.bundle.pem.* 证书包,mTLS 场景不再需要自定义 exporter;
  6. Zipkin OTel 弃用 (#49557):ZipkinWithOpenTelemetryTracingAutoConfiguration 保留但标注弃用,Spring Boot 4.2 移除 。还在用 opentelemetry-exporter-zipkin 的建议尽快评估两条出路:切 Brave + Zipkin(spring-boot-starter-zipkin),或让 Zipkin 直接接收 OTLP(zipkin-otel 模块);
  7. 依赖升级: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);
    }
}

启动后打几个请求,验证路径:

  1. Jaeger (localhost:16686):按 service.name=order-service 查,能看到 10% 概率采样的完整链路------HTTP span → @Async 子 span → 下游;
  2. Prometheus (localhost:9090):http.server.requests_secondsorder.confirm 系列指标;
  3. Grafana:Prometheus 数据源查指标,配合 exemplar 直接从指标点跳 Jaeger trace;
  4. 日志 :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 把指标和链路关联在一起。

三个要点:

  1. trace 导出要显式配 endpoint,metrics 默认打 localhost 4318,logs 要 endpoint + 第三方 appender------三条链路的默认行为完全不一样,这是最常见的三个坑;
  2. spring.task.execution.propagate-context=true 是 4.1 升级必加的一行,否则 @Async 里的链路全是断的;
  3. 容器化部署优先用 OTEL_* 环境变量,Boot 4.1 会自动映射成自己的属性,代码配置零改动。

相关推荐
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之跨域处理
java·开发语言·spring
学渣超2 小时前
从一次早高峰数据库告警说起:你真的理解缓存该如何落地应用吗?
redis·后端·架构
我是大猴子2 小时前
MyBatis‑Plus & MyBatis‑Flex 区别
java·服务器·数据库
中趴菜2 小时前
接口返回的JSON为什么有反斜杠
后端
Bs_MoneyMagnet2 小时前
基于springboot+vue的在线音乐管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
小番茄程序猿2 小时前
Agent 工程化实测:p95 从 836ms 降到 12ms,而真正的收获是发现瓶颈根本不在 Agent 这层
后端
YangYang9YangYan2 小时前
2026 校招管理会计 JD 拆解,数据分析能力要求与工具清单
java·数据库·数据分析
仍然.2 小时前
SpringCloud---Seata
spring boot·后端·spring cloud