Skip to content

Commit b4017f0

Browse files
committed
Merge pull request #41213 from timpeeters
* pr/41213: Polish "Add auto-configuration for OTLP gRPC format when using tracing" Add auto-configuration for OTLP gRPC format when using tracing Closes gh-41213
2 parents 1562372 + abf923e commit b4017f0

File tree

7 files changed

+175
-10
lines changed

7 files changed

+175
-10
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: 28 additions & 1 deletion
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.
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,7 +99,21 @@ public void setHeaders(Map<String, String> headers) {
8699
this.headers = headers;
87100
}
88101

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

91118
/**
92119
* Gzip compression.

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

Lines changed: 21 additions & 4 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;
@@ -66,13 +68,14 @@ public String getUrl() {
6668
}
6769

6870
@Configuration(proxyBeanMethods = false)
71+
@ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
72+
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
73+
@ConditionalOnEnabledTracing("otlp")
6974
static class Exporters {
7075

7176
@Bean
72-
@ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class,
73-
type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter")
74-
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
75-
@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,20 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties,
8588
return builder.build();
8689
}
8790

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

90107
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,10 @@
21022102
"type": "java.lang.Boolean",
21032103
"description": "Whether auto-configuration of tracing is enabled to export OTLP traces."
21042104
},
2105+
{
2106+
"name": "management.otlp.tracing.transport",
2107+
"defaultValue": "http"
2108+
},
21052109
{
21062110
"name": "management.prometheus.metrics.export.histogram-flavor",
21072111
"defaultValue": "prometheus"

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

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,40 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
1818

19-
import java.io.IOException;
19+
import java.io.InputStream;
2020
import java.nio.charset.StandardCharsets;
21+
import java.util.concurrent.BlockingQueue;
22+
import java.util.concurrent.LinkedBlockingQueue;
2123
import java.util.concurrent.TimeUnit;
2224

2325
import io.micrometer.tracing.Tracer;
2426
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
27+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
2528
import io.opentelemetry.sdk.common.CompletableResultCode;
2629
import io.opentelemetry.sdk.trace.export.SpanExporter;
2730
import okhttp3.mockwebserver.MockResponse;
2831
import okhttp3.mockwebserver.MockWebServer;
2932
import okhttp3.mockwebserver.RecordedRequest;
3033
import okio.Buffer;
3134
import okio.GzipSource;
35+
import org.eclipse.jetty.http.HttpFields;
36+
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
37+
import org.eclipse.jetty.io.Content;
38+
import org.eclipse.jetty.server.Handler;
39+
import org.eclipse.jetty.server.HttpConfiguration;
40+
import org.eclipse.jetty.server.Request;
41+
import org.eclipse.jetty.server.Response;
42+
import org.eclipse.jetty.server.Server;
43+
import org.eclipse.jetty.server.ServerConnector;
44+
import org.eclipse.jetty.util.Callback;
3245
import org.junit.jupiter.api.AfterEach;
3346
import org.junit.jupiter.api.BeforeEach;
3447
import org.junit.jupiter.api.Test;
3548

3649
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
3750
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
3851
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
52+
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfigurationIntegrationTests.MockGrpcServer.RecordedGrpcRequest;
3953
import org.springframework.boot.autoconfigure.AutoConfigurations;
4054
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
4155

@@ -57,14 +71,18 @@ class OtlpAutoConfigurationIntegrationTests {
5771

5872
private final MockWebServer mockWebServer = new MockWebServer();
5973

74+
private final MockGrpcServer mockGrpcServer = new MockGrpcServer();
75+
6076
@BeforeEach
61-
void setUp() throws IOException {
77+
void startServers() throws Exception {
6278
this.mockWebServer.start();
79+
this.mockGrpcServer.start();
6380
}
6481

6582
@AfterEach
66-
void tearDown() throws IOException {
83+
void stopServers() throws Exception {
6784
this.mockWebServer.close();
85+
this.mockGrpcServer.close();
6886
}
6987

7088
@Test
@@ -113,4 +131,85 @@ void httpSpanExporterCanBeConfiguredToUseGzipCompression() {
113131
});
114132
}
115133

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

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)