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 章节,输出都来自本机实测。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。