Polish "Align OTLP signal-specific properties with common fallback configuration"

Allow `management.opentelemetry.otlp.compression` to act as a common
default for OTLP compression, mirroring the existing endpoint/headers
fallback. Tracing, logging and metrics exporters now fall back to it
when their own signal-specific compression property is unset.

See gh-50461
This commit is contained in:
Moritz Halbritter
2026-08-24 14:51:43 +02:00
parent faf93946cd
commit 6af1fb5c26
13 changed files with 198 additions and 108 deletions
@@ -151,16 +151,6 @@
"description": "Whether auto-configuration of logging is enabled to export logs.",
"defaultValue": true
},
{
"name": "management.opentelemetry.otlp.endpoint",
"type": "java.lang.String",
"description": "OTLP target endpoint URL."
},
{
"name": "management.opentelemetry.otlp.headers",
"type": "java.util.Map<java.lang.String,java.lang.String>",
"description": "Custom headers to be appended to OTLP requests."
},
{
"name": "management.server.add-application-context-header",
"type": "java.lang.Boolean",
@@ -53,7 +53,7 @@ public class OtlpMetricsProperties extends StepRegistryProperties {
/**
* Compression mode to use when exporting metrics.
*/
private CompressionMode compressionMode = CompressionMode.NONE;
private @Nullable CompressionMode compressionMode;
/**
* Headers for the exported metrics.
@@ -109,11 +109,11 @@ public class OtlpMetricsProperties extends StepRegistryProperties {
this.aggregationTemporality = aggregationTemporality;
}
public CompressionMode getCompressionMode() {
public @Nullable CompressionMode getCompressionMode() {
return this.compressionMode;
}
public void setCompressionMode(CompressionMode compressionMode) {
public void setCompressionMode(@Nullable CompressionMode compressionMode) {
this.compressionMode = compressionMode;
}
@@ -79,7 +79,18 @@ class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAda
@Override
public CompressionMode compressionMode() {
return obtain(OtlpMetricsProperties::getCompressionMode, OtlpConfig.super::compressionMode);
CompressionMode compressionMode = this.properties.getCompressionMode();
if (compressionMode != null) {
return compressionMode;
}
OtlpProperties.Compression commonCompression = this.otlpProperties.getCompression();
if (commonCompression != null) {
return switch (commonCompression) {
case GZIP -> CompressionMode.GZIP;
case NONE -> CompressionMode.NONE;
};
}
return OtlpConfig.super.compressionMode();
}
@Override
@@ -279,4 +279,17 @@ class OtlpMetricsPropertiesConfigAdapterTests {
assertThat(createAdapter().url()).isEqualTo("http://signal-endpoint:4318/custom/metrics");
}
@Test
void whenPropertiesCompressionModeIsNotSetThenUseOtlpPropertiesCompressionAsFallback() {
this.otlpProperties.setCompression(OtlpProperties.Compression.GZIP);
assertThat(createAdapter().compressionMode()).isSameAs(CompressionMode.GZIP);
}
@Test
void whenPropertiesCompressionModeIsSetThenItOverridesOtlpPropertiesCompression() {
this.otlpProperties.setCompression(OtlpProperties.Compression.GZIP);
this.properties.setCompressionMode(CompressionMode.NONE);
assertThat(createAdapter().compressionMode()).isSameAs(CompressionMode.NONE);
}
}
@@ -36,7 +36,7 @@ class OtlpMetricsPropertiesTests extends StepRegistryPropertiesTests {
OtlpConfig config = OtlpConfig.DEFAULT;
assertStepRegistryDefaultValues(properties, config);
assertThat(properties.getAggregationTemporality()).isSameAs(config.aggregationTemporality());
assertThat(properties.getCompressionMode()).isSameAs(config.compressionMode());
assertThat(properties.getCompressionMode()).isNull();
assertThat(properties.getHistogramFlavor()).isSameAs(config.histogramFlavor());
assertThat(properties.getMaxScale()).isEqualTo(config.maxScale());
assertThat(properties.getMaxBucketCount()).isEqualTo(config.maxBucketCount());
@@ -16,7 +16,6 @@
package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
@@ -34,20 +33,16 @@ import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.micrometer.tracing.autoconfigure.ConditionalOnEnabledTracingExport;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -81,13 +76,11 @@ final class OtlpTracingConfigurations {
}
@ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint")
@SuppressWarnings("unused")
static class TracingEndpoint {
}
@ConditionalOnProperty("management.opentelemetry.otlp.endpoint")
@SuppressWarnings("unused")
static class CommonEndpoint {
}
@@ -115,6 +108,9 @@ final class OtlpTracingConfigurations {
@Override
public String getUrl(Transport transport) {
Assert.state(transport == this.properties.getTransport(),
"Requested transport %s doesn't match configured transport %s".formatted(transport,
this.properties.getTransport()));
String endpoint = this.properties.getEndpoint();
if (!StringUtils.hasLength(endpoint)) {
endpoint = this.otlpProperties.getEndpoint();
@@ -153,23 +149,13 @@ final class OtlpTracingConfigurations {
OtlpTracingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpHttpSpanExporterBuilderCustomizer> customizers) {
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
.setEndpoint(connectionDetails.getUrl(Transport.HTTP));
Duration timeout = properties.getTimeout();
builder.setTimeout(timeout);
Duration connectTimeout = properties.getConnectTimeout();
builder.setConnectTimeout(connectTimeout);
String compression = properties.getCompression().name().toLowerCase(Locale.ROOT);
if (StringUtils.hasLength(compression)) {
builder.setCompression(compression);
}
.setEndpoint(connectionDetails.getUrl(Transport.HTTP))
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.ROOT));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -182,29 +168,35 @@ final class OtlpTracingConfigurations {
OtlpTracingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpGrpcSpanExporterBuilderCustomizer> customizers) {
OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder()
.setEndpoint(connectionDetails.getUrl(Transport.GRPC));
Duration timeout = properties.getTimeout();
builder.setTimeout(timeout);
Duration connectTimeout = properties.getConnectTimeout();
builder.setConnectTimeout(connectTimeout);
String compression = properties.getCompression().name().toLowerCase(Locale.ROOT);
if (StringUtils.hasLength(compression)) {
builder.setCompression(compression);
}
.setEndpoint(connectionDetails.getUrl(Transport.GRPC))
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.ROOT));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
private OtlpTracingProperties.Compression resolveCompression(OtlpTracingProperties properties,
OtlpProperties otlpProperties) {
OtlpTracingProperties.Compression compression = properties.getCompression();
if (compression != null) {
return compression;
}
OtlpProperties.Compression commonCompression = otlpProperties.getCompression();
if (commonCompression != null) {
return switch (commonCompression) {
case GZIP -> OtlpTracingProperties.Compression.GZIP;
case NONE -> OtlpTracingProperties.Compression.NONE;
};
}
return OtlpTracingProperties.Compression.NONE;
}
private void configureSsl(OtlpTracingConnectionDetails connectionDetails,
SslContextConfigurer sslContextConfigurer) {
SslBundle sslBundle = connectionDetails.getSslBundle();
@@ -230,32 +222,6 @@ final class OtlpTracingConfigurations {
}
static class HttpTransportCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String tracingTransport = context.getEnvironment()
.getProperty("management.opentelemetry.tracing.export.otlp.transport");
String activeTransport = (tracingTransport != null) ? tracingTransport : "http";
return new ConditionOutcome("http".equalsIgnoreCase(activeTransport),
"Transport is " + activeTransport);
}
}
static class GrpcTransportCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String tracingTransport = context.getEnvironment()
.getProperty("management.opentelemetry.tracing.export.otlp.transport");
String activeTransport = (tracingTransport != null) ? tracingTransport : "http";
return new ConditionOutcome("grpc".equalsIgnoreCase(activeTransport),
"Transport is " + activeTransport);
}
}
}
}
@@ -59,7 +59,7 @@ public class OtlpTracingProperties {
/**
* Method used to compress the payload.
*/
private Compression compression = Compression.NONE;
private @Nullable Compression compression;
/**
* Custom HTTP headers you want to pass to the collector, for example auth headers.
@@ -100,11 +100,11 @@ public class OtlpTracingProperties {
this.transport = transport;
}
public Compression getCompression() {
public @Nullable Compression getCompression() {
return this.compression;
}
public void setCompression(Compression compression) {
public void setCompression(@Nullable Compression compression) {
this.compression = compression;
}
@@ -134,6 +134,41 @@ class OtlpTracingAutoConfigurationIntegrationTests {
});
}
@Test
void httpSpanExporterFallsBackToCommonCompressionWhenSignalSpecificCompressionIsNotSet() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.opentelemetry.otlp.compression=gzip",
"management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:%d/test"
.formatted(this.mockWebServer.getPort()))
.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.getHeader("Content-Encoding")).isEqualTo("gzip");
});
}
@Test
void httpSpanExporterSignalSpecificCompressionWinsOverCommonCompression() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.opentelemetry.otlp.compression=gzip",
"management.opentelemetry.tracing.export.otlp.compression=none",
"management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:%d/test"
.formatted(this.mockWebServer.getPort()))
.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.getHeader("Content-Encoding")).isNull();
});
}
@Test
void grpcSpanExporterShouldExportSpans() {
this.contextRunner
@@ -42,6 +42,11 @@ public class OtlpProperties {
*/
private final Map<String, String> headers = new LinkedHashMap<>();
/**
* Method used to compress the payload.
*/
private @Nullable Compression compression;
public @Nullable String getEndpoint() {
return this.endpoint;
}
@@ -54,4 +59,29 @@ public class OtlpProperties {
return this.headers;
}
public @Nullable Compression getCompression() {
return this.compression;
}
public void setCompression(@Nullable Compression compression) {
this.compression = compression;
}
/**
* Compression methods.
*/
public enum Compression {
/**
* Gzip compression.
*/
GZIP,
/**
* No compression.
*/
NONE
}
}
@@ -16,7 +16,6 @@
package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
@@ -78,13 +77,11 @@ final class OtlpLoggingConfigurations {
}
@ConditionalOnProperty("management.opentelemetry.logging.export.otlp.endpoint")
@SuppressWarnings("unused")
static class LoggingEndpoint {
}
@ConditionalOnProperty("management.opentelemetry.otlp.endpoint")
@SuppressWarnings("unused")
static class CommonEndpoint {
}
@@ -112,6 +109,9 @@ final class OtlpLoggingConfigurations {
@Override
public String getUrl(Transport transport) {
Assert.state(transport == this.properties.getTransport(),
"Requested transport %s doesn't match configured transport %s".formatted(transport,
this.properties.getTransport()));
String endpoint = this.properties.getEndpoint();
if (!StringUtils.hasLength(endpoint)) {
endpoint = this.otlpProperties.getEndpoint();
@@ -152,20 +152,13 @@ final class OtlpLoggingConfigurations {
ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpHttpLogRecordExporterBuilderCustomizer> customizers) {
OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder()
.setEndpoint(connectionDetails.getUrl(Transport.HTTP));
Duration timeout = properties.getTimeout();
builder.setTimeout(timeout);
String compression = properties.getCompression().name().toLowerCase(Locale.ROOT);
if (StringUtils.hasLength(compression)) {
builder.setCompression(compression);
}
.setEndpoint(connectionDetails.getUrl(Transport.HTTP))
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.US));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -179,26 +172,35 @@ final class OtlpLoggingConfigurations {
ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpGrpcLogRecordExporterBuilderCustomizer> customizers) {
OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder()
.setEndpoint(connectionDetails.getUrl(Transport.GRPC));
Duration timeout = properties.getTimeout();
builder.setTimeout(timeout);
String compression = properties.getCompression().name().toLowerCase(Locale.ROOT);
if (StringUtils.hasLength(compression)) {
builder.setCompression(compression);
}
.setEndpoint(connectionDetails.getUrl(Transport.GRPC))
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.US));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
private OtlpLoggingProperties.Compression resolveCompression(OtlpLoggingProperties properties,
OtlpProperties otlpProperties) {
OtlpLoggingProperties.Compression compression = properties.getCompression();
if (compression != null) {
return compression;
}
OtlpProperties.Compression commonCompression = otlpProperties.getCompression();
if (commonCompression != null) {
return switch (commonCompression) {
case GZIP -> OtlpLoggingProperties.Compression.GZIP;
case NONE -> OtlpLoggingProperties.Compression.NONE;
};
}
return OtlpLoggingProperties.Compression.NONE;
}
private void configureSsl(OtlpLoggingConnectionDetails connectionDetails,
SslContextConfigurer sslContextConfigurer) {
SslBundle sslBundle = connectionDetails.getSslBundle();
@@ -59,7 +59,7 @@ public class OtlpLoggingProperties {
/**
* Method used to compress the payload.
*/
private Compression compression = Compression.NONE;
private @Nullable Compression compression;
/**
* Custom HTTP headers you want to pass to the collector, for example auth headers.
@@ -100,11 +100,11 @@ public class OtlpLoggingProperties {
this.transport = transport;
}
public Compression getCompression() {
public @Nullable Compression getCompression() {
return this.compression;
}
public void setCompression(Compression compression) {
public void setCompression(@Nullable Compression compression) {
this.compression = compression;
}
@@ -108,6 +108,37 @@ class OtlpLoggingAutoConfigurationIntegrationTests {
});
}
@Test
void httpLogRecordExporterFallsBackToCommonCompressionWhenSignalSpecificCompressionIsNotSet() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=http://localhost:%d/v1/logs"
.formatted(this.mockWebServer.getPort()), "management.opentelemetry.otlp.compression=gzip")
.run((context) -> {
logMessage(context);
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip");
});
}
@Test
void httpLogRecordExporterSignalSpecificCompressionWinsOverCommonCompression() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues(
"management.opentelemetry.logging.export.otlp.endpoint=http://localhost:%d/v1/logs"
.formatted(this.mockWebServer.getPort()),
"management.opentelemetry.logging.export.otlp.compression=none",
"management.opentelemetry.otlp.compression=gzip")
.run((context) -> {
logMessage(context);
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getHeader("Content-Encoding")).isNull();
});
}
private static void logMessage(ApplicationContext context) {
SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class);
loggerProvider.get("test")
@@ -323,14 +323,26 @@ class OtlpLoggingAutoConfigurationTests {
}
@Test
void shouldAppendLogsPathToCommonEndpoint() {
void shouldAppendLogsPathToCommonEndpointForHttpTransport() {
OtlpLoggingProperties properties = new OtlpLoggingProperties();
properties.setTransport(Transport.HTTP);
OtlpProperties otlpProperties = new OtlpProperties();
otlpProperties.setEndpoint("http://localhost:4318");
PropertiesOtlpLoggingConnectionDetails connectionDetails = new PropertiesOtlpLoggingConnectionDetails(
properties, otlpProperties, null);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://localhost:4318/v1/logs");
}
@Test
void shouldNotAppendLogsPathToCommonEndpointForGrpcTransport() {
OtlpLoggingProperties properties = new OtlpLoggingProperties();
properties.setTransport(Transport.GRPC);
OtlpProperties otlpProperties = new OtlpProperties();
otlpProperties.setEndpoint("http://localhost:4318");
PropertiesOtlpLoggingConnectionDetails connectionDetails = new PropertiesOtlpLoggingConnectionDetails(
properties, otlpProperties, null);
assertThat(connectionDetails.getUrl(Transport.GRPC)).isEqualTo("http://localhost:4318");
}