Browse Source
* gh-34508: Polish "Add auto-configuration for OTLP span exporter" Add auto-configuration for OTLP span exporter Closes gh-34508pull/35090/head
8 changed files with 431 additions and 1 deletions
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
/* |
||||
* 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.exporter.otlp.trace.OtlpGrpcSpanExporter; |
||||
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: <a href= |
||||
* "https://github.com/open-telemetry/opentelemetry-java/issues/3651">opentelemetry-java#3651</a>. |
||||
* 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, |
||||
* define an {@link OtlpGrpcSpanExporter} and this auto-configuration will back off. |
||||
* |
||||
* @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(value = OtlpHttpSpanExporter.class, |
||||
type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter") |
||||
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) { |
||||
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() |
||||
.setEndpoint(properties.getEndpoint()) |
||||
.setTimeout(properties.getTimeout()) |
||||
.setCompression(properties.getCompression().name().toLowerCase()); |
||||
for (Entry<String, String> header : properties.getHeaders().entrySet()) { |
||||
builder.addHeader(header.getKey(), header.getValue()); |
||||
} |
||||
return builder.build(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,103 @@
@@ -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 exporting traces using OTLP. |
||||
* |
||||
* @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<String, String> 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<String, String> getHeaders() { |
||||
return this.headers; |
||||
} |
||||
|
||||
public void setHeaders(Map<String, String> headers) { |
||||
this.headers = headers; |
||||
} |
||||
|
||||
enum Compression { |
||||
|
||||
/** |
||||
* Gzip compression. |
||||
*/ |
||||
GZIP, |
||||
|
||||
/** |
||||
* No compression. |
||||
*/ |
||||
NONE |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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; |
||||
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
/* |
||||
* 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 final MockWebServer mockWebServer = new MockWebServer(); |
||||
|
||||
@BeforeEach |
||||
void setUp() throws IOException { |
||||
this.mockWebServer.start(); |
||||
} |
||||
|
||||
@AfterEach |
||||
void tearDown() throws IOException { |
||||
this.mockWebServer.close(); |
||||
} |
||||
|
||||
@Test |
||||
void httpSpanExporterShouldUseProtoBufAndNoCompressionByDefault() { |
||||
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 httpSpanExporterCanBeConfiguredToUseGzipCompression() { |
||||
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"); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
|
||||
/* |
||||
* 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 shouldBackOffWhenCustomHttpExporterIsDefined() { |
||||
this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class) |
||||
.run((context) -> assertThat(context).hasBean("customOtlpHttpSpanExporter") |
||||
.hasSingleBean(SpanExporter.class)); |
||||
} |
||||
|
||||
@Test |
||||
void shouldBackOffWhenCustomGrpcExporterIsDefined() { |
||||
this.contextRunner.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(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue