如果要把 Spring Boot 的日志送到 OpenTelemetry,Logback 走 OTLP 是一条常用路。官方提供了 opentelemetry-logback-appender,但一直是 α 版本。α 期间不要指望 Spring Boot 给这个 Appender 做自动配置(OpenTelemetry 对象本身会自动配置,Appender 不会)。结果就是每个项目自己写一份 logback-spring.xml,还得盯着 SDK 版本。
我这边用的是 otel-logs-autoconfigure。加上依赖,在 application.properties 里写 OTLP 地址就行。支持 Spring Boot 3.5 和 4,0.6.0 之后 Log4j2 也能用。
接收端先用 Lognroll 确认 OTLP 能打出去。它自己也写了:hobby,SQLite,不适合要可靠性和吞吐的环境。生产换别的实现,应用这边的 endpoint 不用改思路。
OTLP 后端(本地:Lognroll)
docker run --rm -p 4318:4318 ghcr.io/categolj/lognroll:native
打开 http://localhost:4318,用户名留空,密码 changeme。应用要把日志发到 http://localhost:4318/v1/logs 。Apple Silicon 上官方 native 镜像可能起不来,改用 JVM 版镜像。

Spring Boot 4+
从 start.spring.io 建项目时把 opentelemetry 选上,Java 指定 25:
curl -s https://start.spring.io/starter.tgz \
-d artifactId=demo-otel-logs \
-d name=demo-otel-logs \
-d baseDir=demo-otel-logs \
-d packageName=com.example \
-d javaVersion=25 \
-d dependencies=web,actuator,configuration-processor,prometheus,opentelemetry \
-d type=maven-project \
-d applicationName=DemoOtelLogsApplication | tar -xzvf -
cd demo-otel-logs
pom.xml 加上依赖,并把 OpenTelemetry SDK 升到 1.63.0。Boot 4.1.x 默认是 1.62.0,和 otel-logs-autoconfigure 0.7.0 带的 Appender 对不上,会报 ClassNotFoundException: ExtendedAttributeKey。
<properties>
<java.version>25</java.version>
<opentelemetry.version>1.63.0</opentelemetry.version>
</properties>
<dependency>
<groupId>am.ik.spring.opentelemetry</groupId>
<artifactId>otel-logs-autoconfigure-logback</artifactId>
<version>0.7.0</version>
</dependency>
src/main/resources/application.properties:
logging.level.web=debug
management.opentelemetry.instrumentation.logback-appender.capture-experimental-attributes=true
management.opentelemetry.instrumentation.logback-appender.capture-key-value-pair-attributes=true
management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4318/v1/logs
management.opentelemetry.logging.export.otlp.compression=gzip
management.opentelemetry.logging.export.otlp.headers.Authorization=Bearer changeme
management.otlp.metrics.export.enabled=false
Boot 4 自带 OpenTelemetry starter,不用再加 Micrometer Tracing 桥。
Spring Boot 3.5
3.5 没有 OpenTelemetry starter,创建时不要带 opentelemetry,版本钉在 3.5.10:
curl -s https://start.spring.io/starter.tgz \
-d artifactId=demo-otel-logs \
-d name=demo-otel-logs \
-d baseDir=demo-otel-logs \
-d packageName=com.example \
-d dependencies=web,actuator,configuration-processor,prometheus \
-d type=maven-project \
-d bootVersion=3.5.10 \
-d applicationName=DemoOtelLogsApplication | tar -xzvf -
cd demo-otel-logs
依赖两个:自动配置,以及把 Trace ID 写进日志的桥:
<dependency>
<groupId>am.ik.spring.opentelemetry</groupId>
<artifactId>otel-logs-autoconfigure-logback</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
这个 Appender 用的 OpenTelemetry Java SDK 比 Boot 3.5 BOM 里的新,不改版本启动会失败。在 pom.xml 的 <properties> 里覆盖:
<opentelemetry.version>1.63.0</opentelemetry.version>
OTLP 相关配置项的名字和 Boot 4 不一样:
logging.level.web=debug
management.opentelemetry.instrumentation.logback-appender.capture-experimental-attributes=true
management.opentelemetry.instrumentation.logback-appender.capture-key-value-pair-attributes=true
management.otlp.logging.endpoint=http://localhost:4318/v1/logs
management.otlp.logging.compression=gzip
management.otlp.logging.headers.Authorization=Bearer changeme
management.otlp.metrics.export.enabled=false
Boot 4 用 management.opentelemetry.logging.export.otlp.endpoint,3.5 用 management.otlp.logging.endpoint。其余压缩、Header 只是前缀跟着变。
跑起来看一眼
./mvnw clean package -DskipTests
java -jar target/demo-otel-logs-0.0.1-SNAPSHOT.jar
到 Lognroll 点 View Logs,能看到启动日志。再请求一次:
curl http://localhost:8080/actuator/health
刷新日志,这条请求会出现,trace_id 列有值就说明 Appender 和 Tracing 接上了。

