From 166b88b9e197edc300fc6ce4e43993cdd13bfdba Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 12 Mar 2026 14:32:41 +0100 Subject: [PATCH] Add support for SslBundles to OTLP logging export Closes gh-49583 --- ...rComposeConnectionDetailsFactoryTests.java | 11 ++++ ...rComposeConnectionDetailsFactoryTests.java | 11 ++++ ...ontainerConnectionDetailsFactoryTests.java | 1 + ...ontainerConnectionDetailsFactoryTests.java | 1 + .../boot/opentelemetry/docker/compose/ca.crt | 32 ++++++++++ .../docker/compose/otlp-ssl-compose.yaml | 8 +++ .../otlp/OtlpLoggingConfigurations.java | 55 +++++++++++++++++- .../otlp/OtlpLoggingConnectionDetails.java | 12 ++++ .../logging/otlp/OtlpLoggingProperties.java | 23 ++++++++ ...DockerComposeConnectionDetailsFactory.java | 14 ++++- ...gingContainerConnectionDetailsFactory.java | 10 ++++ ...gingContainerConnectionDetailsFactory.java | 10 +++- .../OtlpLoggingAutoConfigurationTests.java | 55 ++++++++++++++++++ .../autoconfigure/logging/otlp/test.jks | Bin 0 -> 1276 bytes 14 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt create mode 100644 module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml create mode 100644 module/spring-boot-opentelemetry/src/test/resources/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/test.jks diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java index 8f7a08f51a1..b3a0f2911a8 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.opentelemetry.docker.compose; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -35,6 +36,16 @@ class GrafanaOtlpLoggingDockerComposeConnectionDetailsFactoryTests { void runCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/logs"); assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/logs"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("https://").endsWith("/v1/logs"); + assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("https://").endsWith("/v1/logs"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java index f4909682332..93eedf0bc4a 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/docker/compose/OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.opentelemetry.docker.compose; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -34,6 +35,16 @@ class OtelCollectorOtlpLoggingDockerComposeConnectionDetailsFactoryTests { void runCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/logs"); assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/logs"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.OTEL_COLLECTOR, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("https://").endsWith("/v1/logs"); + assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("https://").endsWith("/v1/logs"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java index 563755574a6..2cac2305170 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests.java @@ -55,6 +55,7 @@ class GrafanaOtlpLoggingContainerConnectionDetailsFactoryTests { .isEqualTo("%s/v1/logs".formatted(container.getOtlpGrpcUrl())); assertThat(this.connectionDetails.getUrl(Transport.HTTP)) .isEqualTo("%s/v1/logs".formatted(container.getOtlpHttpUrl())); + assertThat(this.connectionDetails.getSslBundle()).isNull(); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java index 69aab41c197..fc93129bb0e 100644 --- a/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java +++ b/module/spring-boot-opentelemetry/src/dockerTest/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests.java @@ -56,6 +56,7 @@ class OtelCollectorOltpLoggingContainerConnectionDetailsFactoryTests { .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4318) + "/v1/logs"); assertThat(this.connectionDetails.getUrl(Transport.GRPC)) .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4317) + "/v1/logs"); + assertThat(this.connectionDetails.getSslBundle()).isNull(); } @Configuration(proxyBeanMethods = false) diff --git a/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt new file mode 100644 index 00000000000..beed250b132 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUfIkk29IT9OpbgfjL8oRIPSLjUcAwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAusN2 +KzQQUUxZSiI3ZZuZohFwq2KXSUNPdJ6rgD3/YKNTDSZXKZPO53kYPP0DXf0sm3CH +cyWSWVabyimZYuPWena1MElSL4ZpJ9WwkZoOQ3bPFK1utz6kMOwrgAUcky8H/rIK +j2JEBhkSHUIGr57NjUEwG1ygaSerM8RzWw1PtMq+C8LOu3v94qzE3NDg1QRpyvV9 +OmsLsjISd0ZmAJNi9vmiEH923KnPyiqnQmWKpYicdgQmX1GXylS22jZqAwaOkYGj +X8UdeyvrohkZkM0hn9uaSufQGEW4yKACn3PkjJtzi8drBIyjIi9YcAzBxZB9oVKq +XZMlltgO2fDMmIJi0Ngt0Ci7fCoEMqSocKyDKML6YLr9UWtx4bfsrk+rVO9Q/D/v +8RKgstv7dCf2KWRX3ZJEC0IBHS5gLNq0qqqVcGx3LcSyhdiKJOtSwAnNkHMh+jSQ +xLSlBjcSqTPiGTRK/Rddl+xnU/mBgk7ZBGNrUFaD5McMFjddS7Ih82aHnpQ1gekW +nUGv+Tm/G68h2BvZ5U2q+RfeOCgRW9i/AYW2jgT7IFnfjyUXgBQveauMAchomqFE +VLe95ZgViF6vmH34EKo3w9L5TQiwk/r53YlM7TSOTyDqx66t4zGYDsVMicpKmzi4 +2Rp8EpErARRyREUIKSvWs9O9+uT3+7arNLgHe5ECAwEAAaOBgTB/MB0GA1UdDgQW +BBRVMLDVqPECWaH6GruL9E52VcTrPjAfBgNVHSMEGDAWgBRVMLDVqPECWaH6GruL +9E52VcTrPjAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAeSpjCL3j +2GIFBNKr/5amLOYa0kZ6r1dJs+K6xvMsUvsBJ/QQsV5nYDMIoV/NYUd8SyYV4lEj +7LHX5ZbmJrvPk30LGEBG/5Vy2MIATrQrQ14S4nXtEdSnBvTQwPOOaHc+2dTp3YpM +f4ffELKWyispTifx1eqdiUJhURKeQBh+3W7zpyaiN4vJaqEDKGgFQtHA/OyZL2hZ +BpxHB0zpb2iDHV8MeyfOT7HQWUk6p13vdYm6EnyJT8fzWvE+TqYNbqFmB+CLRSXy +R3p1yaeTd4LnVknJ0UBKqEyul3ziHZDhKhBpwdglYOQz4eWjSFhikX9XZ8NaI38Q +QqLZVn0DsH2ztkjrQrUVgK2xn4aUuqoLDk4Hu6h5baUn+f2GLuzx+EXc/i3ikYvw +Y3JyufOgw6nGGFG+/QXEj85XtLPhN7Wm42z2e/BGzi0MLl65sfpEDXvFTA72Yzws +OYaeg/HxeYwUHQgs2fKl/LgV4chntSCvTqfNl6OnQafD/ISJNpx3xWR3HwF+ypFG +UaLE+e1soqEJbzL31U/6pypHLsj8Y8r9hJbZXo2ibnhjFV6fypUAP0rbIzaoWcrJ +T0Sbliz+KQTMzCcubiAi4bI/kZ5FJ4kkaHqUpIWzlx1h2WVJ65ASFDjBWb8eVmB6 +Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU= +-----END CERTIFICATE----- diff --git a/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml new file mode 100644 index 00000000000..75707fa3e68 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/dockerTest/resources/org/springframework/boot/opentelemetry/docker/compose/otlp-ssl-compose.yaml @@ -0,0 +1,8 @@ +services: + otlp: + image: '{imageName}' + ports: + - '4317' + - '4318' + labels: + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' 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 4763b7affc6..0722701f2a4 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 @@ -18,11 +18,16 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; import java.util.Locale; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -30,9 +35,12 @@ 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.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.Configuration; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Configurations imported by {@link OtlpLoggingAutoConfiguration}. @@ -48,8 +56,9 @@ final class OtlpLoggingConfigurations { @Bean @ConditionalOnMissingBean(OtlpLoggingConnectionDetails.class) @ConditionalOnProperty("management.opentelemetry.logging.export.otlp.endpoint") - PropertiesOtlpLoggingConnectionDetails openTelemetryLoggingConnectionDetails(OtlpLoggingProperties properties) { - return new PropertiesOtlpLoggingConnectionDetails(properties); + PropertiesOtlpLoggingConnectionDetails openTelemetryLoggingConnectionDetails(OtlpLoggingProperties properties, + ObjectProvider sslBundles) { + return new PropertiesOtlpLoggingConnectionDetails(properties, sslBundles.getIfAvailable()); } /** @@ -59,8 +68,11 @@ final class OtlpLoggingConfigurations { private final OtlpLoggingProperties properties; - PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties) { + private final @Nullable SslBundles sslBundles; + + PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; } @Override @@ -73,6 +85,16 @@ final class OtlpLoggingConfigurations { return endpoint; } + @Override + public @Nullable SslBundle getSslBundle() { + String bundleName = this.properties.getSsl().getBundle(); + if (StringUtils.hasLength(bundleName)) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundleName); + } + return null; + } + } } @@ -97,6 +119,7 @@ final class OtlpLoggingConfigurations { .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + configureSsl(connectionDetails, builder::setSslContext); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -113,10 +136,36 @@ final class OtlpLoggingConfigurations { .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); meterProvider.ifAvailable(builder::setMeterProvider); + configureSsl(connectionDetails, builder::setSslContext); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } + private void configureSsl(OtlpLoggingConnectionDetails connectionDetails, + SslContextConfigurer sslContextConfigurer) { + SslBundle sslBundle = connectionDetails.getSslBundle(); + if (sslBundle != null) { + SSLContext sslContext = sslBundle.createSslContext(); + X509TrustManager trustManager = extractTrustManager(sslBundle); + sslContextConfigurer.configure(sslContext, trustManager); + } + } + + private X509TrustManager extractTrustManager(SslBundle sslBundle) { + for (TrustManager trustManager : sslBundle.getManagers().getTrustManagers()) { + if (trustManager instanceof X509TrustManager x509TrustManager) { + return x509TrustManager; + } + } + throw new IllegalStateException("No X509TrustManager found in the SSL bundle trust managers"); + } + + private interface SslContextConfigurer { + + void configure(SSLContext sslContext, X509TrustManager trustManager); + + } + } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java index a61350f7a3b..1ebe588cbc2 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingConnectionDetails.java @@ -16,7 +16,10 @@ package org.springframework.boot.opentelemetry.autoconfigure.logging.otlp; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to an OpenTelemetry logging service. @@ -33,4 +36,13 @@ public interface OtlpLoggingConnectionDetails extends ConnectionDetails { */ String getUrl(Transport transport); + /** + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} + * @since 4.1.0 + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java index 1d4ac6e7f77..569935763a6 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/OtlpLoggingProperties.java @@ -66,6 +66,8 @@ public class OtlpLoggingProperties { */ private final Map headers = new HashMap<>(); + private final Ssl ssl = new Ssl(); + public @Nullable String getEndpoint() { return this.endpoint; } @@ -110,6 +112,27 @@ public class OtlpLoggingProperties { return this.headers; } + public Ssl getSsl() { + return this.ssl; + } + + public static class Ssl { + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + public enum Compression { /** diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java index 160fec162ba..d457dc8c8a9 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/docker/compose/OtlpLoggingDockerComposeConnectionDetailsFactory.java @@ -16,11 +16,14 @@ package org.springframework.boot.opentelemetry.docker.compose; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create @@ -57,11 +60,14 @@ class OtlpLoggingDockerComposeConnectionDetailsFactory private final int httpPort; + private final @Nullable SslBundle sslBundle; + private OtlpLoggingDockerComposeConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.grpcPort = source.ports().get(OTLP_GRPC_PORT); this.httpPort = source.ports().get(OTLP_HTTP_PORT); + this.sslBundle = getSslBundle(source); } @Override @@ -70,7 +76,13 @@ class OtlpLoggingDockerComposeConnectionDetailsFactory case HTTP -> this.httpPort; case GRPC -> this.grpcPort; }; - return "http://%s:%d/v1/logs".formatted(this.host, port); + String scheme = (this.sslBundle != null) ? "https" : "http"; + return "%s://%s:%d/v1/logs".formatted(scheme, this.host, port); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java index b50188dbbcb..b773ac15e52 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/GrafanaOtlpLoggingContainerConnectionDetailsFactory.java @@ -16,10 +16,12 @@ package org.springframework.boot.opentelemetry.testcontainers; +import org.jspecify.annotations.Nullable; import org.testcontainers.grafana.LgtmStackContainer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -58,9 +60,17 @@ class GrafanaOtlpLoggingContainerConnectionDetailsFactory case HTTP -> getContainer().getOtlpHttpUrl(); case GRPC -> getContainer().getOtlpGrpcUrl(); }; + if (getSslBundle() != null) { + url = url.replaceFirst("^http://", "https://"); + } return "%s/v1/logs".formatted(url); } + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java index b65c8308acb..08e71ed6b35 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/testcontainers/OtelCollectorOltpLoggingContainerConnectionDetailsFactory.java @@ -16,11 +16,13 @@ package org.springframework.boot.opentelemetry.testcontainers; +import org.jspecify.annotations.Nullable; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.Transport; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -64,7 +66,13 @@ class OtelCollectorOltpLoggingContainerConnectionDetailsFactory case HTTP -> OTLP_HTTP_PORT; case GRPC -> OTLP_GRPC_PORT; }; - return "http://%s:%d/v1/logs".formatted(getContainer().getHost(), getContainer().getMappedPort(port)); + String scheme = (getSslBundle() != null) ? "https" : "http"; + return "%s://%s:%d/v1/logs".formatted(scheme, getContainer().getHost(), getContainer().getMappedPort(port)); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } 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 41313a95de1..2839b3c9b9d 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 @@ -20,6 +20,8 @@ import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import javax.net.ssl.X509TrustManager; + import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; @@ -33,13 +35,16 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfiguration; 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.logging.OpenTelemetryLoggingAutoConfiguration; import org.springframework.boot.opentelemetry.autoconfigure.logging.SdkLoggerProviderBuilderCustomizer; import org.springframework.boot.opentelemetry.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -266,6 +271,56 @@ class OtlpLoggingAutoConfigurationTests { }); } + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresHttpExporter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=https://localhost:4318/v1/logs", + "management.opentelemetry.logging.export.otlp.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.httpSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresGrpcExporter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues("management.opentelemetry.logging.export.otlp.endpoint=https://localhost:4318/v1/logs", + "management.opentelemetry.logging.export.otlp.transport=grpc", + "management.opentelemetry.logging.export.otlp.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpGrpcLogRecordExporter.class); + OtlpGrpcLogRecordExporter exporter = context.getBean(OtlpGrpcLogRecordExporter.class); + assertThat(exporter).extracting("delegate.grpcSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.grpcSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + + @Test + void whenCustomConnectionDetailsProvidesSslBundleConfiguresHttpExporter() { + SslBundle sslBundle = SslBundle.systemDefault(); + OtlpLoggingConnectionDetails connectionDetails = mock(OtlpLoggingConnectionDetails.class); + given(connectionDetails.getUrl(Transport.HTTP)).willReturn("https://localhost:4318/v1/logs"); + given(connectionDetails.getSslBundle()).willReturn(sslBundle); + this.contextRunner + .withBean("customOtlpLoggingConnectionDetails", OtlpLoggingConnectionDetails.class, () -> connectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporter exporter = context.getBean(OtlpHttpLogRecordExporter.class); + assertThat(exporter).extracting("delegate.httpSender.client.sslSocketFactoryOrNull").isNotNull(); + assertThat(exporter).extracting("delegate.httpSender.client.x509TrustManager") + .isInstanceOf(X509TrustManager.class); + }); + } + @Configuration(proxyBeanMethods = false) public static class MultipleSdkLoggerProviderBuilderCustomizersConfig { diff --git a/module/spring-boot-opentelemetry/src/test/resources/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/test.jks b/module/spring-boot-opentelemetry/src/test/resources/org/springframework/boot/opentelemetry/autoconfigure/logging/otlp/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS%