Skip to content

Commit e33b592

Browse files
committed
Merge pull request #34508 from jonatan-ivanov
* gh-34508: Polish "Add auto-configuration for OTLP span exporter" Add auto-configuration for OTLP span exporter Closes gh-34508
2 parents d4f8576 + c543d91 commit e33b592

File tree

8 files changed

+431
-1
lines changed

8 files changed

+431
-1
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ dependencies {
7070
optional("io.micrometer:micrometer-registry-wavefront")
7171
optional("io.zipkin.reporter2:zipkin-sender-urlconnection")
7272
optional("io.opentelemetry:opentelemetry-exporter-zipkin")
73+
optional("io.opentelemetry:opentelemetry-exporter-otlp")
7374
optional("io.projectreactor.netty:reactor-netty-http")
7475
optional("io.r2dbc:r2dbc-pool")
7576
optional("io.r2dbc:r2dbc-spi")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
18+
19+
import java.util.Map.Entry;
20+
21+
import io.micrometer.tracing.otel.bridge.OtelTracer;
22+
import io.opentelemetry.api.OpenTelemetry;
23+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
24+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
25+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
26+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
27+
28+
import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing;
29+
import org.springframework.boot.autoconfigure.AutoConfiguration;
30+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
33+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
34+
import org.springframework.context.annotation.Bean;
35+
36+
/**
37+
* {@link EnableAutoConfiguration Auto-configuration} for OTLP. Brave does not support
38+
* OTLP, so we only configure it for OpenTelemetry. OTLP defines three transports that are
39+
* supported: gRPC (/protobuf), HTTP/protobuf, HTTP/JSON. From these transports HTTP/JSON
40+
* is not supported by the OTel Java SDK, and it seems there are no plans supporting it in
41+
* the future, see: <a href=
42+
* "https://github.com/open-telemetry/opentelemetry-java/issues/3651">opentelemetry-java#3651</a>.
43+
* Because this class configures components from the OTel SDK, it can't support HTTP/JSON.
44+
* To keep things simple, we only auto-configure HTTP/protobuf. If you want to use gRPC,
45+
* define an {@link OtlpGrpcSpanExporter} and this auto-configuration will back off.
46+
*
47+
* @author Jonatan Ivanov
48+
* @since 3.1.0
49+
*/
50+
@AutoConfiguration
51+
@ConditionalOnEnabledTracing
52+
@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class })
53+
@EnableConfigurationProperties(OtlpProperties.class)
54+
public class OtlpAutoConfiguration {
55+
56+
@Bean
57+
@ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class,
58+
type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter")
59+
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) {
60+
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
61+
.setEndpoint(properties.getEndpoint())
62+
.setTimeout(properties.getTimeout())
63+
.setCompression(properties.getCompression().name().toLowerCase());
64+
for (Entry<String, String> header : properties.getHeaders().entrySet()) {
65+
builder.addHeader(header.getKey(), header.getValue());
66+
}
67+
return builder.build();
68+
}
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
18+
19+
import java.time.Duration;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import org.springframework.boot.context.properties.ConfigurationProperties;
24+
25+
/**
26+
* Configuration properties for exporting traces using OTLP.
27+
*
28+
* @author Jonatan Ivanov
29+
* @since 3.1.0
30+
*/
31+
@ConfigurationProperties("management.otlp.tracing")
32+
public class OtlpProperties {
33+
34+
/**
35+
* URL to the OTel collector's HTTP API.
36+
*/
37+
private String endpoint = "http://localhost:4318/v1/traces";
38+
39+
/**
40+
* Call timeout for the OTel Collector to process an exported batch of data. This
41+
* timeout spans the entire call: resolving DNS, connecting, writing the request body,
42+
* server processing, and reading the response body. If the call requires redirects or
43+
* retries all must complete within one timeout period.
44+
*/
45+
private Duration timeout = Duration.ofSeconds(10);
46+
47+
/**
48+
* The method used to compress the payload.
49+
*/
50+
private Compression compression = Compression.NONE;
51+
52+
/**
53+
* Custom HTTP headers you want to pass to the collector, for example auth headers.
54+
*/
55+
private Map<String, String> headers = new HashMap<>();
56+
57+
public String getEndpoint() {
58+
return this.endpoint;
59+
}
60+
61+
public void setEndpoint(String endpoint) {
62+
this.endpoint = endpoint;
63+
}
64+
65+
public Duration getTimeout() {
66+
return this.timeout;
67+
}
68+
69+
public void setTimeout(Duration timeout) {
70+
this.timeout = timeout;
71+
}
72+
73+
public Compression getCompression() {
74+
return this.compression;
75+
}
76+
77+
public void setCompression(Compression compression) {
78+
this.compression = compression;
79+
}
80+
81+
public Map<String, String> getHeaders() {
82+
return this.headers;
83+
}
84+
85+
public void setHeaders(Map<String, String> headers) {
86+
this.headers = headers;
87+
}
88+
89+
enum Compression {
90+
91+
/**
92+
* Gzip compression.
93+
*/
94+
GZIP,
95+
96+
/**
97+
* No compression.
98+
*/
99+
NONE
100+
101+
}
102+
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Auto-configuration for tracing with OTLP.
19+
*/
20+
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributor
101101
org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
102102
org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration
103103
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration
104+
org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration
104105
org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration
105106
org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration
106107
org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
18+
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import io.micrometer.tracing.Tracer;
24+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
25+
import io.opentelemetry.sdk.common.CompletableResultCode;
26+
import io.opentelemetry.sdk.trace.export.SpanExporter;
27+
import okhttp3.mockwebserver.MockResponse;
28+
import okhttp3.mockwebserver.MockWebServer;
29+
import okhttp3.mockwebserver.RecordedRequest;
30+
import okio.Buffer;
31+
import okio.GzipSource;
32+
import org.junit.jupiter.api.AfterEach;
33+
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.api.Test;
35+
36+
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
37+
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
38+
import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration;
39+
import org.springframework.boot.autoconfigure.AutoConfigurations;
40+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
41+
42+
import static org.assertj.core.api.Assertions.assertThat;
43+
44+
/**
45+
* Integration tests for {@link OtlpAutoConfiguration}.
46+
*
47+
* @author Jonatan Ivanov
48+
*/
49+
class OtlpAutoConfigurationIntegrationTests {
50+
51+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
52+
.withPropertyValues("management.tracing.sampling.probability=1.0")
53+
.withConfiguration(
54+
AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class,
55+
OpenTelemetryAutoConfiguration.class, OtlpAutoConfiguration.class));
56+
57+
private final MockWebServer mockWebServer = new MockWebServer();
58+
59+
@BeforeEach
60+
void setUp() throws IOException {
61+
this.mockWebServer.start();
62+
}
63+
64+
@AfterEach
65+
void tearDown() throws IOException {
66+
this.mockWebServer.close();
67+
}
68+
69+
@Test
70+
void httpSpanExporterShouldUseProtoBufAndNoCompressionByDefault() {
71+
this.mockWebServer.enqueue(new MockResponse());
72+
this.contextRunner
73+
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:%d/v1/traces"
74+
.formatted(this.mockWebServer.getPort()), "management.otlp.tracing.headers.custom=42")
75+
.run((context) -> {
76+
context.getBean(Tracer.class).nextSpan().name("test").end();
77+
assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
78+
.isSameAs(CompletableResultCode.ofSuccess());
79+
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
80+
assertThat(request).isNotNull();
81+
assertThat(request.getRequestLine()).contains("/v1/traces");
82+
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
83+
assertThat(request.getHeader("custom")).isEqualTo("42");
84+
assertThat(request.getBodySize()).isPositive();
85+
try (Buffer body = request.getBody()) {
86+
assertThat(body.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
87+
}
88+
});
89+
}
90+
91+
@Test
92+
void httpSpanExporterCanBeConfiguredToUseGzipCompression() {
93+
this.mockWebServer.enqueue(new MockResponse());
94+
this.contextRunner
95+
.withPropertyValues("management.otlp.tracing.compression=gzip",
96+
"management.otlp.tracing.endpoint=http://localhost:%d/test".formatted(this.mockWebServer.getPort()))
97+
.run((context) -> {
98+
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class).hasSingleBean(SpanExporter.class);
99+
context.getBean(Tracer.class).nextSpan().name("test").end();
100+
assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
101+
.isSameAs(CompletableResultCode.ofSuccess());
102+
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
103+
assertThat(request).isNotNull();
104+
assertThat(request.getRequestLine()).contains("/test");
105+
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
106+
assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip");
107+
assertThat(request.getBodySize()).isPositive();
108+
try (Buffer unCompressed = new Buffer(); Buffer body = request.getBody()) {
109+
unCompressed.writeAll(new GzipSource(body));
110+
assertThat(unCompressed.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
111+
}
112+
});
113+
}
114+
115+
}

0 commit comments

Comments
 (0)