Auto-configure observations for RestClients
Closes gh-38500
This commit is contained in:
parent
9c68a2ab87
commit
f613ab89b9
|
|
@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
|
@ -48,13 +49,15 @@ import org.springframework.core.annotation.Order;
|
||||||
* @author Stephane Nicoll
|
* @author Stephane Nicoll
|
||||||
* @author Raheela Aslam
|
* @author Raheela Aslam
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
|
* @author Moritz Halbritter
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration(after = { ObservationAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
|
@AutoConfiguration(after = { ObservationAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
|
||||||
RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class })
|
RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class, RestClientAutoConfiguration.class })
|
||||||
@ConditionalOnClass(Observation.class)
|
@ConditionalOnClass(Observation.class)
|
||||||
@ConditionalOnBean(ObservationRegistry.class)
|
@ConditionalOnBean(ObservationRegistry.class)
|
||||||
@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class })
|
@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class,
|
||||||
|
RestClientObservationConfiguration.class })
|
||||||
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
|
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
|
||||||
public class HttpClientObservationsAutoConfiguration {
|
public class HttpClientObservationsAutoConfiguration {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
||||||
|
import org.springframework.boot.actuate.metrics.web.client.ObservationRestClientCustomizer;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
||||||
|
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the instrumentation of {@link RestClient}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(RestClient.class)
|
||||||
|
@ConditionalOnBean(RestClient.Builder.class)
|
||||||
|
class RestClientObservationConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RestClientCustomizer observationRestClientCustomizer(ObservationRegistry observationRegistry,
|
||||||
|
ObjectProvider<ClientRequestObservationConvention> customConvention,
|
||||||
|
ObservationProperties observationProperties) {
|
||||||
|
String name = observationProperties.getHttp().getClient().getRequests().getName();
|
||||||
|
ClientRequestObservationConvention observationConvention = customConvention
|
||||||
|
.getIfAvailable(() -> new DefaultClientRequestObservationConvention(name));
|
||||||
|
return new ObservationRestClientCustomizer(observationRegistry, observationConvention);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.client;
|
||||||
import io.micrometer.observation.ObservationRegistry;
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
|
||||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
||||||
import org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer;
|
import org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
|
@ -44,7 +43,7 @@ class RestTemplateObservationConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry,
|
ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry,
|
||||||
ObjectProvider<ClientRequestObservationConvention> customConvention,
|
ObjectProvider<ClientRequestObservationConvention> customConvention,
|
||||||
ObservationProperties observationProperties, MetricsProperties metricsProperties) {
|
ObservationProperties observationProperties) {
|
||||||
String name = observationProperties.getHttp().getClient().getRequests().getName();
|
String name = observationProperties.getHttp().getClient().getRequests().getName();
|
||||||
ClientRequestObservationConvention observationConvention = customConvention
|
ClientRequestObservationConvention observationConvention = customConvention
|
||||||
.getIfAvailable(() -> new DefaultClientRequestObservationConvention(name));
|
.getIfAvailable(() -> new DefaultClientRequestObservationConvention(name));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.common.KeyValues;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||||
|
import org.springframework.boot.actuate.metrics.web.client.ObservationRestClientCustomizer;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.boot.test.system.CapturedOutput;
|
||||||
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||||
|
import org.springframework.boot.test.web.client.MockServerRestClientCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.client.observation.ClientRequestObservationContext;
|
||||||
|
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
||||||
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import org.springframework.web.client.RestClient.Builder;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||||
|
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link RestClientObservationConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
|
class RestClientObservationConfigurationTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||||
|
.withBean(ObservationRegistry.class, TestObservationRegistry::create)
|
||||||
|
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, RestClientAutoConfiguration.class,
|
||||||
|
HttpClientObservationsAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contributesCustomizerBean() {
|
||||||
|
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationRestClientCustomizer.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restClientCreatedWithBuilderIsInstrumented() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
RestClient restClient = buildRestClient(context);
|
||||||
|
restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity();
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualToIgnoringCase("http.client.requests");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restClientCreatedWithBuilderUsesCustomConventionName() {
|
||||||
|
final String observationName = "test.metric.name";
|
||||||
|
this.contextRunner.withPropertyValues("management.observations.http.client.requests.name=" + observationName)
|
||||||
|
.run((context) -> {
|
||||||
|
RestClient restClient = buildRestClient(context);
|
||||||
|
restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity();
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualToIgnoringCase(observationName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restClientCreatedWithBuilderUsesCustomConvention() {
|
||||||
|
this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> {
|
||||||
|
RestClient restClient = buildRestClient(context);
|
||||||
|
restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity();
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualTo("http.client.requests")
|
||||||
|
.that()
|
||||||
|
.hasLowCardinalityKeyValue("project", "spring-boot");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
|
||||||
|
this.contextRunner.with(MetricsRun.simple())
|
||||||
|
.withPropertyValues("management.metrics.web.client.max-uri-tags=2")
|
||||||
|
.run((context) -> {
|
||||||
|
RestClientWithMockServer restClientWithMockServer = buildRestClientAndMockServer(context);
|
||||||
|
MockRestServiceServer server = restClientWithMockServer.mockServer();
|
||||||
|
RestClient restClient = restClientWithMockServer.restClient();
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
server.expect(requestTo("/test/" + i)).andRespond(withStatus(HttpStatus.OK));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
restClient.get().uri("/test/" + i, String.class).retrieve().toBodilessEntity();
|
||||||
|
}
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasNumberOfObservationsWithNameEqualTo("http.client.requests", 3);
|
||||||
|
MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);
|
||||||
|
assertThat(meterRegistry.find("http.client.requests").timers()).hasSize(2);
|
||||||
|
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
|
||||||
|
.contains("Are you using 'uriVariables'?");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void backsOffWhenRestClientBuilderIsMissing() {
|
||||||
|
new ApplicationContextRunner().with(MetricsRun.simple())
|
||||||
|
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
|
||||||
|
HttpClientObservationsAutoConfiguration.class))
|
||||||
|
.run((context) -> assertThat(context).doesNotHaveBean(ObservationRestClientCustomizer.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestClient buildRestClient(AssertableApplicationContext context) {
|
||||||
|
RestClientWithMockServer restClientWithMockServer = buildRestClientAndMockServer(context);
|
||||||
|
restClientWithMockServer.mockServer()
|
||||||
|
.expect(requestTo("/projects/spring-boot"))
|
||||||
|
.andRespond(withStatus(HttpStatus.OK));
|
||||||
|
return restClientWithMockServer.restClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestClientWithMockServer buildRestClientAndMockServer(AssertableApplicationContext context) {
|
||||||
|
Builder builder = context.getBean(Builder.class);
|
||||||
|
MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer();
|
||||||
|
customizer.customize(builder);
|
||||||
|
return new RestClientWithMockServer(builder.build(), customizer.getServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RestClientWithMockServer(RestClient restClient, MockRestServiceServer mockServer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class CustomConventionConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
CustomConvention customConvention() {
|
||||||
|
return new CustomConvention();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CustomConvention extends DefaultClientRequestObservationConvention {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
|
||||||
|
return super.getLowCardinalityKeyValues(context).and("project", "spring-boot");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||||
|
import org.springframework.boot.test.web.client.MockServerRestClientCustomizer;
|
||||||
|
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import org.springframework.web.client.RestClient.Builder;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||||
|
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link RestClientObservationConfiguration} without Micrometer Metrics.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
|
@ClassPathExclusions("micrometer-core-*.jar")
|
||||||
|
class RestClientObservationConfigurationWithoutMetricsTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||||
|
.withBean(ObservationRegistry.class, TestObservationRegistry::create)
|
||||||
|
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, RestClientAutoConfiguration.class,
|
||||||
|
HttpClientObservationsAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restClientCreatedWithBuilderIsInstrumented() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
RestClient restClient = buildRestClient(context);
|
||||||
|
restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity();
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualToIgnoringCase("http.client.requests");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestClient buildRestClient(AssertableApplicationContext context) {
|
||||||
|
Builder builder = context.getBean(Builder.class);
|
||||||
|
MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer();
|
||||||
|
customizer.customize(builder);
|
||||||
|
customizer.getServer().expect(requestTo("/projects/spring-boot")).andRespond(withStatus(HttpStatus.OK));
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.actuate.metrics.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.client.RestClientCustomizer;
|
||||||
|
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.client.RestClient.Builder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RestClientCustomizer} that configures the {@link Builder RestClient builder} to
|
||||||
|
* record request observations.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
* @since 3.2.0
|
||||||
|
*/
|
||||||
|
public class ObservationRestClientCustomizer implements RestClientCustomizer {
|
||||||
|
|
||||||
|
private final ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
|
private final ClientRequestObservationConvention observationConvention;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link ObservationRestClientCustomizer}.
|
||||||
|
* @param observationRegistry the observation registry
|
||||||
|
* @param observationConvention the observation convention
|
||||||
|
*/
|
||||||
|
public ObservationRestClientCustomizer(ObservationRegistry observationRegistry,
|
||||||
|
ClientRequestObservationConvention observationConvention) {
|
||||||
|
Assert.notNull(observationConvention, "ObservationConvention must not be null");
|
||||||
|
Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
|
||||||
|
this.observationRegistry = observationRegistry;
|
||||||
|
this.observationConvention = observationConvention;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(Builder restClientBuilder) {
|
||||||
|
restClientBuilder.observationRegistry(this.observationRegistry);
|
||||||
|
restClientBuilder.observationConvention(this.observationConvention);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2023 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.actuate.metrics.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ObservationRestClientCustomizer}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
class ObservationRestClientCustomizerTests {
|
||||||
|
|
||||||
|
private static final String TEST_METRIC_NAME = "http.test.metric.name";
|
||||||
|
|
||||||
|
private final ObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||||
|
|
||||||
|
private final RestClient.Builder restClientBuilder = RestClient.builder();
|
||||||
|
|
||||||
|
private final ObservationRestClientCustomizer customizer = new ObservationRestClientCustomizer(
|
||||||
|
this.observationRegistry, new DefaultClientRequestObservationConvention(TEST_METRIC_NAME));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCustomizeObservationConfiguration() {
|
||||||
|
this.customizer.customize(this.restClientBuilder);
|
||||||
|
assertThat(this.restClientBuilder).hasFieldOrPropertyWithValue("observationRegistry", this.observationRegistry);
|
||||||
|
assertThat(this.restClientBuilder).extracting("observationConvention")
|
||||||
|
.isInstanceOf(DefaultClientRequestObservationConvention.class)
|
||||||
|
.hasFieldOrPropertyWithValue("name", TEST_METRIC_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -822,20 +822,21 @@ To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`.
|
||||||
|
|
||||||
[[actuator.metrics.supported.http-clients]]
|
[[actuator.metrics.supported.http-clients]]
|
||||||
==== HTTP Client Metrics
|
==== HTTP Client Metrics
|
||||||
Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`.
|
Spring Boot Actuator manages the instrumentation of `RestTemplate`, `WebClient` and `RestClient`.
|
||||||
For that, you have to inject the auto-configured builder and use it to create instances:
|
For that, you have to inject the auto-configured builder and use it to create instances:
|
||||||
|
|
||||||
* `RestTemplateBuilder` for `RestTemplate`
|
* `RestTemplateBuilder` for `RestTemplate`
|
||||||
* `WebClient.Builder` for `WebClient`
|
* `WebClient.Builder` for `WebClient`
|
||||||
|
* `RestClient.Builder` for `RestClient`
|
||||||
|
|
||||||
You can also manually apply the customizers responsible for this instrumentation, namely `ObservationRestTemplateCustomizer` and `ObservationWebClientCustomizer`.
|
You can also manually apply the customizers responsible for this instrumentation, namely `ObservationRestTemplateCustomizer`, `ObservationWebClientCustomizer` and `ObservationRestClientCustomizer`.
|
||||||
|
|
||||||
By default, metrics are generated with the name, `http.client.requests`.
|
By default, metrics are generated with the name, `http.client.requests`.
|
||||||
You can customize the name by setting the configprop:management.observations.http.client.requests.name[] property.
|
You can customize the name by setting the configprop:management.observations.http.client.requests.name[] property.
|
||||||
|
|
||||||
See the {spring-framework-docs}/integration/observability.html#observability.http-client[Spring Framework reference documentation for more information on produced observations].
|
See the {spring-framework-docs}/integration/observability.html#observability.http-client[Spring Framework reference documentation for more information on produced observations].
|
||||||
|
|
||||||
To customize the tags when using `RestTemplate`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.http.client.observation` package.
|
To customize the tags when using `RestTemplate` or `RestClient`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.http.client.observation` package.
|
||||||
To customize the tags when using `WebClient`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.web.reactive.function.client` package.
|
To customize the tags when using `WebClient`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.web.reactive.function.client` package.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue