Add auto-configuration for OTLP gRPC format when using tracing
See gh-41213
This commit is contained in:
committed by
Moritz Halbritter
parent
156237227c
commit
6b50d6783b
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user