diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle
index 08a5bc58d6a5..d0ea4aa428fc 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle
@@ -70,6 +70,7 @@ dependencies {
optional("io.micrometer:micrometer-registry-wavefront")
optional("io.zipkin.reporter2:zipkin-sender-urlconnection")
optional("io.opentelemetry:opentelemetry-exporter-zipkin")
+ optional("io.opentelemetry:opentelemetry-exporter-otlp")
optional("io.projectreactor.netty:reactor-netty-http")
optional("io.r2dbc:r2dbc-pool")
optional("io.r2dbc:r2dbc-spi")
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java
new file mode 100644
index 000000000000..ac27a4c58e81
--- /dev/null
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2012-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
+
+import java.util.Map.Entry;
+
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+
+import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for OTLP. Brave does not support
+ * OTLP, so we only configure it for OpenTelemetry. OTLP defines three transports that are
+ * supported: gRPC (/protobuf), HTTP/protobuf, HTTP/JSON. From these transports HTTP/JSON
+ * is not supported by the OTel Java SDK, and it seems there are no plans supporting it in
+ * the future, see: opentelemetry-java#3651.
+ * Because this class configures components from the OTel SDK, it can't support HTTP/JSON.
+ * To keep things simple, we only auto-configure HTTP/protobuf. If you want to use gRPC,
+ * please disable this auto-configuration and create a bean.
+ *
+ * @author Jonatan Ivanov
+ * @since 3.1.0
+ */
+@AutoConfiguration
+@ConditionalOnEnabledTracing
+@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class })
+@EnableConfigurationProperties(OtlpProperties.class)
+public class OtlpAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) {
+ OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
+ .setEndpoint(properties.getEndpoint())
+ .setTimeout(properties.getTimeout())
+ .setCompression(properties.getCompression().name().toLowerCase());
+
+ for (Entry header : properties.getHeaders().entrySet()) {
+ builder.addHeader(header.getKey(), header.getValue());
+ }
+
+ return builder.build();
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java
new file mode 100644
index 000000000000..f767ec44c0fa
--- /dev/null
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2012-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for {@link OtlpAutoConfiguration}.
+ *
+ * @author Jonatan Ivanov
+ * @since 3.1.0
+ */
+@ConfigurationProperties("management.otlp.tracing")
+public class OtlpProperties {
+
+ /**
+ * URL to the OTel collector's HTTP API.
+ */
+ private String endpoint = "http://localhost:4318/v1/traces";
+
+ /**
+ * Call timeout for the OTel Collector to process an exported batch of data. This
+ * timeout spans the entire call: resolving DNS, connecting, writing the request body,
+ * server processing, and reading the response body. If the call requires redirects or
+ * retries all must complete within one timeout period.
+ */
+ private Duration timeout = Duration.ofSeconds(10);
+
+ /**
+ * The method used to compress the payload.
+ */
+ private Compression compression = Compression.NONE;
+
+ /**
+ * Custom HTTP headers you want to pass to the collector, for example auth headers.
+ */
+ private Map headers = new HashMap<>();
+
+ public String getEndpoint() {
+ return this.endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public Duration getTimeout() {
+ return this.timeout;
+ }
+
+ public void setTimeout(Duration timeout) {
+ this.timeout = timeout;
+ }
+
+ public Compression getCompression() {
+ return this.compression;
+ }
+
+ public void setCompression(Compression compression) {
+ this.compression = compression;
+ }
+
+ public Map getHeaders() {
+ return this.headers;
+ }
+
+ public void setHeaders(Map headers) {
+ this.headers = headers;
+ }
+
+ enum Compression {
+
+ /**
+ * Gzip compression.
+ */
+ GZIP,
+
+ /**
+ * No compression.
+ */
+ NONE
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/package-info.java
new file mode 100644
index 000000000000..b021e3da9fca
--- /dev/null
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Auto-configuration for tracing with OTLP.
+ */
+package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 7518c97da65c..affbe7607f9e 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -101,6 +101,7 @@ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributor
org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration
+org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java
new file mode 100644
index 000000000000..c43efa049407
--- /dev/null
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+import io.micrometer.tracing.Tracer;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okio.Buffer;
+import okio.GzipSource;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link OtlpAutoConfiguration}.
+ *
+ * @author Jonatan Ivanov
+ */
+class OtlpAutoConfigurationIntegrationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("management.tracing.sampling.probability=1.0")
+ .withConfiguration(
+ AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class,
+ OpenTelemetryAutoConfiguration.class, OtlpAutoConfiguration.class));
+
+ private MockWebServer mockWebServer;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ this.mockWebServer = new MockWebServer();
+ this.mockWebServer.start();
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ this.mockWebServer.close();
+ }
+
+ @Test
+ void httpSpanExporterShouldUseProtoBufAndNoCompression() {
+ this.mockWebServer.enqueue(new MockResponse());
+ this.contextRunner
+ .withPropertyValues("management.otlp.tracing.endpoint=http://localhost:%d/v1/traces"
+ .formatted(this.mockWebServer.getPort()), "management.otlp.tracing.headers.custom=42")
+ .run((context) -> {
+ context.getBean(Tracer.class).nextSpan().name("test").end();
+ assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
+ .isSameAs(CompletableResultCode.ofSuccess());
+
+ RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
+ assertThat(request).isNotNull();
+ assertThat(request.getRequestLine()).contains("/v1/traces");
+ assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
+ assertThat(request.getHeader("custom")).isEqualTo("42");
+ assertThat(request.getBodySize()).isPositive();
+ try (Buffer body = request.getBody()) {
+ assertThat(body.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
+ }
+ });
+ }
+
+ @Test
+ void httpSpanExporterShouldUseProtoBufAndGzip() {
+ this.mockWebServer.enqueue(new MockResponse());
+ this.contextRunner
+ .withPropertyValues("management.otlp.tracing.compression=GZIP",
+ "management.otlp.tracing.endpoint=http://localhost:%d/test".formatted(this.mockWebServer.getPort()))
+ .run((context) -> {
+ assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class).hasSingleBean(SpanExporter.class);
+ context.getBean(Tracer.class).nextSpan().name("test").end();
+ assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
+ .isSameAs(CompletableResultCode.ofSuccess());
+
+ RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
+ assertThat(request).isNotNull();
+ assertThat(request.getRequestLine()).contains("/test");
+ assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
+ assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip");
+ assertThat(request.getBodySize()).isPositive();
+ try (Buffer unCompressed = new Buffer(); Buffer body = request.getBody()) {
+ unCompressed.writeAll(new GzipSource(body));
+ assertThat(unCompressed.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
+ }
+ });
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java
new file mode 100644
index 000000000000..ede4f321c41f
--- /dev/null
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2012-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
+
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link OtlpAutoConfiguration}.
+ *
+ * @author Jonatan Ivanov
+ */
+class OtlpAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(OtlpAutoConfiguration.class));
+
+ @Test
+ void shouldSupplyBeans() {
+ this.contextRunner.run((context) -> assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class)
+ .hasSingleBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldNotSupplyBeansIfTracingIsDisabled() {
+ this.contextRunner.withPropertyValues("management.tracing.enabled=false")
+ .run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldNotSupplyBeansIfTracingBridgeIsMissing() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing"))
+ .run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldNotSupplyBeansIfOtelSdkIsMissing() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.sdk"))
+ .run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldNotSupplyBeansIfOtelApiIsMissing() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.api"))
+ .run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldNotSupplyBeansIfExporterIsMissing() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.exporter"))
+ .run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldSupplyCustomHttpExporter() {
+ this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class)
+ .run((context) -> assertThat(context).hasBean("customOtlpHttpSpanExporter")
+ .hasSingleBean(SpanExporter.class));
+ }
+
+ @Test
+ void shouldSupplyCustomGrpcExporter() {
+ this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.exporter"))
+ .withUserConfiguration(CustomGrpcExporterConfiguration.class)
+ .run((context) -> assertThat(context).hasBean("customOtlpGrpcSpanExporter")
+ .hasSingleBean(SpanExporter.class));
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ private static class CustomHttpExporterConfiguration {
+
+ @Bean
+ OtlpHttpSpanExporter customOtlpHttpSpanExporter() {
+ return OtlpHttpSpanExporter.builder().build();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ private static class CustomGrpcExporterConfiguration {
+
+ @Bean
+ OtlpGrpcSpanExporter customOtlpGrpcSpanExporter() {
+ return OtlpGrpcSpanExporter.builder().build();
+ }
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc
index 8650a9b93d0f..ad5b9e72edcd 100644
--- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc
+++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc
@@ -10,7 +10,7 @@ TIP: To learn more about Micrometer Tracing capabilities, see its https://microm
=== Supported Tracers
Spring Boot ships auto-configuration for the following tracers:
-* https://opentelemetry.io/[OpenTelemetry] with https://zipkin.io/[Zipkin] or https://docs.wavefront.com/[Wavefront]
+* https://opentelemetry.io/[OpenTelemetry] with https://zipkin.io/[Zipkin], https://docs.wavefront.com/[Wavefront], or https://opentelemetry.io/docs/reference/specification/protocol/[OTLP]
* https://github.com/openzipkin/brave[OpenZipkin Brave] with https://zipkin.io/[Zipkin] or https://docs.wavefront.com/[Wavefront]
@@ -90,6 +90,14 @@ All tracer implementations need the `org.springframework.boot:spring-boot-starte
+[[actuator.micrometer-tracing.tracer-implementations.otel-otlp]]
+==== OpenTelemetry With OTLP
+
+* `io.micrometer:micrometer-tracing-bridge-otel` - which is needed to bridge the Micrometer Observation API to OpenTelemetry.
+* `io.opentelemetry:opentelemetry-exporter-otlp` - which is needed to report traces to a collector that can accept OTLP.
+
+
+
[[actuator.micrometer-tracing.tracer-implementations.brave-zipkin]]
==== OpenZipkin Brave With Zipkin