diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java index 5c218497e65..1933816b85a 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConf import org.springframework.boot.micrometer.metrics.autoconfigure.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.micrometer.metrics.autoconfigure.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.thread.Threading; @@ -61,7 +62,7 @@ import org.springframework.util.StringUtils; @ConditionalOnBean(Clock.class) @ConditionalOnClass({ OtlpMeterRegistry.class, OpenTelemetryProperties.class }) @ConditionalOnEnabledMetricsExport("otlp") -@EnableConfigurationProperties({ OtlpMetricsProperties.class, OpenTelemetryProperties.class }) +@EnableConfigurationProperties({ OtlpMetricsProperties.class, OpenTelemetryProperties.class, OtlpProperties.class }) public final class OtlpMetricsExportAutoConfiguration { private final OtlpMetricsProperties properties; @@ -72,16 +73,17 @@ public final class OtlpMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(ObjectProvider sslBundles) { - return new PropertiesOtlpMetricsConnectionDetails(this.properties, sslBundles.getIfAvailable()); + OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(OtlpProperties otlpProperties, + ObjectProvider sslBundles) { + return new PropertiesOtlpMetricsConnectionDetails(this.properties, otlpProperties, sslBundles.getIfAvailable()); } @Bean @ConditionalOnMissingBean - OtlpConfig otlpConfig(OpenTelemetryProperties openTelemetryProperties, + OtlpConfig otlpConfig(OtlpProperties otlpProperties, OpenTelemetryProperties openTelemetryProperties, OtlpMetricsConnectionDetails connectionDetails, Environment environment) { - return new OtlpMetricsPropertiesConfigAdapter(this.properties, openTelemetryProperties, connectionDetails, - environment); + return new OtlpMetricsPropertiesConfigAdapter(this.properties, otlpProperties, openTelemetryProperties, + connectionDetails, environment); } @Bean @@ -129,16 +131,27 @@ public final class OtlpMetricsExportAutoConfiguration { private final OtlpMetricsProperties properties; + private final OtlpProperties otlpProperties; + private final @Nullable SslBundles sslBundles; - PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties, @Nullable SslBundles sslBundles) { + PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties, OtlpProperties otlpProperties, + @Nullable SslBundles sslBundles) { this.properties = properties; + this.otlpProperties = otlpProperties; this.sslBundles = sslBundles; } @Override public @Nullable String getUrl() { - return this.properties.getUrl(); + if (StringUtils.hasLength(this.properties.getUrl())) { + return this.properties.getUrl(); + } + String endpoint = this.otlpProperties.getEndpoint(); + if (StringUtils.hasLength(endpoint)) { + return endpoint.endsWith("/") ? endpoint + "v1/metrics" : endpoint + "/v1/metrics"; + } + return null; } @Override diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index 99c11d898da..7e20a1b01c4 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -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; } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index 4cc7073c009..7b563e8384d 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java @@ -30,6 +30,7 @@ import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.Otl import org.springframework.boot.micrometer.metrics.autoconfigure.export.properties.StepRegistryPropertiesConfigAdapter; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryResourceAttributes; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.core.env.Environment; import org.springframework.util.CollectionUtils; @@ -43,16 +44,19 @@ import org.springframework.util.CollectionUtils; class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter implements OtlpConfig { + private final OtlpProperties otlpProperties; + private final OpenTelemetryProperties openTelemetryProperties; private final OtlpMetricsConnectionDetails connectionDetails; private final Environment environment; - OtlpMetricsPropertiesConfigAdapter(OtlpMetricsProperties properties, + OtlpMetricsPropertiesConfigAdapter(OtlpMetricsProperties properties, OtlpProperties otlpProperties, OpenTelemetryProperties openTelemetryProperties, OtlpMetricsConnectionDetails connectionDetails, Environment environment) { super(properties); + this.otlpProperties = otlpProperties; this.connectionDetails = connectionDetails; this.openTelemetryProperties = openTelemetryProperties; this.environment = environment; @@ -75,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 @@ -88,7 +103,9 @@ class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAda @Override public Map headers() { - return obtain(OtlpMetricsProperties::getHeaders, OtlpConfig.super::headers); + Map headers = new LinkedHashMap<>(this.otlpProperties.getHeaders()); + headers.putAll(obtain(OtlpMetricsProperties::getHeaders, OtlpConfig.super::headers)); + return Collections.unmodifiableMap(headers); } @Override diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index 49283bbb74d..6e3c41fdda3 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -265,6 +265,17 @@ class OtlpMetricsExportAutoConfigurationTests { }); } + @Test + void testUrlFallbackToCommonOtlpEndpoint() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://common-host:4318") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpConfig.class); + OtlpConfig config = context.getBean(OtlpConfig.class); + assertThat(config.url()).isEqualTo("http://common-host:4318/v1/metrics"); + }); + } + private HttpClient extractHttpClient(OtlpHttpMetricsSender metricsSender) { Object field = ReflectionTestUtils.getField(metricsSender, "httpSender"); assertThat(field).isNotNull(); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java index e78e7ddbd45..5c3d140ed02 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsProperties.Meter; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +45,8 @@ class OtlpMetricsPropertiesConfigAdapterTests { private OtlpMetricsProperties properties; + private OtlpProperties otlpProperties; + private OpenTelemetryProperties openTelemetryProperties; private MockEnvironment environment; @@ -53,9 +56,10 @@ class OtlpMetricsPropertiesConfigAdapterTests { @BeforeEach void setUp() { this.properties = new OtlpMetricsProperties(); + this.otlpProperties = new OtlpProperties(); this.openTelemetryProperties = new OpenTelemetryProperties(); this.environment = new MockEnvironment(); - this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties, null); + this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties, this.otlpProperties, null); } @Test @@ -238,8 +242,54 @@ class OtlpMetricsPropertiesConfigAdapterTests { } private OtlpMetricsPropertiesConfigAdapter createAdapter() { - return new OtlpMetricsPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, - this.connectionDetails, this.environment); + return new OtlpMetricsPropertiesConfigAdapter(this.properties, this.otlpProperties, + this.openTelemetryProperties, this.connectionDetails, this.environment); + } + + @Test + void whenPropertiesHeadersIsNotSetThenUseOtlpPropertiesHeadersAsFallback() { + this.otlpProperties.getHeaders().put("common-header", "common-value"); + assertThat(createAdapter().headers()).containsEntry("common-header", "common-value"); + } + + @Test + void whenPropertiesHeadersIsSetThenMergeWithOtlpPropertiesHeaders() { + this.otlpProperties.getHeaders().put("common-header", "common-value"); + this.properties.setHeaders(Map.of("signal-header", "signal-value")); + assertThat(createAdapter().headers()).containsEntry("common-header", "common-value") + .containsEntry("signal-header", "signal-value"); + } + + @Test + void whenPropertiesUrlIsNotSetThenUseOtlpPropertiesEndpointAsFallbackWithAppendix() { + this.otlpProperties.setEndpoint("http://common-endpoint:4318"); + assertThat(createAdapter().url()).isEqualTo("http://common-endpoint:4318/v1/metrics"); + } + + @Test + void whenPropertiesUrlIsNotSetThenUseOtlpPropertiesEndpointAsFallbackWithAppendixAndTrailingSlash() { + this.otlpProperties.setEndpoint("http://common-endpoint:4318/"); + assertThat(createAdapter().url()).isEqualTo("http://common-endpoint:4318/v1/metrics"); + } + + @Test + void whenPropertiesUrlIsSetThenItOverridesOtlpPropertiesEndpoint() { + this.otlpProperties.setEndpoint("http://common-endpoint:4318"); + this.properties.setUrl("http://signal-endpoint:4318/custom/metrics"); + 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); } } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java index c34f0c39f20..78a66beb6b5 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesTests.java @@ -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()); diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfiguration.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfiguration.java index 2138ee908c2..ce6cd6cb5bb 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfiguration.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.context.annotation.Import; /** @@ -48,7 +49,7 @@ import org.springframework.context.annotation.Import; */ @AutoConfiguration @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class }) -@EnableConfigurationProperties(OtlpTracingProperties.class) +@EnableConfigurationProperties({ OtlpTracingProperties.class, OtlpProperties.class }) @Import({ OtlpTracingConfigurations.ConnectionDetails.class, OtlpTracingConfigurations.Exporters.class }) public final class OtlpTracingAutoConfiguration { diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java index 4cb6a7c41d1..4fb14716cac 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingConfigurations.java @@ -16,7 +16,9 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp; +import java.util.LinkedHashMap; import java.util.Locale; +import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -30,13 +32,16 @@ import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 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.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.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -54,23 +59,50 @@ final class OtlpTracingConfigurations { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint") + @Conditional(OtlpEndpointCondition.class) OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpTracingProperties properties, - ObjectProvider sslBundles) { - return new PropertiesOtlpTracingConnectionDetails(properties, sslBundles.getIfAvailable()); + OtlpProperties otlpProperties, ObjectProvider sslBundles) { + return new PropertiesOtlpTracingConnectionDetails(properties, otlpProperties, sslBundles.getIfAvailable()); } /** - * Adapts {@link OtlpTracingProperties} to {@link OtlpTracingConnectionDetails}. + * Condition to check if either the tracing-specific endpoint or the common OTLP + * endpoint is set. + */ + static class OtlpEndpointCondition extends AnyNestedCondition { + + OtlpEndpointCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint") + static class TracingEndpoint { + + } + + @ConditionalOnProperty("management.opentelemetry.otlp.endpoint") + static class CommonEndpoint { + + } + + } + + /** + * Adapts {@link OtlpTracingProperties} and {@link OtlpProperties} to + * {@link OtlpTracingConnectionDetails}. */ static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnectionDetails { private final OtlpTracingProperties properties; + private final OtlpProperties otlpProperties; + private final @Nullable SslBundles sslBundles; - PropertiesOtlpTracingConnectionDetails(OtlpTracingProperties properties, @Nullable SslBundles sslBundles) { + PropertiesOtlpTracingConnectionDetails(OtlpTracingProperties properties, OtlpProperties otlpProperties, + @Nullable SslBundles sslBundles) { this.properties = properties; + this.otlpProperties = otlpProperties; this.sslBundles = sslBundles; } @@ -80,6 +112,12 @@ final class OtlpTracingConfigurations { "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(); + if (endpoint != null && transport == Transport.HTTP) { + endpoint = endpoint.endsWith("/") ? endpoint + "v1/traces" : endpoint + "/v1/traces"; + } + } Assert.state(endpoint != null, "'endpoint' must not be null"); return endpoint; } @@ -107,15 +145,17 @@ final class OtlpTracingConfigurations { @Bean @ConditionalOnProperty(name = "management.opentelemetry.tracing.export.otlp.transport", havingValue = "http", matchIfMissing = true) - OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, + OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, OtlpProperties otlpProperties, OtlpTracingConnectionDetails connectionDetails, ObjectProvider meterProvider, ObjectProvider customizers) { OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.HTTP)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) - .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); - properties.getHeaders().forEach(builder::addHeader); + .setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.ROOT)); + Map 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)); @@ -124,21 +164,39 @@ final class OtlpTracingConfigurations { @Bean @ConditionalOnProperty(name = "management.opentelemetry.tracing.export.otlp.transport", havingValue = "grpc") - OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties, + OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties, OtlpProperties otlpProperties, OtlpTracingConnectionDetails connectionDetails, ObjectProvider meterProvider, ObjectProvider customizers) { OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.GRPC)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) - .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); - properties.getHeaders().forEach(builder::addHeader); + .setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.ROOT)); + Map 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(); diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java index b76c63cb610..c4390ea1ec9 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingProperties.java @@ -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; } diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java index 50f47d4286a..9e86db47034 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java @@ -48,6 +48,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.extension.trace.propagation.B3Propagator; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.resources.Resource; @@ -67,6 +68,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration; import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration; import org.springframework.boot.micrometer.tracing.brave.autoconfigure.BraveAutoConfiguration; +import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -528,6 +530,57 @@ class OpenTelemetryTracingAutoConfigurationTests { }); } + @Test + void shouldMergeCommonAndSignalSpecificHeadersForTracing() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318", + "management.opentelemetry.otlp.headers.common-header=common-value", + "management.opentelemetry.otlp.headers.shared-header=common-wins", + "management.opentelemetry.tracing.export.otlp.headers.tracing-header=tracing-value", + "management.opentelemetry.tracing.export.otlp.headers.shared-header=tracing-wins") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).extracting("delegate.httpSender.headerSupplier") + .asInstanceOf(InstanceOfAssertFactories.type(Supplier.class)) + .satisfies((headerSupplier) -> assertThat(headerSupplier.get()) + .asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class)) + .containsEntry("common-header", List.of("common-value")) + .containsEntry("tracing-header", List.of("tracing-value")) + .containsEntry("shared-header", List.of("tracing-wins"))); + }); + } + + @Test + void shouldAppendTracesPathToCommonEndpoint() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).extracting("delegate.httpSender.url") + .extracting(Object::toString) + .isEqualTo("http://localhost:4318/v1/traces"); + }); + } + + @Test + void shouldNotAppendTracesPathToTracingSpecificEndpoint() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4318/custom/traces") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).extracting("delegate.httpSender.url") + .extracting(Object::toString) + .isEqualTo("http://localhost:4318/custom/traces"); + }); + } + private void initializeOpenTelemetry(ConfigurableApplicationContext context) { context.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener()); Span.current(); diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationIntegrationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationIntegrationTests.java index e42e7de54ba..bb758039245 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationIntegrationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/otlp/OtlpTracingAutoConfigurationIntegrationTests.java @@ -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 diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OtlpProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OtlpProperties.java new file mode 100644 index 00000000000..6bc97903bc0 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OtlpProperties.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-present 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.opentelemetry.autoconfigure; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Common configuration properties for OpenTelemetry Protocol (OTLP) exporters. + * + * @author Somil Jain + * @since 4.2.0 + */ +@ConfigurationProperties("management.opentelemetry.otlp") +public class OtlpProperties { + + /** + * OTLP endpoint to connect to. + */ + private @Nullable String endpoint; + + /** + * Additional headers to be passed with every request. + */ + private final Map headers = new LinkedHashMap<>(); + + /** + * Method used to compress the payload. + */ + private @Nullable Compression compression; + + public @Nullable String getEndpoint() { + return this.endpoint; + } + + public void setEndpoint(@Nullable String endpoint) { + this.endpoint = endpoint; + } + + public Map getHeaders() { + 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 + + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java index 571632b0544..622bee40ac0 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.Exporters; import org.springframework.context.annotation.Import; @@ -35,7 +36,7 @@ import org.springframework.context.annotation.Import; */ @AutoConfiguration @ConditionalOnClass({ OpenTelemetry.class, SdkLoggerProvider.class }) -@EnableConfigurationProperties(OtlpLoggingProperties.class) +@EnableConfigurationProperties({ OtlpLoggingProperties.class, OtlpProperties.class }) @Import({ ConnectionDetails.class, Exporters.class }) public final class OtlpLoggingAutoConfiguration { diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java index 0722701f2a4..0ef2d0a1055 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java @@ -16,7 +16,9 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; +import java.util.LinkedHashMap; import java.util.Locale; +import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -30,14 +32,17 @@ import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.boot.opentelemetry.autoconfigure.logging.ConditionalOnEnabledLoggingExport; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -55,23 +60,50 @@ final class OtlpLoggingConfigurations { @Bean @ConditionalOnMissingBean(OtlpLoggingConnectionDetails.class) - @ConditionalOnProperty("management.opentelemetry.logging.export.otlp.endpoint") + @Conditional(OtlpEndpointCondition.class) PropertiesOtlpLoggingConnectionDetails openTelemetryLoggingConnectionDetails(OtlpLoggingProperties properties, - ObjectProvider sslBundles) { - return new PropertiesOtlpLoggingConnectionDetails(properties, sslBundles.getIfAvailable()); + OtlpProperties otlpProperties, ObjectProvider sslBundles) { + return new PropertiesOtlpLoggingConnectionDetails(properties, otlpProperties, sslBundles.getIfAvailable()); } /** - * Adapts {@link OtlpLoggingProperties} to {@link OtlpLoggingConnectionDetails}. + * Condition to check if either the logging-specific endpoint or the common OTLP + * endpoint is set. + */ + static class OtlpEndpointCondition extends AnyNestedCondition { + + OtlpEndpointCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty("management.opentelemetry.logging.export.otlp.endpoint") + static class LoggingEndpoint { + + } + + @ConditionalOnProperty("management.opentelemetry.otlp.endpoint") + static class CommonEndpoint { + + } + + } + + /** + * Adapts {@link OtlpLoggingProperties} and {@link OtlpProperties} to + * {@link OtlpLoggingConnectionDetails}. */ static class PropertiesOtlpLoggingConnectionDetails implements OtlpLoggingConnectionDetails { private final OtlpLoggingProperties properties; + private final OtlpProperties otlpProperties; + private final @Nullable SslBundles sslBundles; - PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties, @Nullable SslBundles sslBundles) { + PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties, OtlpProperties otlpProperties, + @Nullable SslBundles sslBundles) { this.properties = properties; + this.otlpProperties = otlpProperties; this.sslBundles = sslBundles; } @@ -81,6 +113,12 @@ final class OtlpLoggingConfigurations { "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(); + if (endpoint != null && transport == Transport.HTTP) { + endpoint = endpoint.endsWith("/") ? endpoint + "v1/logs" : endpoint + "/v1/logs"; + } + } Assert.state(endpoint != null, "'endpoint' must not be null"); return endpoint; } @@ -110,14 +148,17 @@ final class OtlpLoggingConfigurations { @ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "http", matchIfMissing = true) OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties, - OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider, + OtlpProperties otlpProperties, OtlpLoggingConnectionDetails connectionDetails, + ObjectProvider meterProvider, ObjectProvider customizers) { OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.HTTP)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) - .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); - properties.getHeaders().forEach(builder::addHeader); + .setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.US)); + Map 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)); @@ -127,20 +168,39 @@ final class OtlpLoggingConfigurations { @Bean @ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "grpc") OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OtlpLoggingProperties properties, - OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider, + OtlpProperties otlpProperties, OtlpLoggingConnectionDetails connectionDetails, + ObjectProvider meterProvider, ObjectProvider customizers) { OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.GRPC)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) - .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); - properties.getHeaders().forEach(builder::addHeader); + .setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.US)); + Map 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(); diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java index ebb114ae24b..68749110f4b 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java @@ -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; } diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java index 6df0b728035..02ba84dc1bf 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/OpenTelemetryLoggingAutoConfigurationTests.java @@ -18,10 +18,13 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import io.opentelemetry.context.Context; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.LogLimits; import io.opentelemetry.sdk.logs.LogRecordProcessor; @@ -31,12 +34,16 @@ import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.annotation.ImportCandidates; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration; +import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; +import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -176,6 +183,50 @@ class OpenTelemetryLoggingAutoConfigurationTests { }); } + @Test + void shouldPreferSignalSpecificEndpointOverCommonEndpointForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://common-host:4318", + "management.opentelemetry.logging.export.otlp.endpoint=http://logging-host:4318") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class); + OtlpLoggingConnectionDetails connectionDetails = context.getBean(OtlpLoggingConnectionDetails.class); + assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://logging-host:4318"); + }); + } + + @Test + void shouldMergeCommonAndSignalSpecificHeadersForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318", + "management.opentelemetry.otlp.headers.common-header=common-value", + "management.opentelemetry.otlp.headers.shared-header=common-wins", + "management.opentelemetry.logging.export.otlp.headers.logging-header=logging-value", + "management.opentelemetry.logging.export.otlp.headers.shared-header=logging-wins") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).extracting("delegate.httpSender.headerSupplier") + .asInstanceOf(InstanceOfAssertFactories.type(Supplier.class)) + .satisfies((headerSupplier) -> assertThat(headerSupplier.get()) + .asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class)) + .containsEntry("common-header", List.of("common-value")) + .containsEntry("logging-header", List.of("logging-value")) + .containsEntry("shared-header", List.of("logging-wins"))); + }); + } + + @Test + void shouldFallbackToCommonEndpointAndAppendPathForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.endpoint=http://common-host:4318") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class); + OtlpLoggingConnectionDetails connectionDetails = context.getBean(OtlpLoggingConnectionDetails.class); + assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://common-host:4318/v1/logs"); + }); + } + @Configuration(proxyBeanMethods = false) static class UserConfiguration { diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java index 6d52496c3fa..475036f8d6f 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java @@ -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") diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java index 2839b3c9b9d..49ac80726d1 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java @@ -38,6 +38,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.context.annotation.ImportCandidates; import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties; import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.logging.SdkLoggerProviderBuilderCustomizer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails; @@ -321,6 +322,55 @@ class OtlpLoggingAutoConfigurationTests { }); } + @Test + 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"); + } + + @Test + void shouldNotAppendLogsPathToLoggingSpecificEndpoint() { + OtlpLoggingProperties properties = new OtlpLoggingProperties(); + properties.setEndpoint("http://localhost:4318/custom/logs"); + 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/custom/logs"); + } + + @Test + void shouldAppendLogsPathToCommonEndpointWithTrailingSlash() { + OtlpLoggingProperties properties = new OtlpLoggingProperties(); + + 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"); + } + @Configuration(proxyBeanMethods = false) public static class MultipleSdkLoggerProviderBuilderCustomizersConfig {