Spring Boot 4 的 spring-boot-starter-opentelemetry 不用挂 Java agent,就能把 trace 和指标通过 OTLP 推给 Collector,日志再加一个 Logback appender 也能推。我在 Boot 4.1.1(OpenTelemetry SDK 1.62.0、Micrometer 1.17.1、JDK 21)上接了一个本地 otel/opentelemetry-collector-contrib:0.163.1,用它的 debug exporter 看到了 3 个请求产生的 12 个 span(每个请求 4 个)、61 个指标和带 trace ID 的日志。但默认配置下 trace 只采样 10%,trace 的地址也不会自己填,只补地址时发了 20 个请求,Collector 一个 span 都没收到。
OpenTelemetry Java agent 那条路线,之前那篇讲 agent 2.32.0 当 3.0 预览的文章已经聊过,这里只看 Spring 自己这套。
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
它带进来的关键几样:
[INFO] +- org.springframework.boot:spring-boot-starter-opentelemetry:jar:4.1.1:compile
[INFO] | +- org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry:jar:4.1.1:compile
[INFO] | +- org.springframework.boot:spring-boot-opentelemetry:jar:4.1.1:compile
[INFO] | | \- io.opentelemetry:opentelemetry-sdk:jar:1.62.0:compile
[INFO] | +- io.micrometer:micrometer-registry-otlp:jar:1.17.1:runtime
[INFO] | +- io.micrometer:micrometer-tracing-bridge-otel:jar:1.7.1:runtime
[INFO] | \- io.opentelemetry:opentelemetry-exporter-otlp:jar:1.62.0:runtime
所以 trace 走的是 Micrometer Tracing 桥接到 OpenTelemetry SDK,指标走 Micrometer 的 OtlpMeterRegistry。Boot 文档里也说得直白:Spring 官方支持的 OpenTelemetry 是“Micrometer 加 OTLP exporter”这一套,指标并不经过 OpenTelemetry 的 metrics SDK。
演示里还加了 spring-boot-starter-webmvc、spring-boot-starter-actuator,以及 spring-boot-starter-restclient。最后这个是因为我在接口里用 RestClient 调了自己一次,好让 trace 里有客户端 span。Boot 4 拆了模块,不加它启动就报找不到 RestClient$Builder。
接口很简单,/orders/{id} 打一行日志,调 /prices/{id},再包一个自定义 Observation:
@GetMapping("/orders/{id}")
String order(@PathVariable String id) {
log.info("loading order {}", id);
String price = restClient.get().uri("/prices/{id}", id).retrieve().body(String.class);
return Observation.createNotStarted("order.render", registry)
.lowCardinalityKeyValue("order.type", "standard")
.observe(() -> "order " + id + " costs " + price);
}
Collector 只开 OTLP 接收和 debug exporter,三条 pipeline 都接上:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
exporters: [debug]
metrics:
receivers: [otlp]
exporters: [debug]
logs:
receivers: [otlp]
exporters: [debug]
把上面的配置存成 config.yaml,在同一目录下启动 Collector:
docker run -d --name otel-collector \
-p 4317:4317 -p 4318:4318 \
-v "$(pwd)/config.yaml:/etc/otelcol-contrib/config.yaml:ro" \
otel/opentelemetry-collector-contrib:0.163.1
之后用 docker logs otel-collector 就能看到 debug exporter 打印的内容。
什么都不配会怎样
只加依赖、不写任何配置,发 20 个请求。应用启动日志里有这么一行:
2026-10-17T08:14:09.807+08:00 INFO 1133652 --- [ main] [ ] i.m.c.instrument.push.PushMeterRegistry : Publishing metrics for OtlpMeterRegistry every 1m to http://localhost:4318/v1/metrics with resource attributes {service.name=unknown_service}
指标默认就往 http://localhost:4318/v1/metrics 推,每分钟一次。Collector 那边只收到了指标,trace 和日志一条没有。原因在配置元数据里:management.opentelemetry.tracing.export.otlp.endpoint 和 management.opentelemetry.logging.export.otlp.endpoint 都没有默认值,不填就不会创建 exporter。另外 service.name 是 unknown_service,因为我还没设 spring.application.name。
只填地址,采样还是 10%
把 trace 和日志的地址补上:
management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4318/v1/traces
management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4318/v1/logs
再发 20 个请求,接口都正常返回,最后一个是:
order A-20 costs 42.00
Collector 收到了日志和指标,trace 还是零。
应用日志里每个请求其实都带着 trace ID:
2026-10-17T08:14:36.487+08:00 INFO 1134422 --- [nio-8080-exec-1] [ba6c21ba8c336ace3128c25ca87cbdf3-a02152833707109b] com.example.otel.OrderController : loading order A-1
日志里有 trace ID 不代表这个 trace 被采样了。management.tracing.sampling.probability 默认是 0.1,management.opentelemetry.tracing.sampler 默认是 parent-based-trace-id-ratio。20 个请求按 10% 抽样,这一轮刚好一个都没抽中。开发环境把它调到 1.0:
management.tracing.sampling.probability=1.0
全部配好以后 Collector 看到的东西
最后用的配置:
spring.application.name=order-service
management.tracing.sampling.probability=1.0
management.otlp.metrics.export.step=10s
management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4318/v1/traces
management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4318/v1/logs
发 3 个请求,Collector 的汇总日志:
2026-10-17T00:16:07.157Z info Traces {"resource": {"service.instance.id": "2ece1511-61f9-4294-9efa-b84a846e262b", "service.name": "otelcol-contrib", "service.version": "0.163.1"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "traces", "resource spans": 12, "spans": 12}
2026-10-17T00:16:11.361Z info Metrics {"resource": {"service.instance.id": "2ece1511-61f9-4294-9efa-b84a846e262b", "service.name": "otelcol-contrib", "service.version": "0.163.1"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics", "resource metrics": 1, "metrics": 61, "data points": 99}
每个请求 4 个 span。拿 trace 480b0d544076b1ba846c7aa3323ade4f 来看,根是服务端的 /orders/{id}:
Trace ID : 480b0d544076b1ba846c7aa3323ade4f
Parent ID :
ID : c6d3bd1f378c97a1
Name : http get /orders/{id}
Kind : Server
它下面是 RestClient 发出的客户端 span,以及我自己包的 order.render:
Trace ID : 480b0d544076b1ba846c7aa3323ade4f
Parent ID : c6d3bd1f378c97a1
ID : 2b8925ba7b5c0d89
Name : http get
Kind : Client
Trace ID : 480b0d544076b1ba846c7aa3323ade4f
Parent ID : c6d3bd1f378c97a1
ID : 8016713ab6515797
Name : order.render
Kind : Internal
/prices/{id} 的服务端 span 挂在客户端 span 下面,说明 traceparent 头已经自动传过去了:
Trace ID : 480b0d544076b1ba846c7aa3323ade4f
Parent ID : 2b8925ba7b5c0d89
ID : 85ae7dc8fc89d146
Name : http get /prices/{id}
Kind : Server
span 上的属性是 Micrometer 的命名风格:
-> exception: Str(none)
-> http.url: Str(/prices/A-3)
-> method: Str(GET)
-> outcome: Str(SUCCESS)
-> status: Str(200)
-> uri: Str(/prices/{id})
指标名也是 Micrometer 那一套,http.server.requests、http.client.requests、jvm.memory.used、logback.events、tomcat.sessions.active.current,自定义的 Observation 还会多出 order.render 和 order.render.active。http.server.requests 是直方图,单位毫秒,而且自动带上了 exemplar,能从指标直接跳到 trace:
-> Name: http.server.requests
-> Description:
-> Unit: milliseconds
-> DataType: Histogram
-> AggregationTemporality: Cumulative
Count: 3
Sum: 125.633036
Exemplars:
Exemplar #0
-> Trace ID: 480b0d544076b1ba846c7aa3323ade4f
-> Span ID: c6d3bd1f378c97a1
日志要自己接 appender
Boot 会配好 SdkLoggerProvider 和 OTLP 日志 exporter,但不会把 Logback 的日志接进去。我加了 OpenTelemetry 官方的 opentelemetry-logback-appender-1.0(2.32.0-alpha),在 logback-spring.xml 里挂上 OpenTelemetryAppender,再在启动时把 Boot 的 OpenTelemetry bean 交给它:
@Component
class OtelLogbackInstaller implements InitializingBean {
private final OpenTelemetry openTelemetry;
OtelLogbackInstaller(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@Override
public void afterPropertiesSet() {
OpenTelemetryAppender.install(this.openTelemetry);
}
}
Collector 里的日志记录带着 trace ID 和 span ID,和上面那个 trace 对得上:
Body: Str(loading order A-1)
Trace ID: 480b0d544076b1ba846c7aa3323ade4f
Span ID: c6d3bd1f378c97a1
这一步我卡了一阵。appender 2.32.0-alpha 传递依赖了 opentelemetry-api-incubator 1.66.0-alpha,而 Boot 管理的 OpenTelemetry API 是 1.62.0:
[INFO] | \- io.opentelemetry:opentelemetry-api-incubator:jar:1.66.0-alpha:runtime
版本对不上的时候,应用照常启动,接口也正常返回,只有日志批处理线程在后台抛了一个异常,Collector 一条日志都收不到:
Exception in thread "BatchLogRecordProcessor_WorkerThread-1" java.lang.NoClassDefFoundError: io/opentelemetry/api/incubator/common/ExtendedAttributes
Caused by: java.lang.ClassNotFoundException: io.opentelemetry.api.incubator.common.ExtendedAttributes
这个异常不走 Logback,混在启动日志里很容易漏看。把 incubator 钉到和 SDK 一致的 1.62.0-alpha 之后才正常:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api-incubator</artifactId>
<version>1.62.0-alpha</version>
</dependency>
老属性名会被悄悄忽略
Boot 3 时代 trace 地址是 management.otlp.tracing.endpoint。在 4.1.1 的配置元数据里它被标成 error 级弃用,替代项是 management.opentelemetry.tracing.export.otlp.endpoint,日志那边的 management.otlp.logging.endpoint 同理。我用老 key 加上 probability=1.0 跑了 5 个请求,应用日志里没有一条 WARN,Collector 只收到了指标,trace 是零。
和 Java agent 怎么选
Boot 文档开头就讲了,OpenTelemetry 有好几种接法,agent 和 OTel 社区的 Spring Boot starter 用的是 OTel 的语义约定,Boot 自己这套用的是 Spring 项目文档里定义的约定。上面看到的 method、uri、outcome 属性和 http.server.requests 指标名就是这个区别的直接体现。已经按 OTel 语义约定做好看板的团队,换过来要改查询。
反过来,starter 方案不改启动参数,配置全走 management.*,版本跟着 Boot 的依赖管理走。插桩范围取决于 Spring 自己的 Observation 支持,Spring 生态之外的库要靠它们自带的 Micrometer 埋点,agent 那种字节码层面的广覆盖它给不了。
按 Boot 文档,Docker Compose 支持会识别 otel/opentelemetry-collector-contrib 和 grafana/otel-lgtm 镜像,Testcontainers 的 @ServiceConnection 支持 otel/opentelemetry-collector-contrib 镜像和 LgtmStackContainer,都能自动填好 OTLP 地址。
参考
文中的配置项名称和默认值以 Spring Boot 4.1.1 jar 里的配置元数据为准,行为说明对照了 Boot 参考文档的 Observability、Tracing、Loggers、Development-time Services 和 Testcontainers 章节,输出都来自本机实测。
一起交流
分享你的思考,让讨论更进一步。