Skip to content

Commit

Permalink
Add auto-configuration for OTLP span exporter
Browse files Browse the repository at this point in the history
With these changes an OTLP HTTP/protobuf exporter is auto-configured
if opentelemetry-exporter-otlp is on the classpath.

See gh-34508
  • Loading branch information
jonatan-ivanov committed Apr 17, 2023
1 parent a37a722 commit 1a9eca8
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {
optional("io.micrometer:micrometer-registry-wavefront")
optional("io.zipkin.reporter2:zipkin-sender-urlconnection")
optional("io.opentelemetry:opentelemetry-exporter-zipkin")
optional("io.opentelemetry:opentelemetry-exporter-otlp")
optional("io.projectreactor.netty:reactor-netty-http")
optional("io.r2dbc:r2dbc-pool")
optional("io.r2dbc:r2dbc-spi")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import java.util.Map.Entry;

import io.micrometer.tracing.otel.bridge.OtelTracer;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.sdk.trace.SdkTracerProvider;

import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
* {@link EnableAutoConfiguration Auto-configuration} for OTLP. Brave does not support
* OTLP, so we only configure it for OpenTelemetry. OTLP defines three transports that are
* supported: gRPC (/protobuf), HTTP/protobuf, HTTP/JSON. From these transports HTTP/JSON
* is not supported by the OTel Java SDK, and it seems there are no plans supporting it in
* 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,
* please disable this auto-configuration and create a bean.
*
* @author Jonatan Ivanov
* @since 3.1.0
*/
@AutoConfiguration
@ConditionalOnEnabledTracing
@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class })
@EnableConfigurationProperties(OtlpProperties.class)
public class OtlpAutoConfiguration {

@Bean
@ConditionalOnMissingBean
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) {
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
.setEndpoint(properties.getEndpoint())
.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
@@ -0,0 +1,103 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Configuration properties for {@link OtlpAutoConfiguration}.
*
* @author Jonatan Ivanov
* @since 3.1.0
*/
@ConfigurationProperties("management.otlp.tracing")
public class OtlpProperties {

/**
* URL to the OTel collector's HTTP API.
*/
private String endpoint = "http://localhost:4318/v1/traces";

/**
* Call timeout for the OTel Collector to process an exported batch of data. This
* timeout spans the entire call: resolving DNS, connecting, writing the request body,
* server processing, and reading the response body. If the call requires redirects or
* retries all must complete within one timeout period.
*/
private Duration timeout = Duration.ofSeconds(10);

/**
* The method used to compress the payload.
*/
private Compression compression = Compression.NONE;

/**
* Custom HTTP headers you want to pass to the collector, for example auth headers.
*/
private Map<String, String> headers = new HashMap<>();

public String getEndpoint() {
return this.endpoint;
}

public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}

public Duration getTimeout() {
return this.timeout;
}

public void setTimeout(Duration timeout) {
this.timeout = timeout;
}

public Compression getCompression() {
return this.compression;
}

public void setCompression(Compression compression) {
this.compression = compression;
}

public Map<String, String> getHeaders() {
return this.headers;
}

public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}

enum Compression {

/**
* Gzip compression.
*/
GZIP,

/**
* No compression.
*/
NONE

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Auto-configuration for tracing with OTLP.
*/
package org.springframework.boot.actuate.autoconfigure.tracing.otlp;
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributor
org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

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

import io.micrometer.tracing.Tracer;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
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.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.tracing.MicrometerTracingAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

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

/**
* Integration tests for {@link OtlpAutoConfiguration}.
*
* @author Jonatan Ivanov
*/
class OtlpAutoConfigurationIntegrationTests {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.tracing.sampling.probability=1.0")
.withConfiguration(
AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class,
OpenTelemetryAutoConfiguration.class, OtlpAutoConfiguration.class));

private MockWebServer mockWebServer;

@BeforeEach
void setUp() throws IOException {
this.mockWebServer = new MockWebServer();
this.mockWebServer.start();
}

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

@Test
void httpSpanExporterShouldUseProtoBufAndNoCompression() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:%d/v1/traces"
.formatted(this.mockWebServer.getPort()), "management.otlp.tracing.headers.custom=42")
.run((context) -> {
context.getBean(Tracer.class).nextSpan().name("test").end();
assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
.isSameAs(CompletableResultCode.ofSuccess());

RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getRequestLine()).contains("/v1/traces");
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
assertThat(request.getHeader("custom")).isEqualTo("42");
assertThat(request.getBodySize()).isPositive();
try (Buffer body = request.getBody()) {
assertThat(body.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
}
});
}

@Test
void httpSpanExporterShouldUseProtoBufAndGzip() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.otlp.tracing.compression=GZIP",
"management.otlp.tracing.endpoint=http://localhost:%d/test".formatted(this.mockWebServer.getPort()))
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class).hasSingleBean(SpanExporter.class);
context.getBean(Tracer.class).nextSpan().name("test").end();
assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
.isSameAs(CompletableResultCode.ofSuccess());

RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getRequestLine()).contains("/test");
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip");
assertThat(request.getBodySize()).isPositive();
try (Buffer unCompressed = new Buffer(); Buffer body = request.getBody()) {
unCompressed.writeAll(new GzipSource(body));
assertThat(unCompressed.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
}
});
}

}
Loading

0 comments on commit 1a9eca8

Please sign in to comment.