OpenTelemetry 的 Spring Boot starter 从 2.26.0 版本开始支持声明式配置:Java agent 在 2025 年末引入的同一套 YAML schema,如今可以嵌入 application.yaml。本文追踪环境变量 OTEL_SERVICE_NAME=petclinic 在这个新体系中的旅程,并解释其中的衔接细节。
多年来,环境变量以及对应的 JVM -D 参数是配置 OpenTelemetry SDK 的唯一方式:每个 exporter、每个 sampler、每个需要捕获的请求头,都以一串扁平的 OTEL_* 变量表示。
从 OpenTelemetry Spring Boot starter 2.26.0 开始,这份列表有了新的伙伴。SDK 的声明式配置 schema 是一棵 YAML 树,可以按照 SDK 实际运行时的结构,描述完整的遥测流水线——每个 processor、每个 exporter,以及每个嵌套选项。
过去,环境变量无法表达的配置,需要 Spring starter 用户编写 @Bean。Java agent 用户则必须编写完整的扩展,把它打包成单独的 jar,随 agent 一起分发;这种成本可能让人望而却步。
现在,schema 进入了 application.yaml,位于一个统一的 otel: 键之下。环境变量依然有效,但作用范围更窄:启动时,服务名称检测器会读取 OTEL_SERVICE_NAME,将它转换为资源属性;YAML 中写下的任何 ${VAR:default} 占位符,也会按名称读取相应变量。除此之外,以 YAML 为配置依据。
在 YAML 文件里再放一份 YAML
otel:
file_format: '1.0'
resource:
attributes:
- name: service.name
value: petclinic
tracer_provider:
processors:
- batch:
exporter:
otlp_http:
endpoint: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://localhost:4318/v1/traces}
otel: 之下的配置块,是嵌入 Spring application.yaml 的 OpenTelemetry SDK schema:一组 processor,每个 processor 包含 exporter,每个 exporter 再包含配置。otel.file_format 的存在就是开关。它之下的所有内容都会依据 SDK schema 解析,Spring 无需理解这些内容的含义。
仅靠环境变量翻不过去的墙
环境变量只能覆盖一份固定的内置选项列表:通过 OTEL_TRACES_SAMPLER 选择固定集合中的采样器,通过 OTEL_EXPORTER_OTLP_* 配置标准 OTLP exporter,以及常见的信号开关。列表之外的任何内容——自定义的基于规则的采样器、调试流水线中的第二个 OTLP exporter、baggage processor,或 SDK 提供的任意嵌套选项——都超出了环境变量模型的能力。声明式配置开放了树中其余部分。
starter 文档中有一个大多数团队第一天就需要的小例子:将 actuator 端点排除在追踪之外。过去,这要通过一个 @Configuration 类处理:
@Configuration
public class FilterPaths {
@Bean
public AutoConfigurationCustomizerProvider otelCustomizer() {
return p ->
p.addSamplerCustomizer(
(fallback, config) ->
RuleBasedRoutingSampler.builder(SpanKind.SERVER, fallback)
.drop(UrlAttributes.URL_PATH, "^/actuator")
.build());
}
}
现在,它是 application.yaml 中的一个 YAML 配置块:
otel:
tracer_provider:
sampler:
parent_based:
root:
rule_based_routing:
fallback_sampler:
always_on:
span_kind: SERVER
rules:
- action: DROP
attribute: url.path
pattern: /actuator.*
两个版本运行的是同一套 Java 代码:agent 与 starter 已经都包含了 contrib 项目的 opentelemetry-samplers jar。变化的是由谁编写装配配置。
接下来,本文将沿着三个阶段追踪 OTEL_SERVICE_NAME 进入 SDK 的过程。
第一阶段:抵达 Spring 属性栈
Spring 的属性加载器会将应用程序能看到的每个来源——application.yaml、各个启用 profile 的覆盖配置、JVM -D 参数、--key=value 命令行参数、环境变量——叠放到一个统一、可寻址的属性空间中。OTEL_SERVICE_NAME 与 SERVER_PORT、SPRING_PROFILES_ACTIVE 一起处于这个栈中。Spring 不知道哪些属性属于 OpenTelemetry;这是流程末端 starter 的职责。
flowchart LR
S["Spring resolves<br/>all properties"] --> W["starter walks otel.* keys"]
W --> SEAM{"key sits under<br/>a list index?"}
SEAM -- no --> OUT["un-flatten the whole map<br/>→ Jackson (once)<br/>→ SDK model"]
SEAM -- yes --> RE["recompute env-var name<br/>re-read environment"]
RE --> OUT
starter 遍历 Spring 暴露的每个属性,选出 otel.* 键,然后把组装后的整张映射一次性交给 Jackson,而不是逐个元素传递。图中的菱形就是本文讨论的衔接处:列表索引之下的键,需要额外处理。下一阶段会解释它。
第二阶段:差点被 Spring 丢掉的环境变量
大多数 otel.* 环境变量的旅程很轻松,这个却不是:
OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT=http://collector:4318/v1/traces
它能够通过,是因为 starter 中有十六行代码专门按名称查找它。上图中的菱形就是这些代码发挥作用的位置。
第三阶段:两种替换器,同一种语法
application.yaml 与 SDK 的独立 YAML 都使用 ${...} 占位符。它们的含义接近,却不完全相同。Spring 可以轻松解析链式回退,例如 ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318}}/v1/traces:从外层占位符追踪到内层,先采用信号专用的覆盖值,再回退到通用值,最后回退到字面量。SDK 的替换器则只进行一遍非递归的正则表达式替换;同一表达式放在 otel-config.yaml 中就无法解析。
flowchart LR
subgraph STARTER["application.yaml — starter"]
direction TB
S1["${VAR:default}<br/>${VAR}"] --> S2[Spring resolver]
S2 --> S3["reads:<br/>env, sys props, args,<br/>profiles, all yaml"]
end
subgraph AGENT["otel-config.yaml — agent"]
direction TB
A1["${VAR:-default}<br/>${VAR}<br/>${env:VAR:-default}<br/>${sys:property:-default}"] --> A2[SDK resolver]
A2 --> A3["reads:<br/>env vars + system properties"]
end
STARTER --> MODEL[same SDK model]
AGENT --> MODEL
因此,Spring 原生的配置技巧——profile、命令行 --key=value、@Value 风格的外部化,乃至外部配置服务器——都能透明地用于 OTel 配置。starter 没有实现这些能力;实现者是 Spring 的解析器,starter 只是读取属性。
最大的实际影响是:在 starter 中,任何与 YAML 叶节点具有相同规范键路径的环境变量,都会自动覆盖它,无需额外装配。agent 的独立 YAML 无法做到这一点。在那里,必须提前在 YAML 中写入 ${VAR} 占位符,建立环境变量覆盖的通路,否则它不会起作用。
抵达终点:SDK 真正据以启动的已解析配置树
SDK 启动之前,每个 otel.* 值都已完成解析、宽松绑定与规范化,并与其他值合并成一张扁平映射。starter 将这张映射还原为 SDK 需要的树,交给 Jackson;Jackson 生成 SDK 启动时使用的 OpenTelemetryConfigurationModel。不论哪个值来自 YAML、环境变量、profile 覆盖配置,还是命令行参数,SDK 始终只看到解析后的结果。
flowchart TD
A[application.yaml] --> S
B[application-prod.yaml] --> S
C[env vars] --> S
D[JVM system properties] --> S
E[command-line args] --> S
S["Spring property loader<br/>+ ${VAR} resolution"] --> F["flat property map<br/>otel.foo.bar[0].baz = ..."]
F --> X["starter: EmbeddedConfigFile<br/>walks otel.* keys, un-flattens"]
X --> J[Jackson → OpenTelemetryConfigurationModel]
J --> SDK[SDK runtime]
Spring 掌管入口。SDK 看不到原始的 ${VAR}、profile 名称或属性文件,只在启动时一次性接收一棵完全解析好的树。
为什么“实验性”恰恰是现在尝试声明式配置的最佳理由
声明式配置是 OpenTelemetry 正在跨语言趋于统一的 schema,它尚未完成。Spring Boot starter 对它的支持被标记为实验性,正是因为还没有在足够多的真实应用中使用,尚不清楚哪些边角需要收紧。
作者将此视为一份邀请:趁 schema 还未冻结,把声明式配置放进真实的 application.yaml,看看哪里出现问题,是最能产生影响的时机。你遇到的阻力,将帮助塑造最终落地的 schema。
60 秒开始使用
已有两个起点:
- 已经有
application.properties?把它粘贴到文档页面的交互式转换器中,就会得到可直接放入application.yaml的 YAML。 - 从零开始?OpenTelemetry Ecosystem Explorer 可交互式生成声明式配置 YAML:选择 exporter、sampler 与 instrumentation,然后复制结果。新增的 Spring Boot starter 目标模式会将输出包在
otel:之下,并使用正确的distribution.spring_starter.*键。
细节与前提
- Spring Boot 3.5+ 必须进行依赖管理。Spring Boot 3.5 固定了自带的 OpenTelemetry 版本,与 starter 所需版本冲突。应在
dependencyManagement中导入 OTel instrumentation BOM,详见文档。跳过这一步,启动时会出现NoClassDefFoundError: io/opentelemetry/common/ComponentLoader。 - 时长以毫秒数字表示。使用
5000,不要使用5s。 - 编程式定制的形式发生变化。
AutoConfigurationCustomizerProvider由DeclarativeConfigurationCustomizerProvider取代;SDK 组件通过ComponentProviderAPI 接入。agent 扩展 API 文档无需修改,也适用于 starter。
如果把真实应用迁移到这种配置后遇到异常,请提交 issue:代码问题提交到 opentelemetry-java-instrumentation,文档问题提交到 opentelemetry.io。











暂无评论内容