Browse Source

Add support for SslBundles to OTLP metrics export

Closes gh-49590
dependabot/npm_and_yarn/antora/springio/asciidoctor-extensions-1.0.0-alpha.18
Moritz Halbritter 5 days ago
parent
commit
06272143bf
  1. 1
      module/spring-boot-micrometer-metrics/build.gradle
  2. 10
      module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/GrafanaOpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java
  3. 10
      module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java
  4. 7
      module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java
  5. 5
      module/spring-boot-micrometer-metrics/src/dockerTest/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java
  6. 32
      module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt
  7. 8
      module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml
  8. 92
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java
  9. 11
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsConnectionDetails.java
  10. 50
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java
  11. 23
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsProperties.java
  12. 14
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java
  13. 13
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java
  14. 11
      module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java
  15. 127
      module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java
  16. 92
      module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java
  17. 2
      module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java
  18. BIN
      module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks

1
module/spring-boot-micrometer-metrics/build.gradle

@ -74,6 +74,7 @@ dependencies { @@ -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")

10
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; @@ -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 @@ -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();
}
}

10
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; @@ -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 @@ -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();
}
}

7
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; @@ -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; @@ -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 @@ -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));

5
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; @@ -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 { @@ -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));

32
module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/ca.crt

@ -0,0 +1,32 @@ @@ -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-----

8
module/spring-boot-micrometer-metrics/src/dockerTest/resources/org/springframework/boot/micrometer/metrics/docker/compose/otlp/otlp-ssl-compose.yaml

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
services:
otlp:
image: '{imageName}'
ports:
- '4317'
- '4318'
labels:
- 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt'

92
module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSender.java

@ -0,0 +1,92 @@ @@ -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<String> 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);
}
}
}

11
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; @@ -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 { @@ -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;
}
}

50
module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfiguration.java

@ -16,9 +16,12 @@ @@ -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 @@ -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 { @@ -65,8 +72,8 @@ public final class OtlpMetricsExportAutoConfiguration {
@Bean
@ConditionalOnMissingBean
OtlpMetricsConnectionDetails otlpMetricsConnectionDetails() {
return new PropertiesOtlpMetricsConnectionDetails(this.properties);
OtlpMetricsConnectionDetails otlpMetricsConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
return new PropertiesOtlpMetricsConnectionDetails(this.properties, sslBundles.getIfAvailable());
}
@Bean
@ -77,11 +84,20 @@ public final class OtlpMetricsExportAutoConfiguration { @@ -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<OtlpMetricsSender> metricsSender,
OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock, OtlpMetricsSender metricsSender,
ObjectProvider<ExemplarContextProvider> exemplarContextProvider) {
return builder(otlpConfig, clock, metricsSender, exemplarContextProvider).build();
}
@ -90,19 +106,18 @@ public final class OtlpMetricsExportAutoConfiguration { @@ -90,19 +106,18 @@ public final class OtlpMetricsExportAutoConfiguration {
@ConditionalOnMissingBean
@ConditionalOnThreading(Threading.VIRTUAL)
OtlpMeterRegistry otlpMeterRegistryVirtualThreads(OtlpConfig otlpConfig, Clock clock,
ObjectProvider<OtlpMetricsSender> metricsSender,
ObjectProvider<ExemplarContextProvider> exemplarContextProvider) {
OtlpMetricsSender metricsSender, ObjectProvider<ExemplarContextProvider> 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<OtlpMetricsSender> metricsSender,
private OtlpMeterRegistry.Builder builder(OtlpConfig otlpConfig, Clock clock, OtlpMetricsSender metricsSender,
ObjectProvider<ExemplarContextProvider> 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 { @@ -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 { @@ -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;
}
}
}

23
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 { @@ -91,6 +91,8 @@ public class OtlpMetricsProperties extends StepRegistryProperties {
*/
private final Map<String, Meter> 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 { @@ -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.
*/

14
module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/docker/compose/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java

@ -16,10 +16,13 @@ @@ -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 @@ -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;
}
}

13
module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/GrafanaOpenTelemetryMetricsContainerConnectionDetailsFactory.java

@ -16,9 +16,11 @@ @@ -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 @@ -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();
}
}

11
module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/testcontainers/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java

@ -16,10 +16,12 @@ @@ -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 @@ -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();
}
}

127
module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/JdkClientHttpSenderTests.java

@ -0,0 +1,127 @@ @@ -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");
}
}

92
module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsExportAutoConfigurationTests.java

@ -16,11 +16,16 @@ @@ -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; @@ -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 { @@ -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 { @@ -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);

2
module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/OtlpMetricsPropertiesConfigAdapterTests.java

@ -55,7 +55,7 @@ class OtlpMetricsPropertiesConfigAdapterTests { @@ -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

BIN
module/spring-boot-micrometer-metrics/src/test/resources/org/springframework/boot/micrometer/metrics/autoconfigure/export/otlp/test.jks

Binary file not shown.
Loading…
Cancel
Save