Skip to content

Add auto-configuration for OTLP gRPC format when using tracing #41213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ dependencies {
testImplementation("org.awaitility:awaitility")
testImplementation("org.cache2k:cache2k-api")
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.media:jersey-media-json-jackson")
testImplementation("org.hamcrest:hamcrest")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
* the future, see: <a href=
* "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.
* To keep things simple, we only auto-configure HTTP/protobuf. If you want to use gRPC,
* define an {@link OtlpGrpcSpanExporter} and this auto-configuration will back off.
* By default, we auto-configure HTTP/protobuf. If you want to use gRPC, you need to set
* {@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 Moritz Halbritter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public class OtlpProperties {
*/
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.
*/
Expand All @@ -70,6 +75,14 @@ public void setTimeout(Duration timeout) {
this.timeout = timeout;
}

public Transport getTransport() {
return this.transport;
}

public void setTransport(Transport transport) {
this.transport = transport;
}

public Compression getCompression() {
return this.compression;
}
Expand All @@ -86,6 +99,20 @@ public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}

enum Transport {

/**
* HTTP exporter.
*/
HTTP,

/**
* gRPC exporter.
*/
GRPC

}

enum Compression {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
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.autoconfigure.condition.ConditionalOnBean;
Expand Down Expand Up @@ -69,10 +71,11 @@ public String getUrl() {
static class Exporters {

@Bean
@ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class,
type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter")
@ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
@ConditionalOnEnabledTracing("otlp")
@ConditionalOnProperty(prefix = "management.otlp.tracing", name = "transport", havingValue = "http",
matchIfMissing = true)
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties,
OtlpTracingConnectionDetails connectionDetails) {
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
Expand All @@ -85,6 +88,23 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties,
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();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,43 @@
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import io.micrometer.tracing.Tracer;
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.trace.export.SpanExporter;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
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.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
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.test.context.runner.ApplicationContextRunner;
import org.springframework.util.StreamUtils;

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

Expand All @@ -57,16 +73,28 @@ class OtlpAutoConfigurationIntegrationTests {

private final MockWebServer mockWebServer = new MockWebServer();

private final MockGrpcServer mockGrpcServer = new MockGrpcServer();

@BeforeEach
void setUp() throws IOException {
void startMockWebServer() throws IOException {
this.mockWebServer.start();
}

@BeforeEach
void startMockGrpcServer() throws Exception {
this.mockGrpcServer.start();
}

@AfterEach
void tearDown() throws IOException {
void stopMockWebServer() throws IOException {
this.mockWebServer.close();
}

@AfterEach
void stopMockGrpcServer() throws Exception {
this.mockGrpcServer.stop();
}

@Test
void httpSpanExporterShouldUseProtobufAndNoCompressionByDefault() {
this.mockWebServer.enqueue(new MockResponse());
Expand Down Expand Up @@ -113,4 +141,88 @@ void httpSpanExporterCanBeConfiguredToUseGzipCompression() {
});
}

@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) {
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,28 @@ void shouldNotSupplyBeansIfPropertyIsNotSet() {
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
void shouldSupplyBeans() {
this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
.run((context) -> assertThat(context).hasSingleBean(OtlpHttpSpanExporter.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
void shouldNotSupplyBeansIfGlobalTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false")
Expand Down