See gh-50461
This commit is contained in:
Moritz Halbritter
2026-08-25 08:45:47 +02:00
parent e694cf15d9
commit 7a6be52eae
10 changed files with 265 additions and 76 deletions
@@ -144,14 +144,7 @@ public final class OtlpMetricsExportAutoConfiguration {
@Override
public @Nullable String 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;
return this.otlpProperties.resolveEndpoint(this.properties.getUrl(), "v1/metrics");
}
@Override
@@ -79,18 +79,11 @@ class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAda
@Override
public CompressionMode compressionMode() {
CompressionMode compressionMode = this.properties.getCompressionMode();
if (compressionMode != null) {
return compressionMode;
}
OtlpProperties.Compression compressionProperties = this.otlpProperties.getCompression();
if (compressionProperties != null) {
return switch (compressionProperties) {
case GZIP -> CompressionMode.GZIP;
case NONE -> CompressionMode.NONE;
};
}
return OtlpConfig.super.compressionMode();
return this.otlpProperties.resolveCompression(this.properties.getCompressionMode(),
OtlpConfig.super.compressionMode(), (commonCompression) -> switch (commonCompression) {
case GZIP -> CompressionMode.GZIP;
case NONE -> CompressionMode.NONE;
});
}
@Override
@@ -103,8 +96,8 @@ class OtlpMetricsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAda
@Override
public Map<String, String> headers() {
Map<String, String> headers = new LinkedHashMap<>(this.otlpProperties.getHeaders());
headers.putAll(obtain(OtlpMetricsProperties::getHeaders, OtlpConfig.super::headers));
Map<String, String> headers = this.otlpProperties
.mergeHeaders(obtain(OtlpMetricsProperties::getHeaders, OtlpConfig.super::headers));
return Collections.unmodifiableMap(headers);
}
@@ -1821,6 +1821,12 @@
"since": "3.0.0"
}
},
{
"name": "management.otlp.metrics.export.compression-mode",
"type": "org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsProperties$CompressionMode",
"description": "Compression mode to use when exporting metrics.",
"defaultValue": "none"
},
{
"name": "management.promethus.metrics.export.pushgateway.base-url",
"type": "java.lang.String",
@@ -16,9 +16,7 @@
package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
@@ -111,13 +109,8 @@ final class OtlpTracingConfigurations {
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";
}
}
String path = (transport == Transport.HTTP) ? "v1/traces" : null;
String endpoint = this.otlpProperties.resolveEndpoint(this.properties.getEndpoint(), path);
Assert.state(endpoint != null, "'endpoint' must not be null");
return endpoint;
}
@@ -153,9 +146,7 @@ final class OtlpTracingConfigurations {
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.ROOT));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
otlpProperties.mergeHeaders(properties.getHeaders()).forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -172,9 +163,7 @@ final class OtlpTracingConfigurations {
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.ROOT));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
otlpProperties.mergeHeaders(properties.getHeaders()).forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -183,18 +172,11 @@ final class OtlpTracingConfigurations {
private OtlpTracingProperties.Compression resolveCompression(OtlpTracingProperties properties,
OtlpProperties otlpProperties) {
OtlpTracingProperties.Compression compression = properties.getCompression();
if (compression != null) {
return compression;
}
OtlpProperties.Compression compressionProperties = otlpProperties.getCompression();
if (compressionProperties != null) {
return switch (compressionProperties) {
case GZIP -> OtlpTracingProperties.Compression.GZIP;
case NONE -> OtlpTracingProperties.Compression.NONE;
};
}
return OtlpTracingProperties.Compression.NONE;
return otlpProperties.resolveCompression(properties.getCompression(),
OtlpTracingProperties.Compression.NONE, (commonCompression) -> switch (commonCompression) {
case GZIP -> OtlpTracingProperties.Compression.GZIP;
case NONE -> OtlpTracingProperties.Compression.NONE;
});
}
private void configureSsl(OtlpTracingConnectionDetails connectionDetails,
@@ -1,6 +1,12 @@
{
"groups": [],
"properties": [
{
"name": "management.opentelemetry.tracing.export.otlp.compression",
"type": "org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingProperties$Compression",
"description": "Method used to compress the payload.",
"defaultValue": "none"
},
{
"name": "management.otlp.tracing.compression",
"deprecation": {
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.otlp.OtlpTracingConfigurations.ConnectionDetails.PropertiesOtlpTracingConnectionDetails;
import org.springframework.boot.opentelemetry.autoconfigure.OtlpProperties;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -199,6 +200,49 @@ class OtlpTracingAutoConfigurationTests {
.run((context) -> assertThat(context).hasSingleBean(PropertiesOtlpTracingConnectionDetails.class));
}
@Test
void shouldAppendTracesPathToCommonEndpointForHttpTransport() {
OtlpTracingProperties properties = new OtlpTracingProperties();
properties.setTransport(Transport.HTTP);
OtlpProperties otlpProperties = new OtlpProperties();
otlpProperties.setEndpoint("http://localhost:4318");
PropertiesOtlpTracingConnectionDetails connectionDetails = new PropertiesOtlpTracingConnectionDetails(
properties, otlpProperties, null);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://localhost:4318/v1/traces");
}
@Test
void shouldNotAppendTracesPathToCommonEndpointForGrpcTransport() {
OtlpTracingProperties properties = new OtlpTracingProperties();
properties.setTransport(Transport.GRPC);
OtlpProperties otlpProperties = new OtlpProperties();
otlpProperties.setEndpoint("http://localhost:4318");
PropertiesOtlpTracingConnectionDetails connectionDetails = new PropertiesOtlpTracingConnectionDetails(
properties, otlpProperties, null);
assertThat(connectionDetails.getUrl(Transport.GRPC)).isEqualTo("http://localhost:4318");
}
@Test
void shouldNotAppendTracesPathToTracingSpecificEndpoint() {
OtlpTracingProperties properties = new OtlpTracingProperties();
properties.setEndpoint("http://localhost:4318/custom/traces");
OtlpProperties otlpProperties = new OtlpProperties();
otlpProperties.setEndpoint("http://localhost:4318");
PropertiesOtlpTracingConnectionDetails connectionDetails = new PropertiesOtlpTracingConnectionDetails(
properties, otlpProperties, null);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://localhost:4318/custom/traces");
}
@Test
void shouldAppendTracesPathToCommonEndpointWithTrailingSlash() {
OtlpTracingProperties properties = new OtlpTracingProperties();
OtlpProperties otlpProperties = new OtlpProperties();
otlpProperties.setEndpoint("http://localhost:4318/");
PropertiesOtlpTracingConnectionDetails connectionDetails = new PropertiesOtlpTracingConnectionDetails(
properties, otlpProperties, null);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://localhost:4318/v1/traces");
}
@Test
void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() {
this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> {
@@ -18,10 +18,12 @@ package org.springframework.boot.opentelemetry.autoconfigure;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
/**
* Common configuration properties for OpenTelemetry Protocol (OTLP) exporters.
@@ -67,6 +69,57 @@ public class OtlpProperties {
this.compression = compression;
}
/**
* Resolves the endpoint to use, falling back to this common endpoint (with
* {@code path} appended) when {@code signalEndpoint} is not set.
* @param signalEndpoint the signal-specific endpoint, or {@code null} if not set
* @param path the path to append to the common endpoint when used as a fallback, or
* {@code null} if no path should be appended
* @return the resolved endpoint, or {@code null} if neither endpoint is set
*/
public @Nullable String resolveEndpoint(@Nullable String signalEndpoint, @Nullable String path) {
if (StringUtils.hasLength(signalEndpoint)) {
return signalEndpoint;
}
if (this.endpoint == null || path == null) {
return this.endpoint;
}
return this.endpoint.endsWith("/") ? this.endpoint + path : this.endpoint + "/" + path;
}
/**
* Merges the given signal-specific headers with these common headers. Entries in
* {@code signalHeaders} take precedence over common headers with the same key.
* @param signalHeaders the signal-specific headers
* @return the merged headers
*/
public Map<String, String> mergeHeaders(Map<String, String> signalHeaders) {
Map<String, String> merged = new LinkedHashMap<>(this.headers);
merged.putAll(signalHeaders);
return merged;
}
/**
* Resolves the compression to use, falling back to this common compression, mapped
* through {@code mapper}, when {@code signalCompression} is not set.
* @param <T> the signal-specific compression type
* @param signalCompression the signal-specific compression, or {@code null} if not
* set
* @param defaultCompression the compression to use when neither is set
* @param mapper maps this common compression to the signal-specific type
* @return the resolved compression
*/
public <T> T resolveCompression(@Nullable T signalCompression, T defaultCompression,
Function<Compression, T> mapper) {
if (signalCompression != null) {
return signalCompression;
}
if (this.compression != null) {
return mapper.apply(this.compression);
}
return defaultCompression;
}
/**
* Compression methods.
*/
@@ -16,9 +16,7 @@
package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
@@ -112,13 +110,8 @@ final class OtlpLoggingConfigurations {
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";
}
}
String path = (transport == Transport.HTTP) ? "v1/logs" : null;
String endpoint = this.otlpProperties.resolveEndpoint(this.properties.getEndpoint(), path);
Assert.state(endpoint != null, "'endpoint' must not be null");
return endpoint;
}
@@ -156,9 +149,7 @@ final class OtlpLoggingConfigurations {
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.US));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
otlpProperties.mergeHeaders(properties.getHeaders()).forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -176,9 +167,7 @@ final class OtlpLoggingConfigurations {
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(resolveCompression(properties, otlpProperties).name().toLowerCase(Locale.US));
Map<String, String> headers = new LinkedHashMap<>(otlpProperties.getHeaders());
headers.putAll(properties.getHeaders());
headers.forEach(builder::addHeader);
otlpProperties.mergeHeaders(properties.getHeaders()).forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
configureSsl(connectionDetails, builder::setSslContext);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
@@ -187,18 +176,11 @@ final class OtlpLoggingConfigurations {
private OtlpLoggingProperties.Compression resolveCompression(OtlpLoggingProperties properties,
OtlpProperties otlpProperties) {
OtlpLoggingProperties.Compression compression = properties.getCompression();
if (compression != null) {
return compression;
}
OtlpProperties.Compression commonCompression = otlpProperties.getCompression();
if (commonCompression != null) {
return switch (commonCompression) {
case GZIP -> OtlpLoggingProperties.Compression.GZIP;
case NONE -> OtlpLoggingProperties.Compression.NONE;
};
}
return OtlpLoggingProperties.Compression.NONE;
return otlpProperties.resolveCompression(properties.getCompression(),
OtlpLoggingProperties.Compression.NONE, (commonCompression) -> switch (commonCompression) {
case GZIP -> OtlpLoggingProperties.Compression.GZIP;
case NONE -> OtlpLoggingProperties.Compression.NONE;
});
}
private void configureSsl(OtlpLoggingConnectionDetails connectionDetails,
@@ -7,6 +7,12 @@
"defaultValue": true,
"description": "Whether auto-configuration of logging is enabled to export logs over OTLP."
},
{
"name": "management.opentelemetry.logging.export.otlp.compression",
"type": "org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingProperties$Compression",
"description": "Method used to compress the payload.",
"defaultValue": "none"
},
{
"name": "management.opentelemetry.map-environment-variables",
"type": "java.lang.Boolean",
@@ -0,0 +1,124 @@
/*
* 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.Map;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OtlpProperties}.
*
* @author Moritz Halbritter
*/
class OtlpPropertiesTests {
private final OtlpProperties properties = new OtlpProperties();
@Test
void shouldUseSignalEndpointWhenSet() {
this.properties.setEndpoint("http://common:4318");
assertThat(this.properties.resolveEndpoint("http://signal:4318/custom", "v1/traces"))
.isEqualTo("http://signal:4318/custom");
}
@Test
void shouldFallBackToCommonEndpointWithPathWhenSignalEndpointIsNotSet() {
this.properties.setEndpoint("http://common:4318");
assertThat(this.properties.resolveEndpoint(null, "v1/traces")).isEqualTo("http://common:4318/v1/traces");
}
@Test
void shouldFallBackToCommonEndpointWithPathWhenSignalEndpointIsEmpty() {
this.properties.setEndpoint("http://common:4318");
assertThat(this.properties.resolveEndpoint("", "v1/traces")).isEqualTo("http://common:4318/v1/traces");
}
@Test
void shouldNotDuplicateSlashWhenCommonEndpointHasTrailingSlash() {
this.properties.setEndpoint("http://common:4318/");
assertThat(this.properties.resolveEndpoint(null, "v1/traces")).isEqualTo("http://common:4318/v1/traces");
}
@Test
void shouldReturnCommonEndpointUnchangedWhenPathIsNull() {
this.properties.setEndpoint("http://common:4318");
assertThat(this.properties.resolveEndpoint(null, null)).isEqualTo("http://common:4318");
}
@Test
void shouldReturnNullWhenNeitherEndpointIsSet() {
assertThat(this.properties.resolveEndpoint(null, "v1/traces")).isNull();
}
@Test
void shouldMergeCommonAndSignalHeaders() {
this.properties.getHeaders().put("common-header", "common-value");
assertThat(this.properties.mergeHeaders(Map.of("signal-header", "signal-value")))
.containsEntry("common-header", "common-value")
.containsEntry("signal-header", "signal-value");
}
@Test
void shouldPreferSignalHeaderWhenKeyIsPresentInBoth() {
this.properties.getHeaders().put("shared-header", "common-value");
assertThat(this.properties.mergeHeaders(Map.of("shared-header", "signal-value"))).containsEntry("shared-header",
"signal-value");
}
@Test
void shouldReturnSignalHeadersWhenNoCommonHeadersAreSet() {
assertThat(this.properties.mergeHeaders(Map.of("signal-header", "signal-value")))
.containsExactly(Map.entry("signal-header", "signal-value"));
}
@Test
void shouldUseSignalCompressionWhenSet() {
this.properties.setCompression(OtlpProperties.Compression.GZIP);
assertThat(this.properties.resolveCompression(TestCompression.NONE, TestCompression.GZIP, this::map))
.isEqualTo(TestCompression.NONE);
}
@Test
void shouldFallBackToMappedCommonCompressionWhenSignalCompressionIsNotSet() {
this.properties.setCompression(OtlpProperties.Compression.GZIP);
assertThat(this.properties.resolveCompression(null, TestCompression.NONE, this::map))
.isEqualTo(TestCompression.GZIP);
}
@Test
void shouldReturnDefaultCompressionWhenNeitherIsSet() {
assertThat(this.properties.resolveCompression(null, TestCompression.NONE, this::map))
.isEqualTo(TestCompression.NONE);
}
private TestCompression map(OtlpProperties.Compression compression) {
return switch (compression) {
case GZIP -> TestCompression.GZIP;
case NONE -> TestCompression.NONE;
};
}
private enum TestCompression {
GZIP, NONE
}
}