From f613ab89b9cb83ec19bc35fb63068f31e22c86a0 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 23 Nov 2023 09:28:09 +0100 Subject: [PATCH] Auto-configure observations for RestClients Closes gh-38500 --- ...tpClientObservationsAutoConfiguration.java | 7 +- .../RestClientObservationConfiguration.java | 53 ++++++ .../RestTemplateObservationConfiguration.java | 3 +- ...stClientObservationConfigurationTests.java | 175 ++++++++++++++++++ ...ationConfigurationWithoutMetricsTests.java | 75 ++++++++ .../ObservationRestClientCustomizer.java | 58 ++++++ .../ObservationRestClientCustomizerTests.java | 54 ++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 7 +- 8 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java index 563a805e654c..60595014ef39 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -48,13 +49,15 @@ * @author Stephane Nicoll * @author Raheela Aslam * @author Brian Clozel + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { ObservationAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, - RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class }) + RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnClass(Observation.class) @ConditionalOnBean(ObservationRegistry.class) -@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class }) +@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class, + RestClientObservationConfiguration.class }) @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) public class HttpClientObservationsAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java new file mode 100644 index 000000000000..6b97d6c65e51 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java @@ -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 customConvention, + ObservationProperties observationProperties) { + String name = observationProperties.getHttp().getClient().getRequests().getName(); + ClientRequestObservationConvention observationConvention = customConvention + .getIfAvailable(() -> new DefaultClientRequestObservationConvention(name)); + return new ObservationRestClientCustomizer(observationRegistry, observationConvention); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java index a1d2a152de56..81fb154a230a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java @@ -19,7 +19,6 @@ import io.micrometer.observation.ObservationRegistry; 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.metrics.web.client.ObservationRestTemplateCustomizer; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -44,7 +43,7 @@ class RestTemplateObservationConfiguration { @Bean ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry, ObjectProvider customConvention, - ObservationProperties observationProperties, MetricsProperties metricsProperties) { + ObservationProperties observationProperties) { String name = observationProperties.getHttp().getClient().getRequests().getName(); ClientRequestObservationConvention observationConvention = customConvention .getIfAvailable(() -> new DefaultClientRequestObservationConvention(name)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java new file mode 100644 index 000000000000..1400c4f6c027 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java @@ -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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java new file mode 100644 index 000000000000..3aa82c08c26b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java new file mode 100644 index 000000000000..904e94260340 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java new file mode 100644 index 000000000000..b945b4d7f596 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index af495e2ac885..9248822b142e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -822,20 +822,21 @@ To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`. [[actuator.metrics.supported.http-clients]] ==== 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: * `RestTemplateBuilder` for `RestTemplate` * `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`. 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]. -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.