Skip to content

Commit 6b50d67

Browse files
Peeters Tim EXTmhalbritter
authored andcommitted
Add auto-configuration for OTLP gRPC format when using tracing
See gh-41213
1 parent 1562372 commit 6b50d67

File tree

6 files changed

+183
-6
lines changed

6 files changed

+183
-6
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
@@ -161,6 +161,7 @@ dependencies {
161161
testImplementation("org.awaitility:awaitility")
162162
testImplementation("org.cache2k:cache2k-api")
163163
testImplementation("org.eclipse.jetty.ee10:jetty-ee10-webapp")
164+
testImplementation("org.eclipse.jetty.http2:jetty-http2-server")
164165
testImplementation("org.glassfish.jersey.ext:jersey-spring6")
165166
testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson")
166167
testImplementation("org.hamcrest:hamcrest")

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
* the future, see: <a href=
3737
* "https://github.com/open-telemetry/opentelemetry-java/issues/3651">opentelemetry-java#3651</a>.
3838
* Because this class configures components from the OTel SDK, it can't support HTTP/JSON.
39-
* To keep things simple, we only auto-configure HTTP/protobuf. If you want to use gRPC,
40-
* define an {@link OtlpGrpcSpanExporter} and this auto-configuration will back off.
39+
* By default, we auto-configure HTTP/protobuf. If you want to use gRPC, you need to set
40+
* {@code management.otlp.tracing.transport=grpc}. If you define a
41+
* {@link OtlpHttpSpanExporter} or {@link OtlpGrpcSpanExporter}, this auto-configuration
42+
* will back off.
4143
*
4244
* @author Jonatan Ivanov
4345
* @author Moritz Halbritter

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public class OtlpProperties {
4444
*/
4545
private Duration timeout = Duration.ofSeconds(10);
4646

47+
/**
48+
* Transport used to send the spans. Defaults to HTTP.
49+
*/
50+
private Transport transport = Transport.HTTP;
51+
4752
/**
4853
* Method used to compress the payload.
4954
*/
@@ -70,6 +75,14 @@ public void setTimeout(Duration timeout) {
7075
this.timeout = timeout;
7176
}
7277

78+
public Transport getTransport() {
79+
return this.transport;
80+
}
81+
82+
public void setTransport(Transport transport) {
83+
this.transport = transport;
84+
}
85+
7386
public Compression getCompression() {
7487
return this.compression;
7588
}
@@ -86,6 +99,20 @@ public void setHeaders(Map<String, String> headers) {
8699
this.headers = headers;
87100
}
88101

102+
enum Transport {
103+
104+
/**
105+
* HTTP exporter.
106+
*/
107+
HTTP,
108+
109+
/**
110+
* gRPC exporter.
111+
*/
112+
GRPC
113+
114+
}
115+
89116
enum Compression {
90117

91118
/**

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
2222
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
23+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
24+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
2325

2426
import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing;
2527
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -69,10 +71,11 @@ public String getUrl() {
6971
static class Exporters {
7072

7173
@Bean
72-
@ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class,
73-
type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter")
74+
@ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
7475
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
7576
@ConditionalOnEnabledTracing("otlp")
77+
@ConditionalOnProperty(prefix = "management.otlp.tracing", name = "transport", havingValue = "http",
78+
matchIfMissing = true)
7679
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties,
7780
OtlpTracingConnectionDetails connectionDetails) {
7881
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
@@ -85,6 +88,23 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties,
8588
return builder.build();
8689
}
8790

91+
@Bean
92+
@ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
93+
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
94+
@ConditionalOnEnabledTracing("otlp")
95+
@ConditionalOnProperty(prefix = "management.otlp.tracing", name = "transport", havingValue = "grpc")
96+
OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpProperties properties,
97+
OtlpTracingConnectionDetails connectionDetails) {
98+
OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder()
99+
.setEndpoint(connectionDetails.getUrl())
100+
.setTimeout(properties.getTimeout())
101+
.setCompression(properties.getCompression().name().toLowerCase());
102+
for (Entry<String, String> header : properties.getHeaders().entrySet()) {
103+
builder.addHeader(header.getKey(), header.getValue());
104+
}
105+
return builder.build();
106+
}
107+
88108
}
89109

90110
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,43 @@
1717
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
1818

1919
import java.io.IOException;
20+
import java.io.InputStream;
2021
import java.nio.charset.StandardCharsets;
22+
import java.util.concurrent.BlockingQueue;
23+
import java.util.concurrent.LinkedBlockingQueue;
2124
import java.util.concurrent.TimeUnit;
2225

2326
import io.micrometer.tracing.Tracer;
2427
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
28+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
2529
import io.opentelemetry.sdk.common.CompletableResultCode;
2630
import io.opentelemetry.sdk.trace.export.SpanExporter;
2731
import okhttp3.mockwebserver.MockResponse;
2832
import okhttp3.mockwebserver.MockWebServer;
2933
import okhttp3.mockwebserver.RecordedRequest;
3034
import okio.Buffer;
3135
import okio.GzipSource;
36+
import org.eclipse.jetty.http.HttpFields;
37+
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
38+
import org.eclipse.jetty.io.Content;
39+
import org.eclipse.jetty.server.Handler;
40+
import org.eclipse.jetty.server.HttpConfiguration;
41+
import org.eclipse.jetty.server.Request;
42+
import org.eclipse.jetty.server.Response;
43+
import org.eclipse.jetty.server.Server;
44+
import org.eclipse.jetty.server.ServerConnector;
45+
import org.eclipse.jetty.util.Callback;
3246
import org.junit.jupiter.api.AfterEach;
3347
import org.junit.jupiter.api.BeforeEach;
3448
import org.junit.jupiter.api.Test;
3549

3650
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
3751
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
3852
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
53+
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfigurationIntegrationTests.MockGrpcServer.RecordedGrpcRequest;
3954
import org.springframework.boot.autoconfigure.AutoConfigurations;
4055
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
56+
import org.springframework.util.StreamUtils;
4157

4258
import static org.assertj.core.api.Assertions.assertThat;
4359

@@ -57,16 +73,28 @@ class OtlpAutoConfigurationIntegrationTests {
5773

5874
private final MockWebServer mockWebServer = new MockWebServer();
5975

76+
private final MockGrpcServer mockGrpcServer = new MockGrpcServer();
77+
6078
@BeforeEach
61-
void setUp() throws IOException {
79+
void startMockWebServer() throws IOException {
6280
this.mockWebServer.start();
6381
}
6482

83+
@BeforeEach
84+
void startMockGrpcServer() throws Exception {
85+
this.mockGrpcServer.start();
86+
}
87+
6588
@AfterEach
66-
void tearDown() throws IOException {
89+
void stopMockWebServer() throws IOException {
6790
this.mockWebServer.close();
6891
}
6992

93+
@AfterEach
94+
void stopMockGrpcServer() throws Exception {
95+
this.mockGrpcServer.stop();
96+
}
97+
7098
@Test
7199
void httpSpanExporterShouldUseProtobufAndNoCompressionByDefault() {
72100
this.mockWebServer.enqueue(new MockResponse());
@@ -113,4 +141,88 @@ void httpSpanExporterCanBeConfiguredToUseGzipCompression() {
113141
});
114142
}
115143

144+
@Test
145+
void grpcSpanExporter() {
146+
this.contextRunner
147+
.withPropertyValues(
148+
"management.otlp.tracing.endpoint=http://localhost:%d".formatted(this.mockGrpcServer.getPort()),
149+
"management.otlp.tracing.headers.custom=42", "management.otlp.tracing.transport=grpc")
150+
.run((context) -> {
151+
context.getBean(Tracer.class).nextSpan().name("test").end();
152+
assertThat(context.getBean(OtlpGrpcSpanExporter.class).flush())
153+
.isSameAs(CompletableResultCode.ofSuccess());
154+
RecordedGrpcRequest request = this.mockGrpcServer.takeRequest(10, TimeUnit.SECONDS);
155+
assertThat(request).isNotNull();
156+
assertThat(request.headers().get("Content-Type")).isEqualTo("application/grpc");
157+
assertThat(request.headers().get("custom")).isEqualTo("42");
158+
assertThat(request.body()).contains("org.springframework.boot");
159+
});
160+
}
161+
162+
static class MockGrpcServer {
163+
164+
private final Server server = createServer();
165+
166+
private final BlockingQueue<RecordedGrpcRequest> recordedRequests = new LinkedBlockingQueue<>();
167+
168+
void start() throws Exception {
169+
this.server.start();
170+
}
171+
172+
void stop() throws Exception {
173+
this.server.stop();
174+
}
175+
176+
int getPort() {
177+
return this.server.getURI().getPort();
178+
}
179+
180+
RecordedGrpcRequest takeRequest(int timeout, TimeUnit unit) throws InterruptedException {
181+
return this.recordedRequests.poll(timeout, unit);
182+
}
183+
184+
void recordRequest(RecordedGrpcRequest request) {
185+
this.recordedRequests.add(request);
186+
}
187+
188+
private Server createServer() {
189+
Server server = new Server();
190+
server.addConnector(createConnector(server));
191+
server.setHandler(new GrpcHandler());
192+
193+
return server;
194+
}
195+
196+
private ServerConnector createConnector(Server server) {
197+
ServerConnector connector = new ServerConnector(server,
198+
new HTTP2CServerConnectionFactory(new HttpConfiguration()));
199+
connector.setPort(0);
200+
201+
return connector;
202+
}
203+
204+
class GrpcHandler extends Handler.Abstract {
205+
206+
@Override
207+
public boolean handle(Request request, Response response, Callback callback) throws Exception {
208+
try (InputStream in = Content.Source.asInputStream(request)) {
209+
recordRequest(new RecordedGrpcRequest(request.getHeaders(),
210+
StreamUtils.copyToString(in, StandardCharsets.UTF_8)));
211+
}
212+
213+
response.getHeaders().add("Content-Type", "application/grpc");
214+
response.getHeaders().add("Grpc-Status", "0");
215+
216+
callback.succeeded();
217+
218+
return true;
219+
}
220+
221+
}
222+
223+
record RecordedGrpcRequest(HttpFields headers, String body) {
224+
}
225+
226+
}
227+
116228
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,28 @@ void shouldNotSupplyBeansIfPropertyIsNotSet() {
5151
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class));
5252
}
5353

54+
@Test
55+
void shouldNotSupplyBeansIfGrpcTransportIsEnabledButPropertyIsNotSet() {
56+
this.contextRunner.withPropertyValues("management.otlp.tracing.transport=grpc")
57+
.run((context) -> assertThat(context).doesNotHaveBean(OtlpGrpcSpanExporter.class));
58+
}
59+
5460
@Test
5561
void shouldSupplyBeans() {
5662
this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
5763
.run((context) -> assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class)
5864
.hasSingleBean(SpanExporter.class));
5965
}
6066

67+
@Test
68+
void shouldSupplyBeansIfGrpcTransportIsEnabled() {
69+
this.contextRunner
70+
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces",
71+
"management.otlp.tracing.transport=grpc")
72+
.run((context) -> assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class)
73+
.hasSingleBean(SpanExporter.class));
74+
}
75+
6176
@Test
6277
void shouldNotSupplyBeansIfGlobalTracingIsDisabled() {
6378
this.contextRunner.withPropertyValues("management.tracing.enabled=false")

0 commit comments

Comments
 (0)