其他 OTLP 实现
应用只负责把日志发到一个 OTLP HTTP 地址(Boot 4 是 management.opentelemetry.logging.export.otlp.endpoint)。换后端时改这个 URL 和鉴权即可。生产里不要让每个实例直连存储,前面加 OpenTelemetry Collector,负责重试、批量、压缩,再分发给真正的日志库。
常见接法:
- Grafana Loki:Grafana 栈里最顺。Loki 3 可以直接收 OTLP;也可以 Collector 收 OTLP,再用 Loki exporter 转进去。查询、看板走 Grafana。
- OpenSearch / Elasticsearch:已经有 ELK 的话,Collector 导出到 OpenSearch,按
service.name、host.name检索。 - SigNoz:日志、指标、Trace 想放一起、可以自建,底层 ClickHouse,原生 OTLP。
- 托管服务:Grafana Cloud、Datadog、Splunk、Elastic Cloud 都收 OTLP,把 endpoint 换成厂商给的
/v1/logs,Header 带 API Key。
Collector 最小配置大致是:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
exporters:
otlphttp/backend:
endpoint: http://loki:3100/otlp
tls:
insecure: true
service:
pipelines:
logs:
receivers: [otlp]
exporters: [otlphttp/backend]
Spring Boot 的 OTLP 地址改成 Collector,例如 http://otel-collector:4318/v1/logs。本地仍可用 Lognroll 验通;上线只换接收端。
多实例与主机名
otel-logs-autoconfigure 只负责把当前进程的 Logback 接到 OTLP,不管后面怎么存、怎么查。Lognroll 是单进程 + SQLite,多副本一起打进去会糊成一锅,也撑不住流量。
多实例要两件事:每条日志带上 host.name / service.instance.id,接收端能按这个过滤。
不要只写 ${HOSTNAME}。Linux/K8s 里它通常是 Pod 名;macOS 上这个环境变量经常是空的,Resource 里就看不到主机名。可靠一点用 InetAddress 兜底:
@Configuration
class OpenTelemetryHostResourceConfiguration {
@Bean
@ConditionalOnMissingBean
Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) {
Map<String, String> attributes = new LinkedHashMap<>(properties.getResourceAttributes());
String host = firstNonBlank(
attributes.get("host.name"),
environment.getProperty("HOSTNAME"),
environment.getProperty("COMPUTERNAME"),
localHostName());
attributes.put("host.name", host);
attributes.putIfAbsent("service.instance.id", host);
ResourceBuilder builder = Resource.builder();
new OpenTelemetryResourceAttributes(environment, attributes).applyTo(builder::put);
return Resource.getDefault().merge(builder.build());
}
// firstNonBlank / localHostName 略
}
application.properties 里只保留服务名即可:
spring.application.name=demo-otel-logs
management.opentelemetry.resource-attributes.service.name=${spring.application.name}
K8s 上仍可显式注入:
export OTEL_RESOURCE_ATTRIBUTES=host.name=$HOSTNAME,service.instance.id=$HOSTNAME
Lognroll 里验通时,Resource 应能看到 host.name。多副本查询要放到 Loki / OpenSearch 那边按这个字段过滤。
本文主要参考 Toshiaki Maki 的 Sending Logs from a Spring Boot App with Logback + OpenTelemetry (OTLP),Boot 4 / 3.5 的配置差、
otel-logs-autoconfigure和 Lognroll 的用法以原文为准。