From 06272143bf20ce613c425450e34c7f498271b026 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 13 Mar 2026 09:51:57 +0100 Subject: [PATCH] Add support for SslBundles to OTLP metrics export Closes gh-49590 --- .../build.gradle | 1 + ...nectionDetailsFactoryIntegrationTests.java | 10 ++ ...nectionDetailsFactoryIntegrationTests.java | 10 ++ ...nectionDetailsFactoryIntegrationTests.java | 7 + ...nectionDetailsFactoryIntegrationTests.java | 5 + .../metrics/docker/compose/otlp/ca.crt | 32 +++++ .../docker/compose/otlp/otlp-ssl-compose.yaml | 8 ++ .../export/otlp/JdkClientHttpSender.java | 92 +++++++++++++ .../otlp/OtlpMetricsConnectionDetails.java | 11 ++ .../OtlpMetricsExportAutoConfiguration.java | 50 +++++-- .../export/otlp/OtlpMetricsProperties.java | 23 ++++ ...DockerComposeConnectionDetailsFactory.java | 14 +- ...ricsContainerConnectionDetailsFactory.java | 13 +- ...ricsContainerConnectionDetailsFactory.java | 11 +- .../export/otlp/JdkClientHttpSenderTests.java | 127 ++++++++++++++++++ ...lpMetricsExportAutoConfigurationTests.java | 92 ++++++++++++- ...lpMetricsPropertiesConfigAdapterTests.java | 2 +- .../autoconfigure/export/otlp/test.jks | Bin 0 -> 1276 bytes 18 files changed, 492 insertions(+), 16 deletions(-) create mode 100644 module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt create mode 100644 module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml create mode 100644 module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java create mode 100644 module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java create mode 100644 module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks diff --git a/module/spring-boot-micrometer-metrics/build.gradle b/module/spring-boot-micrometer-metrics/build.gradle index cfaa0830b33..bfa9a9c73d4 100644 --- a/module/spring-boot-micrometer-metrics/build.gradle +++ b/module/spring-boot-micrometer-metrics/build.gradle @@ -74,6 +74,7 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("io.micrometer:micrometer-registry-atlas") testImplementation("io.micrometer:micrometer-registry-new-relic") testImplementation("io.micrometer:micrometer-registry-prometheus") diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java index 1d285361310..f044a4c5a4d 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.micrometer.metrics.docker.compose.otlp; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,15 @@ class GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegratio @DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM) void runCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl()).startsWith("https://").endsWith("/v1/metrics"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java index df854e5470f..84fb8203b5e 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.micrometer.metrics.docker.compose.otlp; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testsupport.container.TestImage; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,15 @@ class OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests @DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.OTEL_COLLECTOR) void runCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "otlp-ssl-compose.yaml", image = TestImage.OTEL_COLLECTOR, + additionalResources = "ca.crt") + void runWithSslCreatesConnectionDetails(OtlpMetricsConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUrl()).startsWith("https://").endsWith("/v1/metrics"); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); } } diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index b60cf69ea4e..1486793828b 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -33,6 +33,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; @@ -42,6 +43,8 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.web.servlet.client.RestTestClient; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory}. * @@ -60,8 +63,12 @@ class GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTes @Autowired private MeterRegistry meterRegistry; + @Autowired + private OtlpMetricsConnectionDetails connectionDetails; + @Test void connectionCanBeMadeToOpenTelemetryCollectorContainer() { + assertThat(this.connectionDetails.getSslBundle()).isNull(); Counter.builder("test.counter").register(this.meterRegistry).increment(42); Gauge.builder("test.gauge", () -> 12).register(this.meterRegistry); Timer.builder("test.timer").register(this.meterRegistry).record(Duration.ofMillis(123)); diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index 927165fdb7b..223414c66ba 100644 --- a/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -35,6 +35,7 @@ import org.testcontainers.utility.MountableFile; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; @@ -75,8 +76,12 @@ class OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests { @Autowired private MeterRegistry meterRegistry; + @Autowired + private OtlpMetricsConnectionDetails connectionDetails; + @Test void connectionCanBeMadeToOpenTelemetryCollectorContainer() { + assertThat(this.connectionDetails.getSslBundle()).isNull(); Counter.builder("test.counter").register(this.meterRegistry).increment(42); Gauge.builder("test.gauge", () -> 12).register(this.meterRegistry); Timer.builder("test.timer").register(this.meterRegistry).record(Duration.ofMillis(123)); diff --git a/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt new file mode 100644 index 00000000000..beed250b132 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/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-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml new file mode 100644 index 00000000000..75707fa3e68 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/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-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java new file mode 100644 index 00000000000..f4fba0b4b74 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java @@ -0,0 +1,92 @@ +/* + * 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.micrometer.metrics.autoconfigure.export.otlp; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; + +import javax.net.ssl.SSLParameters; + +import io.micrometer.core.ipc.http.HttpSender; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; + +/** + * {@link HttpSender} implementation using the JDK {@link HttpClient}. + * + * @author Moritz Halbritter + */ +class JdkClientHttpSender implements HttpSender { + + private final HttpClient httpClient; + + private final Duration timeout; + + /** + * Creates a new {@link JdkClientHttpSender}. + * @param connectTimeout the connect timeout + * @param timeout the request timeout + * @param sslBundle the SSL bundle to use for TLS configuration, or {@code null} + */ + JdkClientHttpSender(Duration connectTimeout, Duration timeout, @Nullable SslBundle sslBundle) { + this.httpClient = buildHttpClient(connectTimeout, sslBundle); + this.timeout = timeout; + } + + private static HttpClient buildHttpClient(Duration connectTimeout, @Nullable SslBundle sslBundle) { + HttpClient.Builder builder = HttpClient.newBuilder().connectTimeout(connectTimeout); + if (sslBundle != null) { + builder.sslContext(sslBundle.createSslContext()); + builder.sslParameters(asSslParameters(sslBundle)); + } + return builder.build(); + } + + private static SSLParameters asSslParameters(SslBundle sslBundle) { + SslOptions options = sslBundle.getOptions(); + SSLParameters parameters = new SSLParameters(); + parameters.setCipherSuites(options.getCiphers()); + parameters.setProtocols(options.getEnabledProtocols()); + return parameters; + } + + @Override + public Response send(Request request) throws IOException { + HttpRequest.Builder httpRequest = HttpRequest.newBuilder() + .uri(URI.create(request.getUrl().toString())) + .timeout(this.timeout); + request.getRequestHeaders().forEach(httpRequest::header); + httpRequest.method(request.getMethod().name(), BodyPublishers.ofByteArray(request.getEntity())); + try { + HttpResponse response = this.httpClient.send(httpRequest.build(), BodyHandlers.ofString()); + return new Response(response.statusCode(), response.body()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("HTTP request interrupted", ex); + } + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java index f5bc6644539..8be74ca7968 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java @@ -19,11 +19,13 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.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 Collector service. * * @author EddĂș MelĂ©ndez + * @author Moritz Halbritter * @since 4.0.0 */ public interface OtlpMetricsConnectionDetails extends ConnectionDetails { @@ -34,4 +36,13 @@ public interface OtlpMetricsConnectionDetails extends ConnectionDetails { */ @Nullable String getUrl(); + /** + * 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-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 832715f6bbf..5c218497e65 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 @@ -16,9 +16,12 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; +import java.time.Duration; + import io.micrometer.core.instrument.Clock; import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpConfig; +import io.micrometer.registry.otlp.OtlpHttpMetricsSender; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; import org.jspecify.annotations.Nullable; @@ -36,10 +39,14 @@ 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.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.core.task.VirtualThreadTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to OTLP. @@ -65,8 +72,8 @@ public final class OtlpMetricsExportAutoConfiguration { @Bean @ConditionalOnMissingBean - OtlpMetricsConnectionDetails otlpMetricsConnectionDetails() { - return new PropertiesOtlpMetricsConnectionDetails(this.properties); + OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(ObjectProvider sslBundles) { + return new PropertiesOtlpMetricsConnectionDetails(this.properties, sslBundles.getIfAvailable()); } @Bean @@ -77,11 +84,20 @@ public final class OtlpMetricsExportAutoConfiguration { environment); } + @Bean + @ConditionalOnMissingBean(OtlpMetricsSender.class) + OtlpHttpMetricsSender otlpMetricsSender(OtlpMetricsConnectionDetails connectionDetails) { + Duration connectTimeout = this.properties.getConnectTimeout(); + Duration timeout = connectTimeout.plus(this.properties.getReadTimeout()); + JdkClientHttpSender httpSender = new JdkClientHttpSender(connectTimeout, timeout, + connectionDetails.getSslBundle()); + return new OtlpHttpMetricsSender(httpSender); + } + @Bean @ConditionalOnMissingBean @ConditionalOnThreading(Threading.PLATFORM) - OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender, + OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, OtlpMetricsSender metricsSender, ObjectProvider exemplarContextProvider) { return builder(otlpConfig, clock, metricsSender, exemplarContextProvider).build(); } @@ -90,19 +106,18 @@ public final class OtlpMetricsExportAutoConfiguration { @ConditionalOnMissingBean @ConditionalOnThreading(Threading.VIRTUAL) OtlpMeterRegistry otlpMeterRegistryVirtualThreads(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender, - ObjectProvider exemplarContextProvider) { + OtlpMetricsSender metricsSender, ObjectProvider exemplarContextProvider) { VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("otlp-meter-registry-"); return builder(otlpConfig, clock, metricsSender, exemplarContextProvider) .threadFactory(executor.getVirtualThreadFactory()) .build(); } - private OtlpMeterRegistry.Builder builder(OtlpConfig otlpConfig, Clock clock, - ObjectProvider metricsSender, + private OtlpMeterRegistry.Builder builder(OtlpConfig otlpConfig, Clock clock, OtlpMetricsSender metricsSender, ObjectProvider exemplarContextProvider) { - OtlpMeterRegistry.Builder builder = OtlpMeterRegistry.builder(otlpConfig).clock(clock); - metricsSender.ifAvailable(builder::metricsSender); + OtlpMeterRegistry.Builder builder = OtlpMeterRegistry.builder(otlpConfig) + .clock(clock) + .metricsSender(metricsSender); exemplarContextProvider.ifAvailable(builder::exemplarContextProvider); return builder; } @@ -114,8 +129,11 @@ public final class OtlpMetricsExportAutoConfiguration { private final OtlpMetricsProperties properties; - PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties) { + private final @Nullable SslBundles sslBundles; + + PropertiesOtlpMetricsConnectionDetails(OtlpMetricsProperties properties, @Nullable SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; } @Override @@ -123,6 +141,16 @@ public final class OtlpMetricsExportAutoConfiguration { return this.properties.getUrl(); } + @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; + } + } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java index 0f4687adbdf..99c11d898da 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java @@ -91,6 +91,8 @@ public class OtlpMetricsProperties extends StepRegistryProperties { */ private final Map meter = new LinkedHashMap<>(); + private final Ssl ssl = new Ssl(); + public @Nullable String getUrl() { return this.url; } @@ -167,6 +169,27 @@ public class OtlpMetricsProperties extends StepRegistryProperties { return this.meter; } + 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; + } + + } + /** * Per-meter settings. */ diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java index c9bc4351be1..64ad38ac769 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java @@ -16,10 +16,13 @@ package org.springframework.boot.micrometer.metrics.docker.compose.otlp; +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.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * {@link DockerComposeConnectionDetailsFactory} to create @@ -52,15 +55,24 @@ class OpenTelemetryMetricsDockerComposeConnectionDetailsFactory private final int port; + private final @Nullable SslBundle sslBundle; + private OpenTelemetryMetricsDockerComposeConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.port = source.ports().get(OTLP_PORT); + this.sslBundle = getSslBundle(source); } @Override public String getUrl() { - return "http://%s:%d/v1/metrics".formatted(this.host, this.port); + String scheme = (this.sslBundle != null) ? "https" : "http"; + return "%s://%s:%d/v1/metrics".formatted(scheme, this.host, this.port); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java index 8111122fbab..2e956722cba 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java @@ -16,9 +16,11 @@ package org.springframework.boot.micrometer.metrics.testcontainers.otlp; +import org.jspecify.annotations.Nullable; import org.testcontainers.grafana.LgtmStackContainer; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +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; @@ -54,7 +56,16 @@ class GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory @Override public String getUrl() { - return "%s/v1/metrics".formatted(getContainer().getOtlpHttpUrl()); + String url = getContainer().getOtlpHttpUrl(); + if (getSslBundle() != null) { + url = url.replaceFirst("^http://", "https://"); + } + return "%s/v1/metrics".formatted(url); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java index 98e5ed282d4..a2944ae0236 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java @@ -16,10 +16,12 @@ package org.springframework.boot.micrometer.metrics.testcontainers.otlp; +import org.jspecify.annotations.Nullable; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsConnectionDetails; +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; @@ -55,7 +57,14 @@ class OpenTelemetryMetricsContainerConnectionDetailsFactory @Override public String getUrl() { - return "http://%s:%d/v1/metrics".formatted(getContainer().getHost(), getContainer().getMappedPort(4318)); + String scheme = (getSslBundle() != null) ? "https" : "http"; + return "%s://%s:%d/v1/metrics".formatted(scheme, getContainer().getHost(), + getContainer().getMappedPort(4318)); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); } } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java new file mode 100644 index 00000000000..60c2e911f4e --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java @@ -0,0 +1,127 @@ +/* + * 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.micrometer.metrics.autoconfigure.export.otlp; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.ipc.http.HttpSender.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link JdkClientHttpSender}. + * + * @author Moritz Halbritter + */ +class JdkClientHttpSenderTests { + + private MockWebServer mockWebServer; + + private JdkClientHttpSender sender; + + @BeforeEach + void setUp() throws IOException { + this.mockWebServer = new MockWebServer(); + this.mockWebServer.start(); + this.sender = new JdkClientHttpSender(Duration.ofSeconds(5), Duration.ofSeconds(5), null); + } + + @AfterEach + void tearDown() throws IOException { + this.mockWebServer.shutdown(); + } + + @Test + void sendShouldSendGetRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setBody("response-body").setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.get(url).send(); + assertThat(response.code()).isEqualTo(200); + assertThat(response.body()).isEqualTo("response-body"); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("GET"); + assertThat(request.getPath()).isEqualTo("/test"); + } + + @Test + void sendShouldSendPostRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.post(url).withJsonContent("{\"key\":\"value\"}").send(); + assertThat(response.code()).isEqualTo(200); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("POST"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); + assertThat(request.getBody().readUtf8()).isEqualTo("{\"key\":\"value\"}"); + } + + @Test + void sendShouldIncludeHeaders() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + this.sender.post(url).withHeader("X-Custom", "custom-value").send(); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getHeader("X-Custom")).isEqualTo("custom-value"); + } + + @Test + void sendShouldHandleServerError() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody("error")); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.get(url).send(); + assertThat(response.code()).isEqualTo(500); + assertThat(response.body()).isEqualTo("error"); + } + + @Test + void sendShouldTimeoutOnSlowResponse() { + JdkClientHttpSender sender = new JdkClientHttpSender(Duration.ofSeconds(5), Duration.ofMillis(10), null); + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200).setHeadersDelay(500, TimeUnit.MILLISECONDS)); + String url = this.mockWebServer.url("/test").toString(); + assertThatIOException().isThrownBy(() -> sender.get(url).send()).withMessageContaining("timed out"); + } + + @Test + void sendShouldSendPutRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(200)); + String url = this.mockWebServer.url("/test").toString(); + this.sender.put(url).withPlainText("data").send(); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("PUT"); + assertThat(request.getBody().readUtf8()).isEqualTo("data"); + } + + @Test + void sendShouldSendDeleteRequest() throws Throwable { + this.mockWebServer.enqueue(new MockResponse().setResponseCode(204)); + String url = this.mockWebServer.url("/test").toString(); + Response response = this.sender.delete(url).send(); + assertThat(response.code()).isEqualTo(204); + RecordedRequest request = this.mockWebServer.takeRequest(); + assertThat(request.getMethod()).isEqualTo("DELETE"); + } + +} 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 95f5e002660..d4b011fadf0 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 @@ -16,11 +16,16 @@ package org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp; +import java.net.http.HttpClient; +import java.security.NoSuchAlgorithmException; import java.util.concurrent.ScheduledExecutorService; +import javax.net.ssl.SSLContext; + import io.micrometer.core.instrument.Clock; import io.micrometer.registry.otlp.ExemplarContextProvider; import io.micrometer.registry.otlp.OtlpConfig; +import io.micrometer.registry.otlp.OtlpHttpMetricsSender; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.micrometer.registry.otlp.OtlpMetricsSender; import org.junit.jupiter.api.Test; @@ -28,16 +33,23 @@ import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.assertj.ScheduledExecutorServiceAssert; +import org.springframework.boot.testsupport.classpath.resources.WithPackageResources; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link OtlpMetricsExportAutoConfiguration}. @@ -140,7 +152,10 @@ class OtlpMetricsExportAutoConfigurationTests { @Test void allowsCustomMetricsSenderToBeUsed() { this.contextRunner.withUserConfiguration(BaseConfiguration.class, CustomMetricsSenderConfiguration.class) - .run(this::assertHasCustomMetricsSender); + .run((context) -> { + assertHasCustomMetricsSender(context); + assertThat(context).doesNotHaveBean(OtlpHttpMetricsSender.class); + }); } @Test @@ -184,6 +199,81 @@ class OtlpMetricsExportAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(OtlpMetricsExportAutoConfiguration.class)); } + @Test + void autoConfiguresOtlpHttpMetricsSender() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class)); + } + + @Test + void whenNoSslBundleConfiguredConnectionDetailsReturnsNull() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OtlpMetricsConnectionDetails.class); + OtlpMetricsConnectionDetails connectionDetails = context.getBean(OtlpMetricsConnectionDetails.class); + assertThat(connectionDetails.getSslBundle()).isNull(); + }); + } + + @Test + void whenNoSslBundleDefaultHttpSenderHasDefaultSslContext() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class); + OtlpHttpMetricsSender metricsSender = context.getBean(OtlpHttpMetricsSender.class); + HttpClient httpClient = extractHttpClient(metricsSender); + assertThat(httpClient.sslContext()).isSameAs(SSLContext.getDefault()); + }); + } + + @Test + @WithPackageResources("test.jks") + void whenHasSslBundleConfiguresSslOnHttpSender() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)) + .withPropertyValues("management.otlp.metrics.export.ssl.bundle=mybundle", + "spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class); + OtlpHttpMetricsSender metricsSender = context.getBean(OtlpHttpMetricsSender.class); + HttpClient httpClient = extractHttpClient(metricsSender); + assertThat(httpClient.sslContext()).isNotSameAs(SSLContext.getDefault()); + }); + } + + @Test + void whenCustomConnectionDetailsProvidesSslBundleConfiguresSslOnHttpSender() throws NoSuchAlgorithmException { + SSLContext customSslContext = SSLContext.getInstance("TLS"); + SslBundle sslBundle = mock(SslBundle.class); + given(sslBundle.createSslContext()).willReturn(customSslContext); + given(sslBundle.getOptions()).willReturn(SslOptions.NONE); + OtlpMetricsConnectionDetails connectionDetails = new OtlpMetricsConnectionDetails() { + @Override + public String getUrl() { + return "https://localhost:4318/v1/metrics"; + } + + @Override + public SslBundle getSslBundle() { + return sslBundle; + } + }; + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withBean("customOtlpMetricsConnectionDetails", OtlpMetricsConnectionDetails.class, () -> connectionDetails) + .run((context) -> { + assertThat(context).hasSingleBean(OtlpHttpMetricsSender.class); + OtlpHttpMetricsSender metricsSender = context.getBean(OtlpHttpMetricsSender.class); + HttpClient httpClient = extractHttpClient(metricsSender); + assertThat(httpClient.sslContext()).isSameAs(customSslContext); + }); + } + + private HttpClient extractHttpClient(OtlpHttpMetricsSender metricsSender) { + Object field = ReflectionTestUtils.getField(metricsSender, "httpSender"); + assertThat(field).isNotNull(); + Object httpClient = ReflectionTestUtils.getField(field, "httpClient"); + assertThat(httpClient).isNotNull(); + return (HttpClient) httpClient; + } + private void assertHasCustomMetricsSender(AssertableApplicationContext context) { assertThat(context).hasSingleBean(OtlpMeterRegistry.class); OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class); 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 1ab506cb9e3..e78e7ddbd45 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 @@ -55,7 +55,7 @@ class OtlpMetricsPropertiesConfigAdapterTests { this.properties = new OtlpMetricsProperties(); this.openTelemetryProperties = new OpenTelemetryProperties(); this.environment = new MockEnvironment(); - this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties); + this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties, null); } @Test diff --git a/module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks b/module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/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%