Browse Source

Add auto-configuration for OTLP gRPC format when using tracing

See gh-41213
pull/41323/head
Peeters Tim EXT 2 years ago committed by Moritz Halbritter
parent
commit
6b50d6783b
  1. 1
      spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle
  2. 6
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java
  3. 27
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java
  4. 24
      spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java
  5. 116
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java
  6. 15
      spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java

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

@ -161,6 +161,7 @@ dependencies {
testImplementation("org.awaitility:awaitility") testImplementation("org.awaitility:awaitility")
testImplementation("org.cache2k:cache2k-api") testImplementation("org.cache2k:cache2k-api")
testImplementation("org.eclipse.jetty.ee10:jetty-ee10-webapp") testImplementation("org.eclipse.jetty.ee10:jetty-ee10-webapp")
testImplementation("org.eclipse.jetty.http2:jetty-http2-server")
testImplementation("org.glassfish.jersey.ext:jersey-spring6") testImplementation("org.glassfish.jersey.ext:jersey-spring6")
testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson") testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson")
testImplementation("org.hamcrest:hamcrest") testImplementation("org.hamcrest:hamcrest")

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

@ -36,8 +36,10 @@ import org.springframework.context.annotation.Import;
* the future, see: <a href= * the future, see: <a href=
* "https://github.com/open-telemetry/opentelemetry-java/issues/3651">opentelemetry-java#3651</a>. * "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. * 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, * By default, we auto-configure HTTP/protobuf. If you want to use gRPC, you need to set
* define an {@link OtlpGrpcSpanExporter} and this auto-configuration will back off. * {@code management.otlp.tracing.transport=grpc}. If you define a
* {@link OtlpHttpSpanExporter} or {@link OtlpGrpcSpanExporter}, this auto-configuration
* will back off.
* *
* @author Jonatan Ivanov * @author Jonatan Ivanov
* @author Moritz Halbritter * @author Moritz Halbritter

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

@ -44,6 +44,11 @@ public class OtlpProperties {
*/ */
private Duration timeout = Duration.ofSeconds(10); private Duration timeout = Duration.ofSeconds(10);
/**
* Transport used to send the spans. Defaults to HTTP.
*/
private Transport transport = Transport.HTTP;
/** /**
* Method used to compress the payload. * Method used to compress the payload.
*/ */
@ -70,6 +75,14 @@ public class OtlpProperties {
this.timeout = timeout; this.timeout = timeout;
} }
public Transport getTransport() {
return this.transport;
}
public void setTransport(Transport transport) {
this.transport = transport;
}
public Compression getCompression() { public Compression getCompression() {
return this.compression; return this.compression;
} }
@ -86,6 +99,20 @@ public class OtlpProperties {
this.headers = headers; this.headers = headers;
} }
enum Transport {
/**
* HTTP exporter.
*/
HTTP,
/**
* gRPC exporter.
*/
GRPC
}
enum Compression { enum Compression {
/** /**

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

@ -20,6 +20,8 @@ import java.util.Map.Entry;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@ -69,10 +71,11 @@ class OtlpTracingConfigurations {
static class Exporters { static class Exporters {
@Bean @Bean
@ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class, @ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter")
@ConditionalOnBean(OtlpTracingConnectionDetails.class) @ConditionalOnBean(OtlpTracingConnectionDetails.class)
@ConditionalOnEnabledTracing("otlp") @ConditionalOnEnabledTracing("otlp")
@ConditionalOnProperty(prefix = "management.otlp.tracing", name = "transport", havingValue = "http",
matchIfMissing = true)
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties, OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties,
OtlpTracingConnectionDetails connectionDetails) { OtlpTracingConnectionDetails connectionDetails) {
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
@ -85,6 +88,23 @@ class OtlpTracingConfigurations {
return builder.build(); return builder.build();
} }
@Bean
@ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
@ConditionalOnEnabledTracing("otlp")
@ConditionalOnProperty(prefix = "management.otlp.tracing", name = "transport", havingValue = "grpc")
OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpProperties properties,
OtlpTracingConnectionDetails connectionDetails) {
OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder()
.setEndpoint(connectionDetails.getUrl())
.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();
}
} }
} }

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

@ -17,11 +17,15 @@
package org.springframework.boot.actuate.autoconfigure.tracing.otlp; package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.micrometer.tracing.Tracer; import io.micrometer.tracing.Tracer;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.export.SpanExporter;
import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockResponse;
@ -29,6 +33,16 @@ import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest; import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer; import okio.Buffer;
import okio.GzipSource; import okio.GzipSource;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -36,8 +50,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfigurationIntegrationTests.MockGrpcServer.RecordedGrpcRequest;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -57,16 +73,28 @@ class OtlpAutoConfigurationIntegrationTests {
private final MockWebServer mockWebServer = new MockWebServer(); private final MockWebServer mockWebServer = new MockWebServer();
private final MockGrpcServer mockGrpcServer = new MockGrpcServer();
@BeforeEach @BeforeEach
void setUp() throws IOException { void startMockWebServer() throws IOException {
this.mockWebServer.start(); this.mockWebServer.start();
} }
@BeforeEach
void startMockGrpcServer() throws Exception {
this.mockGrpcServer.start();
}
@AfterEach @AfterEach
void tearDown() throws IOException { void stopMockWebServer() throws IOException {
this.mockWebServer.close(); this.mockWebServer.close();
} }
@AfterEach
void stopMockGrpcServer() throws Exception {
this.mockGrpcServer.stop();
}
@Test @Test
void httpSpanExporterShouldUseProtobufAndNoCompressionByDefault() { void httpSpanExporterShouldUseProtobufAndNoCompressionByDefault() {
this.mockWebServer.enqueue(new MockResponse()); this.mockWebServer.enqueue(new MockResponse());
@ -113,4 +141,88 @@ class OtlpAutoConfigurationIntegrationTests {
}); });
} }
@Test
void grpcSpanExporter() {
this.contextRunner
.withPropertyValues(
"management.otlp.tracing.endpoint=http://localhost:%d".formatted(this.mockGrpcServer.getPort()),
"management.otlp.tracing.headers.custom=42", "management.otlp.tracing.transport=grpc")
.run((context) -> {
context.getBean(Tracer.class).nextSpan().name("test").end();
assertThat(context.getBean(OtlpGrpcSpanExporter.class).flush())
.isSameAs(CompletableResultCode.ofSuccess());
RecordedGrpcRequest request = this.mockGrpcServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.headers().get("Content-Type")).isEqualTo("application/grpc");
assertThat(request.headers().get("custom")).isEqualTo("42");
assertThat(request.body()).contains("org.springframework.boot");
});
}
static class MockGrpcServer {
private final Server server = createServer();
private final BlockingQueue<RecordedGrpcRequest> recordedRequests = new LinkedBlockingQueue<>();
void start() throws Exception {
this.server.start();
}
void stop() throws Exception {
this.server.stop();
}
int getPort() {
return this.server.getURI().getPort();
}
RecordedGrpcRequest takeRequest(int timeout, TimeUnit unit) throws InterruptedException {
return this.recordedRequests.poll(timeout, unit);
}
void recordRequest(RecordedGrpcRequest request) {
this.recordedRequests.add(request);
}
private Server createServer() {
Server server = new Server();
server.addConnector(createConnector(server));
server.setHandler(new GrpcHandler());
return server;
}
private ServerConnector createConnector(Server server) {
ServerConnector connector = new ServerConnector(server,
new HTTP2CServerConnectionFactory(new HttpConfiguration()));
connector.setPort(0);
return connector;
}
class GrpcHandler extends Handler.Abstract {
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception {
try (InputStream in = Content.Source.asInputStream(request)) {
recordRequest(new RecordedGrpcRequest(request.getHeaders(),
StreamUtils.copyToString(in, StandardCharsets.UTF_8)));
}
response.getHeaders().add("Content-Type", "application/grpc");
response.getHeaders().add("Grpc-Status", "0");
callback.succeeded();
return true;
}
}
record RecordedGrpcRequest(HttpFields headers, String body) {
}
}
} }

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

@ -51,6 +51,12 @@ class OtlpAutoConfigurationTests {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class)); this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class));
} }
@Test
void shouldNotSupplyBeansIfGrpcTransportIsEnabledButPropertyIsNotSet() {
this.contextRunner.withPropertyValues("management.otlp.tracing.transport=grpc")
.run((context) -> assertThat(context).doesNotHaveBean(OtlpGrpcSpanExporter.class));
}
@Test @Test
void shouldSupplyBeans() { void shouldSupplyBeans() {
this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces") this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
@ -58,6 +64,15 @@ class OtlpAutoConfigurationTests {
.hasSingleBean(SpanExporter.class)); .hasSingleBean(SpanExporter.class));
} }
@Test
void shouldSupplyBeansIfGrpcTransportIsEnabled() {
this.contextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces",
"management.otlp.tracing.transport=grpc")
.run((context) -> assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class)
.hasSingleBean(SpanExporter.class));
}
@Test @Test
void shouldNotSupplyBeansIfGlobalTracingIsDisabled() { void shouldNotSupplyBeansIfGlobalTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false") this.contextRunner.withPropertyValues("management.tracing.enabled=false")

Loading…
Cancel
Save