diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a18b995b1a07..b8b9ad4ab5b3 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -4,14 +4,14 @@ - diff --git a/module/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index b539371484c4..d6d1662d7cdc 100644 --- a/module/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -151,6 +151,26 @@ "description": "Whether auto-configuration of logging is enabled to export logs.", "defaultValue": true }, + { + "name": "management.opentelemetry.otlp.connect-timeout", + "type": "java.time.Duration", + "description": "Connection timeout for the OTLP exporters." + }, + { + "name": "management.opentelemetry.otlp.endpoint", + "type": "java.lang.String", + "description": "OTLP target endpoint URL." + }, + { + "name": "management.opentelemetry.otlp.headers", + "type": "java.util.Map", + "description": "Custom headers to be appended to OTLP requests." + }, + { + "name": "management.opentelemetry.otlp.timeout", + "type": "java.time.Duration", + "description": "Read timeout for the OTLP exporters." + }, { "name": "management.server.add-application-context-header", "type": "java.lang.Boolean", 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 5c218497e65c..591eac0d2eef 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.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 @@ @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,23 +73,26 @@ 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 @ConditionalOnMissingBean(OtlpMetricsSender.class) - OtlpHttpMetricsSender otlpMetricsSender(OtlpMetricsConnectionDetails connectionDetails) { + OtlpHttpMetricsSender otlpMetricsSender(OtlpMetricsConnectionDetails connectionDetails, + OtlpProperties otlpProperties) { Duration connectTimeout = this.properties.getConnectTimeout(); - Duration timeout = connectTimeout.plus(this.properties.getReadTimeout()); + Duration readTimeout = this.properties.getReadTimeout(); + Duration timeout = connectTimeout.plus(readTimeout); JdkClientHttpSender httpSender = new JdkClientHttpSender(connectTimeout, timeout, connectionDetails.getSslBundle()); return new OtlpHttpMetricsSender(httpSender); @@ -129,16 +133,27 @@ static class PropertiesOtlpMetricsConnectionDetails implements OtlpMetricsConnec 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/OtlpMetricsPropertiesConfigAdapter.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapter.java index 4cc7073c0090..09976bc5166d 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 @@ -16,6 +16,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; +import java.time.Duration; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -30,6 +31,7 @@ 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 +45,19 @@ 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; @@ -88,7 +93,27 @@ public Map resourceAttributes() { @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 + @SuppressWarnings("deprecation") + public Duration connectTimeout() { + return obtain(OtlpMetricsProperties::getConnectTimeout, () -> { + Duration commonConnectTimeout = this.otlpProperties.getConnectTimeout(); + return (commonConnectTimeout != null) ? commonConnectTimeout : OtlpConfig.super.connectTimeout(); + }); + } + + @Override + @SuppressWarnings("deprecation") + public Duration readTimeout() { + return obtain(OtlpMetricsProperties::getReadTimeout, () -> { + Duration commonTimeout = this.otlpProperties.getTimeout(); + return (commonTimeout != null) ? commonTimeout : OtlpConfig.super.readTimeout(); + }); } @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 f4c267b8f19e..d4f3e62d748c 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 @@ -266,6 +266,17 @@ public SslBundle getSslBundle() { }); } + @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 e78e7ddbd45b..3a97ab693360 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 @@ -16,6 +16,7 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; +import java.time.Duration; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -28,6 +29,7 @@ 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 +46,8 @@ class OtlpMetricsPropertiesConfigAdapterTests { private OtlpMetricsProperties properties; + private OtlpProperties otlpProperties; + private OpenTelemetryProperties openTelemetryProperties; private MockEnvironment environment; @@ -53,9 +57,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 +243,36 @@ void shouldUseDefaultApplicationGroupIfApplicationGroupIsNotSet() { } 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 whenPropertiesTimeoutIsSetItOverridesOtlpPropertiesTimeout() { + this.otlpProperties.setTimeout(Duration.ofSeconds(10)); + this.properties.setReadTimeout(Duration.ofSeconds(3)); + assertThat(createAdapter().readTimeout()).isEqualTo(Duration.ofSeconds(3)); + } + + @Test + void whenPropertiesConnectTimeoutIsSetItOverridesOtlpPropertiesConnectTimeout() { + this.otlpProperties.setConnectTimeout(Duration.ofSeconds(10)); + this.properties.setConnectTimeout(Duration.ofSeconds(3)); + assertThat(createAdapter().connectTimeout()).isEqualTo(Duration.ofSeconds(3)); } } 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 2138ee908c2e..ce6cd6cb5bba 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.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 @@ */ @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 4cb6a7c41d15..35dab24d45a3 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,10 @@ 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; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -30,14 +33,21 @@ 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; @@ -54,32 +64,60 @@ static class ConnectionDetails { @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. + */ + @SuppressWarnings("unused") + 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; } @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(); + } Assert.state(endpoint != null, "'endpoint' must not be null"); return endpoint; } @@ -105,17 +143,28 @@ public String getUrl(Transport transport) { static class Exporters { @Bean - @ConditionalOnProperty(name = "management.opentelemetry.tracing.export.otlp.transport", havingValue = "http", - matchIfMissing = true) - OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, + @Conditional(HttpTransportCondition.class) + 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); + .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); + } + + 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)); @@ -123,16 +172,28 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, } @Bean - @ConditionalOnProperty(name = "management.opentelemetry.tracing.export.otlp.transport", havingValue = "grpc") - OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties, + @Conditional(GrpcTransportCondition.class) + 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); + .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); + } + + 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)); @@ -164,6 +225,38 @@ private interface SslContextConfigurer { } + 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 commonTransport = context.getEnvironment() + .getProperty("management.opentelemetry.otlp.transport"); + String activeTransport = (tracingTransport != null) ? tracingTransport + : (commonTransport != null) ? commonTransport : "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 commonTransport = context.getEnvironment() + .getProperty("management.opentelemetry.otlp.transport"); + String activeTransport = (tracingTransport != null) ? tracingTransport + : (commonTransport != null) ? commonTransport : "http"; + return new ConditionOutcome("grpc".equalsIgnoreCase(activeTransport), + "Transport is " + activeTransport); + } + + } + } } 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 50f47d4286a6..50fb4726327a 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,8 @@ 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.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.extension.trace.propagation.B3Propagator; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.resources.Resource; @@ -67,6 +69,7 @@ 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 +531,101 @@ void shouldPublishEventsWhenContextStorageIsInitializedEarly() { }); } + @Test + void shouldFallbackToCommonTransportForTracing() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.transport=grpc", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class); + assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class); + }); + } + + @Test + void shouldFallbackToCommonTimeoutForTracingExporter() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.timeout=10s", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldFallbackToCommonCompressionForTracingExporter() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.compression=gzip", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldPreferSignalSpecificTimeoutOverCommonTimeoutForTracing() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.timeout=10s", + "management.opentelemetry.tracing.export.otlp.timeout=3s", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldPreferSignalSpecificCompressionOverCommonCompressionForTracing() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.compression=gzip", + "management.opentelemetry.tracing.export.otlp.compression=none", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldPreferSignalSpecificConnectTimeoutOverCommonConnectTimeoutForTracing() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.connect-timeout=10s", + "management.opentelemetry.tracing.export.otlp.connect-timeout=3s", + "management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @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).isNotNull(); + }); + } + private void initializeOpenTelemetry(ConfigurableApplicationContext context) { context.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener()); Span.current(); 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 000000000000..0892bf9cdd96 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OtlpProperties.java @@ -0,0 +1,138 @@ +/* + * 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.time.Duration; +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.0.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<>(); + + /** + * Timeout for executing requests. + */ + private @Nullable Duration timeout; + + /** + * Connection timeout. + */ + private @Nullable Duration connectTimeout; + + /** + * Transport to use. + */ + private @Nullable Transport transport; + + /** + * Compression to use. + */ + 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 Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(@Nullable Duration timeout) { + this.timeout = timeout; + } + + public @Nullable Duration getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(@Nullable Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public @Nullable Transport getTransport() { + return this.transport; + } + + public void setTransport(@Nullable Transport transport) { + this.transport = transport; + } + + public @Nullable Compression getCompression() { + return this.compression; + } + + public void setCompression(@Nullable Compression compression) { + this.compression = compression; + } + + public enum Transport { + + /** + * HTTP transport. + */ + HTTP, + + /** + * gRPC transport. + */ + GRPC + + } + + public enum Compression { + + /** + * No compression. + */ + NONE, + + /** + * GZIP compression. + */ + GZIP + + } + +} 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 571632b0544d..622bee40ac09 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.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 @@ */ @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 0722701f2a45..4a8e31d44694 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,10 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; +import java.time.Duration; +import java.util.LinkedHashMap; import java.util.Locale; +import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -30,15 +33,22 @@ 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.ConditionalOnClass; 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.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.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; @@ -55,32 +65,61 @@ static class ConnectionDetails { @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") + @SuppressWarnings("unused") + static class LoggingEndpoint { + + } + + @ConditionalOnProperty("management.opentelemetry.otlp.endpoint") + @SuppressWarnings("unused") + 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; } @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(); + } Assert.state(endpoint != null, "'endpoint' must not be null"); return endpoint; } @@ -107,17 +146,30 @@ public String getUrl(Transport transport) { static class Exporters { @Bean - @ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "http", - matchIfMissing = true) + @Conditional(HttpTransportCondition.class) 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); + .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); + } + + 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)); @@ -125,16 +177,29 @@ OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties proper } @Bean - @ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "grpc") + @Conditional(GrpcTransportCondition.class) 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); + .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.US); + if (StringUtils.hasLength(compression)) { + builder.setCompression(compression); + } + + 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)); @@ -166,6 +231,38 @@ private interface SslContextConfigurer { } + static class HttpTransportCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + String loggingTransport = context.getEnvironment() + .getProperty("management.opentelemetry.logging.export.otlp.transport"); + String commonTransport = context.getEnvironment() + .getProperty("management.opentelemetry.otlp.transport"); + String activeTransport = (loggingTransport != null) ? loggingTransport + : (commonTransport != null) ? commonTransport : "http"; + return new ConditionOutcome("http".equalsIgnoreCase(activeTransport), + "Transport is " + activeTransport); + } + + } + + static class GrpcTransportCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + String loggingTransport = context.getEnvironment() + .getProperty("management.opentelemetry.logging.export.otlp.transport"); + String commonTransport = context.getEnvironment() + .getProperty("management.opentelemetry.otlp.transport"); + String activeTransport = (loggingTransport != null) ? loggingTransport + : (commonTransport != null) ? commonTransport : "http"; + return new ConditionOutcome("grpc".equalsIgnoreCase(activeTransport), + "Transport is " + activeTransport); + } + + } + } } 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 6df0b728035d..ba11c2babb83 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 @@ -22,6 +22,8 @@ import java.util.concurrent.atomic.AtomicInteger; import io.opentelemetry.context.Context; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.LogLimits; import io.opentelemetry.sdk.logs.LogRecordProcessor; @@ -37,6 +39,9 @@ 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 +181,112 @@ void shouldConfigureBatchLogRecordProcessorWithProperties() { }); } + @Test + void shouldFallbackToCommonTransportForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.transport=grpc", + "management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpGrpcLogRecordExporter.class); + assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class); + }); + } + + @Test + void shouldFallbackToCommonTimeoutForLoggingExporter() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.timeout=5s", + "management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldPreferSignalSpecificTransportOverCommonTransportForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.transport=grpc", + "management.opentelemetry.logging.export.otlp.transport=http", + "management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + assertThat(context).doesNotHaveBean(OtlpGrpcLogRecordExporter.class); + }); + } + + @Test + void shouldPreferSignalSpecificTimeoutOverCommonTimeoutForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.timeout=10s", + "management.opentelemetry.logging.export.otlp.timeout=3s", + "management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldPreferSignalSpecificCompressionOverCommonCompressionForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.compression=gzip", + "management.opentelemetry.logging.export.otlp.compression=none", + "management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @Test + void shouldPreferSignalSpecificConnectTimeoutOverCommonConnectTimeoutForLogging() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.otlp.connect-timeout=10s", + "management.opentelemetry.logging.export.otlp.connect-timeout=3s", + "management.opentelemetry.logging.export.otlp.endpoint=http://localhost:4317") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).isNotNull(); + }); + } + + @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).isNotNull(); + }); + } + @Configuration(proxyBeanMethods = false) static class UserConfiguration {