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..789d8e694392 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,16 @@ "description": "Whether auto-configuration of logging is enabled to export logs.", "defaultValue": true }, + { + "name": "management.opentelemetry.otlp.endpoint", + "type": "java.lang.String", + "description": "OTLP target endpoint URL." + }, + { + "name": "management.opentelemetry.otlp.headers", + "type": "java.util.Map", + "description": "Custom headers to be appended to OTLP requests." + }, { "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..1933816b85a0 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,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 @@ 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..2255b2be8b89 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.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 @@ 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 +92,9 @@ 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 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..39be71c518d0 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.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,41 @@ 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 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"); } } 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..a33a0c9f6c8e 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,64 @@ 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. + */ + static class OtlpEndpointCondition extends AnyNestedCondition { + + OtlpEndpointCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint") + @SuppressWarnings("unused") + static class TracingEndpoint { + + } + + @ConditionalOnProperty("management.opentelemetry.otlp.endpoint") + @SuppressWarnings("unused") + 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(); + 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 +149,27 @@ static class Exporters { @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); + .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)); @@ -124,15 +178,27 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, @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); + .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 +230,32 @@ 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 activeTransport = (tracingTransport != null) ? tracingTransport : "http"; + return new ConditionOutcome("http".equalsIgnoreCase(activeTransport), + "Transport is " + activeTransport); + } + + } + + static class GrpcTransportCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + String tracingTransport = context.getEnvironment() + .getProperty("management.opentelemetry.tracing.export.otlp.transport"); + String activeTransport = (tracingTransport != null) ? tracingTransport : "http"; + return new ConditionOutcome("grpc".equalsIgnoreCase(activeTransport), + "Transport is " + activeTransport); + } + + } + } } 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..9e86db47034a 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.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.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 @@ void shouldPublishEventsWhenContextStorageIsInitializedEarly() { }); } + @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-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..1aed3ee1b89b --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OtlpProperties.java @@ -0,0 +1,57 @@ +/* + * 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<>(); + + public @Nullable String getEndpoint() { + return this.endpoint; + } + + public void setEndpoint(@Nullable String endpoint) { + this.endpoint = endpoint; + } + + public Map getHeaders() { + return this.headers; + } + +} 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..e511d357f798 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,14 +33,17 @@ 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,32 +61,64 @@ 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(); + 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,24 @@ static class Exporters { @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); + .setEndpoint(connectionDetails.getUrl(Transport.HTTP)); + + Duration timeout = properties.getTimeout(); + builder.setTimeout(timeout); + + String compression = properties.getCompression().name().toLowerCase(Locale.ROOT); + if (StringUtils.hasLength(compression)) { + builder.setCompression(compression); + } + + 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,14 +175,24 @@ OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties proper @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); + .setEndpoint(connectionDetails.getUrl(Transport.GRPC)); + + Duration timeout = properties.getTimeout(); + builder.setTimeout(timeout); + + String compression = properties.getCompression().name().toLowerCase(Locale.ROOT); + if (StringUtils.hasLength(compression)) { + builder.setCompression(compression); + } + + 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)); 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..02ba84dc1bfa 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 @@ 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.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 @@ void shouldConfigureBatchLogRecordProcessorWithProperties() { }); } + @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/OtlpLoggingAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java index 2839b3c9b9d1..800f68bd9ef9 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.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,43 @@ void whenCustomConnectionDetailsProvidesSslBundleConfiguresHttpExporter() { }); } + @Test + void shouldAppendLogsPathToCommonEndpoint() { + 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"); + 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 {