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