Align OTLP signal-specific properties with common fallback configuration

See gh-50461

Signed-off-by: somiljain2006 <somil16022006@gmail.com>
This commit is contained in:
somiljain2006
2026-08-24 14:09:52 +02:00
committed by Moritz Halbritter
parent 2051b60a1a
commit faf93946cd
13 changed files with 483 additions and 55 deletions
@@ -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<java.lang.String,java.lang.String>",
"description": "Custom headers to be appended to OTLP requests."
},
{
"name": "management.server.add-application-context-header",
"type": "java.lang.Boolean",
@@ -39,6 +39,7 @@ import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConf
import org.springframework.boot.micrometer.metrics.autoconfigure.export.ConditionalOnEnabledMetricsExport;
import org.springframework.boot.micrometer.metrics.autoconfigure.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.thread.Threading;
@@ -61,7 +62,7 @@ import org.springframework.util.StringUtils;
@ConditionalOnBean(Clock.class)
@ConditionalOnClass({ OtlpMeterRegistry.class, OpenTelemetryProperties.class })
@ConditionalOnEnabledMetricsExport("otlp")
@EnableConfigurationProperties({ OtlpMetricsProperties.class, OpenTelemetryProperties.class })
@EnableConfigurationProperties({ OtlpMetricsProperties.class, OpenTelemetryProperties.class, OtlpProperties.class })
public final class OtlpMetricsExportAutoConfiguration {
private final OtlpMetricsProperties properties;
@@ -72,16 +73,17 @@ public final class OtlpMetricsExportAutoConfiguration {
@Bean
@ConditionalOnMissingBean
OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
return new PropertiesOtlpMetricsConnectionDetails(this.properties, sslBundles.getIfAvailable());
OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(OtlpProperties otlpProperties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesOtlpMetricsConnectionDetails(this.properties, otlpProperties, sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
OtlpConfig otlpConfig(OpenTelemetryProperties openTelemetryProperties,
OtlpConfig otlpConfig(OtlpProperties otlpProperties, OpenTelemetryProperties openTelemetryProperties,
OtlpMetricsConnectionDetails connectionDetails, Environment environment) {
return new OtlpMetricsPropertiesConfigAdapter(this.properties, openTelemetryProperties, connectionDetails,
environment);
return new OtlpMetricsPropertiesConfigAdapter(this.properties, otlpProperties, openTelemetryProperties,
connectionDetails, environment);
}
@Bean
@@ -129,16 +131,27 @@ public final class OtlpMetricsExportAutoConfiguration {
private final OtlpMetricsProperties properties;
private final OtlpProperties otlpProperties;
private final @Nullable SslBundles sslBundles;
PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties, @Nullable SslBundles sslBundles) {
PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties, OtlpProperties otlpProperties,
@Nullable SslBundles sslBundles) {
this.properties = properties;
this.otlpProperties = otlpProperties;
this.sslBundles = sslBundles;
}
@Override
public @Nullable String getUrl() {
return this.properties.getUrl();
if (StringUtils.hasLength(this.properties.getUrl())) {
return this.properties.getUrl();
}
String endpoint = this.otlpProperties.getEndpoint();
if (StringUtils.hasLength(endpoint)) {
return endpoint.endsWith("/") ? endpoint + "v1/metrics" : endpoint + "/v1/metrics";
}
return null;
}
@Override
@@ -30,6 +30,7 @@ import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.Otl
import org.springframework.boot.micrometer.metrics.autoconfigure.export.properties.StepRegistryPropertiesConfigAdapter;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryResourceAttributes;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
@@ -43,16 +44,19 @@ import org.springframework.util.CollectionUtils;
class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter<OtlpMetricsProperties>
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 @@ class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAda
@Override
public Map<String, String> headers() {
return obtain(OtlpMetricsProperties::getHeaders, OtlpConfig.super::headers);
Map<String, String> headers = new LinkedHashMap<>(this.otlpProperties.getHeaders());
headers.putAll(obtain(OtlpMetricsProperties::getHeaders, OtlpConfig.super::headers));
return Collections.unmodifiableMap(headers);
}
@Override
@@ -265,6 +265,17 @@ class OtlpMetricsExportAutoConfigurationTests {
});
}
@Test
void testUrlFallbackToCommonOtlpEndpoint() {
this.contextRunner.withUserConfiguration(BaseConfiguration.class)
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://common-host:4318")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpConfig.class);
OtlpConfig config = context.getBean(OtlpConfig.class);
assertThat(config.url()).isEqualTo("http://common-host:4318/v1/metrics");
});
}
private HttpClient extractHttpClient(OtlpHttpMetricsSender metricsSender) {
Object field = ReflectionTestUtils.getField(metricsSender, "httpSender");
assertThat(field).isNotNull();
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails;
import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsProperties.Meter;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@@ -44,6 +45,8 @@ class OtlpMetricsPropertiesConfigAdapterTests {
private OtlpMetricsProperties properties;
private OtlpProperties otlpProperties;
private OpenTelemetryProperties openTelemetryProperties;
private MockEnvironment environment;
@@ -53,9 +56,10 @@ class OtlpMetricsPropertiesConfigAdapterTests {
@BeforeEach
void setUp() {
this.properties = new OtlpMetricsProperties();
this.otlpProperties = new OtlpProperties();
this.openTelemetryProperties = new OpenTelemetryProperties();
this.environment = new MockEnvironment();
this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties, null);
this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties, this.otlpProperties, null);
}
@Test
@@ -238,8 +242,41 @@ class OtlpMetricsPropertiesConfigAdapterTests {
}
private OtlpMetricsPropertiesConfigAdapter createAdapter() {
return new OtlpMetricsPropertiesConfigAdapter(this.properties, this.openTelemetryProperties,
this.connectionDetails, this.environment);
return new OtlpMetricsPropertiesConfigAdapter(this.properties, this.otlpProperties,
this.openTelemetryProperties, this.connectionDetails, this.environment);
}
@Test
void whenPropertiesHeadersIsNotSetThenUseOtlpPropertiesHeadersAsFallback() {
this.otlpProperties.getHeaders().put("common-header", "common-value");
assertThat(createAdapter().headers()).containsEntry("common-header", "common-value");
}
@Test
void whenPropertiesHeadersIsSetThenMergeWithOtlpPropertiesHeaders() {
this.otlpProperties.getHeaders().put("common-header", "common-value");
this.properties.setHeaders(Map.of("signal-header", "signal-value"));
assertThat(createAdapter().headers()).containsEntry("common-header", "common-value")
.containsEntry("signal-header", "signal-value");
}
@Test
void whenPropertiesUrlIsNotSetThenUseOtlpPropertiesEndpointAsFallbackWithAppendix() {
this.otlpProperties.setEndpoint("http://common-endpoint:4318");
assertThat(createAdapter().url()).isEqualTo("http://common-endpoint:4318/v1/metrics");
}
@Test
void whenPropertiesUrlIsNotSetThenUseOtlpPropertiesEndpointAsFallbackWithAppendixAndTrailingSlash() {
this.otlpProperties.setEndpoint("http://common-endpoint:4318/");
assertThat(createAdapter().url()).isEqualTo("http://common-endpoint:4318/v1/metrics");
}
@Test
void whenPropertiesUrlIsSetThenItOverridesOtlpPropertiesEndpoint() {
this.otlpProperties.setEndpoint("http://common-endpoint:4318");
this.properties.setUrl("http://signal-endpoint:4318/custom/metrics");
assertThat(createAdapter().url()).isEqualTo("http://signal-endpoint:4318/custom/metrics");
}
}
@@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.context.annotation.Import;
/**
@@ -48,7 +49,7 @@ import org.springframework.context.annotation.Import;
*/
@AutoConfiguration
@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class })
@EnableConfigurationProperties(OtlpTracingProperties.class)
@EnableConfigurationProperties({ OtlpTracingProperties.class, OtlpProperties.class })
@Import({ OtlpTracingConfigurations.ConnectionDetails.class, OtlpTracingConfigurations.Exporters.class })
public final class OtlpTracingAutoConfiguration {
@@ -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 io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.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 @@ final class OtlpTracingConfigurations {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("management.opentelemetry.tracing.export.otlp.endpoint")
@Conditional(OtlpEndpointCondition.class)
OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpTracingProperties properties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesOtlpTracingConnectionDetails(properties, sslBundles.getIfAvailable());
OtlpProperties otlpProperties, ObjectProvider<SslBundles> 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 @@ final class OtlpTracingConfigurations {
@Bean
@ConditionalOnProperty(name = "management.opentelemetry.tracing.export.otlp.transport", havingValue = "http",
matchIfMissing = true)
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties,
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, OtlpProperties otlpProperties,
OtlpTracingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpHttpSpanExporterBuilderCustomizer> 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<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -124,15 +178,27 @@ final class OtlpTracingConfigurations {
@Bean
@ConditionalOnProperty(name = "management.opentelemetry.tracing.export.otlp.transport", havingValue = "grpc")
OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties,
OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties, OtlpProperties otlpProperties,
OtlpTracingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpGrpcSpanExporterBuilderCustomizer> 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<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -164,6 +230,32 @@ final class OtlpTracingConfigurations {
}
static class HttpTransportCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String tracingTransport = context.getEnvironment()
.getProperty("management.opentelemetry.tracing.export.otlp.transport");
String activeTransport = (tracingTransport != null) ? tracingTransport : "http";
return new ConditionOutcome("http".equalsIgnoreCase(activeTransport),
"Transport is " + activeTransport);
}
}
static class GrpcTransportCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String tracingTransport = context.getEnvironment()
.getProperty("management.opentelemetry.tracing.export.otlp.transport");
String activeTransport = (tracingTransport != null) ? tracingTransport : "http";
return new ConditionOutcome("grpc".equalsIgnoreCase(activeTransport),
"Transport is " + activeTransport);
}
}
}
}
@@ -48,6 +48,7 @@ import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.resources.Resource;
@@ -67,6 +68,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration;
import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration;
import org.springframework.boot.micrometer.tracing.brave.autoconfigure.BraveAutoConfiguration;
import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -528,6 +530,57 @@ class OpenTelemetryTracingAutoConfigurationTests {
});
}
@Test
void shouldMergeCommonAndSignalSpecificHeadersForTracing() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class))
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318",
"management.opentelemetry.otlp.headers.common-header=common-value",
"management.opentelemetry.otlp.headers.shared-header=common-wins",
"management.opentelemetry.tracing.export.otlp.headers.tracing-header=tracing-value",
"management.opentelemetry.tracing.export.otlp.headers.shared-header=tracing-wins")
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(exporter).extracting("delegate.httpSender.headerSupplier")
.asInstanceOf(InstanceOfAssertFactories.type(Supplier.class))
.satisfies((headerSupplier) -> assertThat(headerSupplier.get())
.asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class))
.containsEntry("common-header", List.of("common-value"))
.containsEntry("tracing-header", List.of("tracing-value"))
.containsEntry("shared-header", List.of("tracing-wins")));
});
}
@Test
void shouldAppendTracesPathToCommonEndpoint() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class))
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318")
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(exporter).extracting("delegate.httpSender.url")
.extracting(Object::toString)
.isEqualTo("http://localhost:4318/v1/traces");
});
}
@Test
void shouldNotAppendTracesPathToTracingSpecificEndpoint() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class))
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318",
"management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4318/custom/traces")
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(exporter).extracting("delegate.httpSender.url")
.extracting(Object::toString)
.isEqualTo("http://localhost:4318/custom/traces");
});
}
private void initializeOpenTelemetry(ConfigurableApplicationContext context) {
context.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener());
Span.current();
@@ -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<String, String> headers = new LinkedHashMap<>();
public @Nullable String getEndpoint() {
return this.endpoint;
}
public void setEndpoint(@Nullable String endpoint) {
this.endpoint = endpoint;
}
public Map<String, String> getHeaders() {
return this.headers;
}
}
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails;
import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.Exporters;
import org.springframework.context.annotation.Import;
@@ -35,7 +36,7 @@ import org.springframework.context.annotation.Import;
*/
@AutoConfiguration
@ConditionalOnClass({ OpenTelemetry.class, SdkLoggerProvider.class })
@EnableConfigurationProperties(OtlpLoggingProperties.class)
@EnableConfigurationProperties({ OtlpLoggingProperties.class, OtlpProperties.class })
@Import({ ConnectionDetails.class, Exporters.class })
public final class OtlpLoggingAutoConfiguration {
@@ -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 io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.boot.opentelemetry.autoconfigure.logging.ConditionalOnEnabledLoggingExport;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -55,32 +61,64 @@ final class OtlpLoggingConfigurations {
@Bean
@ConditionalOnMissingBean(OtlpLoggingConnectionDetails.class)
@ConditionalOnProperty("management.opentelemetry.logging.export.otlp.endpoint")
@Conditional(OtlpEndpointCondition.class)
PropertiesOtlpLoggingConnectionDetails openTelemetryLoggingConnectionDetails(OtlpLoggingProperties properties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesOtlpLoggingConnectionDetails(properties, sslBundles.getIfAvailable());
OtlpProperties otlpProperties, ObjectProvider<SslBundles> 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 @@ final class OtlpLoggingConfigurations {
@ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "http",
matchIfMissing = true)
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties,
OtlpLoggingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
OtlpProperties otlpProperties, OtlpLoggingConnectionDetails connectionDetails,
ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpHttpLogRecordExporterBuilderCustomizer> 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<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -127,14 +175,24 @@ final class OtlpLoggingConfigurations {
@Bean
@ConditionalOnProperty(name = "management.opentelemetry.logging.export.otlp.transport", havingValue = "grpc")
OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OtlpLoggingProperties properties,
OtlpLoggingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
OtlpProperties otlpProperties, OtlpLoggingConnectionDetails connectionDetails,
ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpGrpcLogRecordExporterBuilderCustomizer> 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<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -18,10 +18,13 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import io.opentelemetry.context.Context;
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.logs.LogLimits;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
@@ -31,12 +34,16 @@ import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@@ -176,6 +183,50 @@ class OpenTelemetryLoggingAutoConfigurationTests {
});
}
@Test
void shouldPreferSignalSpecificEndpointOverCommonEndpointForLogging() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class))
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://common-host:4318",
"management.opentelemetry.logging.export.otlp.endpoint=http://logging-host:4318")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class);
OtlpLoggingConnectionDetails connectionDetails = context.getBean(OtlpLoggingConnectionDetails.class);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://logging-host:4318");
});
}
@Test
void shouldMergeCommonAndSignalSpecificHeadersForLogging() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class))
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://localhost:4318",
"management.opentelemetry.otlp.headers.common-header=common-value",
"management.opentelemetry.otlp.headers.shared-header=common-wins",
"management.opentelemetry.logging.export.otlp.headers.logging-header=logging-value",
"management.opentelemetry.logging.export.otlp.headers.shared-header=logging-wins")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class);
OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class);
assertThat(exporter).extracting("delegate.httpSender.headerSupplier")
.asInstanceOf(InstanceOfAssertFactories.type(Supplier.class))
.satisfies((headerSupplier) -> assertThat(headerSupplier.get())
.asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class))
.containsEntry("common-header", List.of("common-value"))
.containsEntry("logging-header", List.of("logging-value"))
.containsEntry("shared-header", List.of("logging-wins")));
});
}
@Test
void shouldFallbackToCommonEndpointAndAppendPathForLogging() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class))
.withPropertyValues("management.opentelemetry.otlp.endpoint=http://common-host:4318")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class);
OtlpLoggingConnectionDetails connectionDetails = context.getBean(OtlpLoggingConnectionDetails.class);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://common-host:4318/v1/logs");
});
}
@Configuration(proxyBeanMethods = false)
static class UserConfiguration {
@@ -38,6 +38,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.logging.SdkLoggerProviderBuilderCustomizer;
import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails;
@@ -321,6 +322,43 @@ class OtlpLoggingAutoConfigurationTests {
});
}
@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 {