mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-04 00:09:01 +00:00
Add support for SslBundles to OTLP metrics export
Closes gh-49590
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -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));
|
||||
|
||||
+5
@@ -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));
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU=
|
||||
-----END CERTIFICATE-----
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
otlp:
|
||||
image: '{imageName}'
|
||||
ports:
|
||||
- '4317'
|
||||
- '4318'
|
||||
labels:
|
||||
- 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt'
|
||||
+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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+39
-11
@@ -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> 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<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 {
|
||||
@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 {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+23
@@ -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 {
|
||||
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.
|
||||
*/
|
||||
|
||||
+13
-1
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-1
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-1
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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");
|
||||
}
|
||||
|
||||
}
|
||||
+91
-1
@@ -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);
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user