Auto-configure Observation support for RestTemplate
Prior to this commit, Spring Boot would auto-configure a customizer that instruments `RestTemplate` through a `RestTemplateBuilder`. This would install a request interceptor that instrumented client exchanges for producing metrics. As of spring-projects/spring-framework#28341, the instrumentation is done at the `RestTemplate` level directly using the `Observation` API. The `Tag` (now `KeyValue`) extraction, observation name and instrumentation behavior now lives in the Spring Framework project. This commit updates the auto-configuration to switch from Boot-specific Metrics instrumentation to a generic Observation instrumentation. As a migration path, some configuration properties are deprecated in favor of the new `management.observations.*` namespace. Closes gh-32484pull/32526/head
parent
3acd9b80e8
commit
eac50a8f0c
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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.metrics.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.common.KeyValues;
|
||||||
|
import io.micrometer.core.instrument.Tag;
|
||||||
|
import io.micrometer.observation.Observation;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
|
||||||
|
import org.springframework.http.client.observation.ClientHttpObservationContext;
|
||||||
|
import org.springframework.http.client.observation.ClientHttpObservationConvention;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter class that applies {@link RestTemplateExchangeTagsProvider} tags as a
|
||||||
|
* {@link ClientHttpObservationConvention}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class ClientHttpObservationConventionAdapter implements ClientHttpObservationConvention {
|
||||||
|
|
||||||
|
private final String metricName;
|
||||||
|
|
||||||
|
private final RestTemplateExchangeTagsProvider tagsProvider;
|
||||||
|
|
||||||
|
ClientHttpObservationConventionAdapter(String metricName, RestTemplateExchangeTagsProvider tagsProvider) {
|
||||||
|
this.metricName = metricName;
|
||||||
|
this.tagsProvider = tagsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsContext(Observation.Context context) {
|
||||||
|
return context instanceof ClientHttpObservationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyValues getLowCardinalityKeyValues(ClientHttpObservationContext context) {
|
||||||
|
KeyValues keyValues = KeyValues.empty();
|
||||||
|
Iterable<Tag> tags = this.tagsProvider.getTags(context.getUriTemplate(), context.getCarrier(),
|
||||||
|
context.getResponse());
|
||||||
|
for (Tag tag : tags) {
|
||||||
|
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||||
|
}
|
||||||
|
return keyValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyValues getHighCardinalityKeyValues(ClientHttpObservationContext context) {
|
||||||
|
return KeyValues.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.metricName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2019 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.metrics.web.client;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Client.ClientRequest;
|
|
||||||
import org.springframework.boot.actuate.metrics.web.client.DefaultRestTemplateExchangeTagsProvider;
|
|
||||||
import org.springframework.boot.actuate.metrics.web.client.MetricsRestTemplateCustomizer;
|
|
||||||
import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the instrumentation of {@link RestTemplate}.
|
|
||||||
*
|
|
||||||
* @author Jon Schneider
|
|
||||||
* @author Phillip Webb
|
|
||||||
* @author Raheela Aslam
|
|
||||||
*/
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
|
||||||
@ConditionalOnClass(RestTemplate.class)
|
|
||||||
@ConditionalOnBean(RestTemplateBuilder.class)
|
|
||||||
class RestTemplateMetricsConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean(RestTemplateExchangeTagsProvider.class)
|
|
||||||
DefaultRestTemplateExchangeTagsProvider restTemplateExchangeTagsProvider() {
|
|
||||||
return new DefaultRestTemplateExchangeTagsProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
MetricsRestTemplateCustomizer metricsRestTemplateCustomizer(MeterRegistry meterRegistry,
|
|
||||||
RestTemplateExchangeTagsProvider restTemplateExchangeTagsProvider, MetricsProperties properties) {
|
|
||||||
ClientRequest request = properties.getWeb().getClient().getRequest();
|
|
||||||
return new MetricsRestTemplateCustomizer(meterRegistry, restTemplateExchangeTagsProvider,
|
|
||||||
request.getMetricName(), request.getAutotime());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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.metrics.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.observation.Observation;
|
||||||
|
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.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.observation.ClientHttpObservationConvention;
|
||||||
|
import org.springframework.http.client.observation.DefaultClientHttpObservationConvention;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the instrumentation of {@link RestTemplate}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ RestTemplate.class, Observation.class })
|
||||||
|
@ConditionalOnBean({ RestTemplateBuilder.class, ObservationRegistry.class })
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class RestTemplateObservationConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ObservationRestTemplateCustomizer metricsRestTemplateCustomizer(ObservationRegistry observationRegistry,
|
||||||
|
ObservationProperties observationProperties, MetricsProperties metricsProperties,
|
||||||
|
ObjectProvider<RestTemplateExchangeTagsProvider> optionalTagsProvider) {
|
||||||
|
String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
|
||||||
|
String observationName = observationProperties.getHttp().getClient().getRequests().getName();
|
||||||
|
String name = (observationName != null) ? observationName : metricName;
|
||||||
|
RestTemplateExchangeTagsProvider tagsProvider = optionalTagsProvider.getIfAvailable();
|
||||||
|
ClientHttpObservationConvention observationConvention = (tagsProvider != null)
|
||||||
|
? new ClientHttpObservationConventionAdapter(name, tagsProvider)
|
||||||
|
: new DefaultClientHttpObservationConvention(name);
|
||||||
|
return new ObservationRestTemplateCustomizer(observationRegistry, observationConvention);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ConfigurationProperties @ConfigurationProperties} for configuring Micrometer
|
||||||
|
* observations.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("management.observations")
|
||||||
|
public class ObservationProperties {
|
||||||
|
|
||||||
|
private final Http http = new Http();
|
||||||
|
|
||||||
|
public Http getHttp() {
|
||||||
|
return this.http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Http {
|
||||||
|
|
||||||
|
private final Client client = new Client();
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Client {
|
||||||
|
|
||||||
|
private final ClientRequests requests = new ClientRequests();
|
||||||
|
|
||||||
|
public ClientRequests getRequests() {
|
||||||
|
return this.requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ClientRequests {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the observation for client requests. If empty, will use the
|
||||||
|
* default "http.client.requests".
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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.metrics.web.client;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import io.micrometer.common.KeyValue;
|
||||||
|
import io.micrometer.observation.Observation;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.metrics.web.client.DefaultRestTemplateExchangeTagsProvider;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.client.ClientHttpRequest;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.http.client.observation.ClientHttpObservationContext;
|
||||||
|
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||||
|
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ClientHttpObservationConventionAdapter}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class ClientHttpObservationConventionAdapterTests {
|
||||||
|
|
||||||
|
private static final String TEST_METRIC_NAME = "test.metric.name";
|
||||||
|
|
||||||
|
private ClientHttpObservationConventionAdapter convention = new ClientHttpObservationConventionAdapter(
|
||||||
|
TEST_METRIC_NAME, new DefaultRestTemplateExchangeTagsProvider());
|
||||||
|
|
||||||
|
private ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/resource/test"));
|
||||||
|
|
||||||
|
private ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.OK);
|
||||||
|
|
||||||
|
private ClientHttpObservationContext context;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
this.context = new ClientHttpObservationContext();
|
||||||
|
this.context.setCarrier(this.request);
|
||||||
|
this.context.setResponse(this.response);
|
||||||
|
this.context.setUriTemplate("/resource/{name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseConfiguredName() {
|
||||||
|
assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOnlySupportClientHttpObservationContext() {
|
||||||
|
assertThat(this.convention.supportsContext(this.context)).isTrue();
|
||||||
|
assertThat(this.convention.supportsContext(new OtherContext())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPushTagsAsLowCardinalityKeyValues() {
|
||||||
|
assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
|
||||||
|
KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
|
||||||
|
KeyValue.of("method", "GET"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotPushAnyHighCardinalityKeyValue() {
|
||||||
|
assertThat(this.convention.getHighCardinalityKeyValues(this.context)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class OtherContext extends Observation.Context {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,162 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2021 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.metrics.web.client;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
|
||||||
import io.micrometer.core.instrument.Timer;
|
|
||||||
import io.micrometer.core.instrument.distribution.HistogramSnapshot;
|
|
||||||
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.metrics.web.client.DefaultRestTemplateExchangeTagsProvider;
|
|
||||||
import org.springframework.boot.actuate.metrics.web.client.MetricsRestTemplateCustomizer;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
|
||||||
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
|
||||||
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.web.client.RestTemplateBuilder;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
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 RestTemplateMetricsConfiguration}.
|
|
||||||
*
|
|
||||||
* @author Stephane Nicoll
|
|
||||||
* @author Jon Schneider
|
|
||||||
* @author Raheela Aslam
|
|
||||||
*/
|
|
||||||
@ExtendWith(OutputCaptureExtension.class)
|
|
||||||
class RestTemplateMetricsConfigurationTests {
|
|
||||||
|
|
||||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
|
|
||||||
.withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class,
|
|
||||||
HttpClientMetricsAutoConfiguration.class));
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void restTemplateCreatedWithBuilderIsInstrumented() {
|
|
||||||
this.contextRunner.run((context) -> {
|
|
||||||
MeterRegistry registry = context.getBean(MeterRegistry.class);
|
|
||||||
RestTemplateBuilder builder = context.getBean(RestTemplateBuilder.class);
|
|
||||||
validateRestTemplate(builder, registry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void restTemplateWithRootUriIsInstrumented() {
|
|
||||||
this.contextRunner.run((context) -> {
|
|
||||||
MeterRegistry registry = context.getBean(MeterRegistry.class);
|
|
||||||
RestTemplateBuilder builder = context.getBean(RestTemplateBuilder.class);
|
|
||||||
builder = builder.rootUri("/root");
|
|
||||||
validateRestTemplate(builder, registry, "/root");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void restTemplateCanBeCustomizedManually() {
|
|
||||||
this.contextRunner.run((context) -> {
|
|
||||||
assertThat(context).hasSingleBean(MetricsRestTemplateCustomizer.class);
|
|
||||||
RestTemplateBuilder customBuilder = new RestTemplateBuilder()
|
|
||||||
.customizers(context.getBean(MetricsRestTemplateCustomizer.class));
|
|
||||||
MeterRegistry registry = context.getBean(MeterRegistry.class);
|
|
||||||
validateRestTemplate(customBuilder, registry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
|
|
||||||
this.contextRunner.withPropertyValues("management.metrics.web.client.max-uri-tags=2").run((context) -> {
|
|
||||||
MeterRegistry registry = getInitializedMeterRegistry(context);
|
|
||||||
assertThat(registry.get("http.client.requests").meters()).hasSize(2);
|
|
||||||
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
|
|
||||||
.contains("Are you using 'uriVariables'?");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) {
|
|
||||||
this.contextRunner.withPropertyValues("management.metrics.web.client.max-uri-tags=5").run((context) -> {
|
|
||||||
MeterRegistry registry = getInitializedMeterRegistry(context);
|
|
||||||
assertThat(registry.get("http.client.requests").meters()).hasSize(3);
|
|
||||||
assertThat(output).doesNotContain("Reached the maximum number of URI tags for 'http.client.requests'.")
|
|
||||||
.doesNotContain("Are you using 'uriVariables'?");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void autoTimeRequestsCanBeConfigured() {
|
|
||||||
this.contextRunner.withPropertyValues("management.metrics.web.client.request.autotime.enabled=true",
|
|
||||||
"management.metrics.web.client.request.autotime.percentiles=0.5,0.7",
|
|
||||||
"management.metrics.web.client.request.autotime.percentiles-histogram=true").run((context) -> {
|
|
||||||
MeterRegistry registry = getInitializedMeterRegistry(context);
|
|
||||||
Timer timer = registry.get("http.client.requests").timer();
|
|
||||||
HistogramSnapshot snapshot = timer.takeSnapshot();
|
|
||||||
assertThat(snapshot.percentileValues()).hasSize(2);
|
|
||||||
assertThat(snapshot.percentileValues()[0].percentile()).isEqualTo(0.5);
|
|
||||||
assertThat(snapshot.percentileValues()[1].percentile()).isEqualTo(0.7);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void backsOffWhenRestTemplateBuilderIsMissing() {
|
|
||||||
new ApplicationContextRunner().with(MetricsRun.simple())
|
|
||||||
.withConfiguration(AutoConfigurations.of(HttpClientMetricsAutoConfiguration.class))
|
|
||||||
.run((context) -> assertThat(context).doesNotHaveBean(DefaultRestTemplateExchangeTagsProvider.class)
|
|
||||||
.doesNotHaveBean(MetricsRestTemplateCustomizer.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext context) {
|
|
||||||
MeterRegistry registry = context.getBean(MeterRegistry.class);
|
|
||||||
RestTemplate restTemplate = context.getBean(RestTemplateBuilder.class).build();
|
|
||||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
server.expect(requestTo("/test/" + i)).andRespond(withStatus(HttpStatus.OK));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
restTemplate.getForObject("/test/" + i, String.class);
|
|
||||||
}
|
|
||||||
return registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateRestTemplate(RestTemplateBuilder builder, MeterRegistry registry) {
|
|
||||||
this.validateRestTemplate(builder, registry, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateRestTemplate(RestTemplateBuilder builder, MeterRegistry registry, String rootUri) {
|
|
||||||
RestTemplate restTemplate = mockRestTemplate(builder, rootUri);
|
|
||||||
assertThat(registry.find("http.client.requests").meter()).isNull();
|
|
||||||
assertThat(restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot").getStatusCode())
|
|
||||||
.isEqualTo(HttpStatus.OK);
|
|
||||||
assertThat(registry.get("http.client.requests").tags("uri", rootUri + "/projects/{project}").meter())
|
|
||||||
.isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
private RestTemplate mockRestTemplate(RestTemplateBuilder builder, String rootUri) {
|
|
||||||
RestTemplate restTemplate = builder.build();
|
|
||||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
|
||||||
server.expect(requestTo(rootUri + "/projects/spring-boot")).andRespond(withStatus(HttpStatus.OK));
|
|
||||||
return restTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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.metrics.web.client;
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.Tag;
|
||||||
|
import io.micrometer.core.instrument.Tags;
|
||||||
|
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.DefaultRestTemplateExchangeTagsProvider;
|
||||||
|
import org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer;
|
||||||
|
import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
||||||
|
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.web.client.RestTemplateBuilder;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpRequest;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
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 RestTemplateObservationConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class RestTemplateObservationConfigurationTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
|
||||||
|
.withBean(ObservationRegistry.class, TestObservationRegistry::create)
|
||||||
|
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
|
||||||
|
RestTemplateAutoConfiguration.class, HttpClientMetricsAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contributesCustomizerBean() {
|
||||||
|
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationRestTemplateCustomizer.class)
|
||||||
|
.doesNotHaveBean(DefaultRestTemplateExchangeTagsProvider.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restTemplateCreatedWithBuilderIsInstrumented() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
RestTemplate restTemplate = buildRestTemplate(context);
|
||||||
|
restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualToIgnoringCase("http.client.requests");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restTemplateCreatedWithBuilderUsesCustomConventionName() {
|
||||||
|
final String observationName = "test.metric.name";
|
||||||
|
this.contextRunner.withPropertyValues("management.observations.http.client.requests.name=" + observationName)
|
||||||
|
.run((context) -> {
|
||||||
|
RestTemplate restTemplate = buildRestTemplate(context);
|
||||||
|
restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualToIgnoringCase(observationName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restTemplateCreatedWithBuilderUsesCustomMetricName() {
|
||||||
|
final String metricName = "test.metric.name";
|
||||||
|
this.contextRunner.withPropertyValues("management.metrics.web.client.request.metric-name=" + metricName)
|
||||||
|
.run((context) -> {
|
||||||
|
RestTemplate restTemplate = buildRestTemplate(context);
|
||||||
|
restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry)
|
||||||
|
.hasObservationWithNameEqualToIgnoringCase(metricName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restTemplateCreatedWithBuilderUsesCustomTagsProvider() {
|
||||||
|
this.contextRunner.withUserConfiguration(CustomTagsConfiguration.class).run((context) -> {
|
||||||
|
RestTemplate restTemplate = buildRestTemplate(context);
|
||||||
|
restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
|
||||||
|
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.withPropertyValues("management.metrics.web.client.max-uri-tags=2").run((context) -> {
|
||||||
|
|
||||||
|
RestTemplate restTemplate = context.getBean(RestTemplateBuilder.class).build();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
server.expect(requestTo("/test/" + i)).andRespond(withStatus(HttpStatus.OK));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
restTemplate.getForObject("/test/" + i, String.class);
|
||||||
|
}
|
||||||
|
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
|
||||||
|
TestObservationRegistryAssert.assertThat(registry);
|
||||||
|
// TODO: check observation count for name
|
||||||
|
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
|
||||||
|
.contains("Are you using 'uriVariables'?");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void backsOffWhenRestTemplateBuilderIsMissing() {
|
||||||
|
new ApplicationContextRunner().with(MetricsRun.simple())
|
||||||
|
.withConfiguration(AutoConfigurations.of(HttpClientMetricsAutoConfiguration.class))
|
||||||
|
.run((context) -> assertThat(context).doesNotHaveBean(DefaultRestTemplateExchangeTagsProvider.class)
|
||||||
|
.doesNotHaveBean(ObservationRestTemplateCustomizer.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestTemplate buildRestTemplate(AssertableApplicationContext context) {
|
||||||
|
RestTemplate restTemplate = context.getBean(RestTemplateBuilder.class).build();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||||
|
server.expect(requestTo("/projects/spring-boot")).andRespond(withStatus(HttpStatus.OK));
|
||||||
|
return restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class CustomTagsConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
CustomTagsProvider customTagsProvider() {
|
||||||
|
return new CustomTagsProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CustomTagsProvider implements RestTemplateExchangeTagsProvider {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterable<Tag> getTags(String urlTemplate, HttpRequest request, ClientHttpResponse response) {
|
||||||
|
return Tags.of("project", "spring-boot");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2022 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 java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
|
||||||
import io.micrometer.core.instrument.Timer;
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
|
||||||
import org.springframework.boot.web.client.RootUriTemplateHandler;
|
|
||||||
import org.springframework.core.NamedThreadLocal;
|
|
||||||
import org.springframework.http.HttpRequest;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.web.util.UriTemplateHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link ClientHttpRequestInterceptor} applied via a
|
|
||||||
* {@link MetricsRestTemplateCustomizer} to record metrics.
|
|
||||||
*
|
|
||||||
* @author Jon Schneider
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class MetricsClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
|
|
||||||
|
|
||||||
private static final Log logger = LogFactory.getLog(MetricsClientHttpRequestInterceptor.class);
|
|
||||||
|
|
||||||
private static final ThreadLocal<Deque<String>> urlTemplate = new UrlTemplateThreadLocal();
|
|
||||||
|
|
||||||
private final MeterRegistry meterRegistry;
|
|
||||||
|
|
||||||
private final RestTemplateExchangeTagsProvider tagProvider;
|
|
||||||
|
|
||||||
private final String metricName;
|
|
||||||
|
|
||||||
private final AutoTimer autoTimer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new {@code MetricsClientHttpRequestInterceptor}.
|
|
||||||
* @param meterRegistry the registry to which metrics are recorded
|
|
||||||
* @param tagProvider provider for metrics tags
|
|
||||||
* @param metricName name of the metric to record
|
|
||||||
* @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing
|
|
||||||
* @since 2.2.0
|
|
||||||
*/
|
|
||||||
MetricsClientHttpRequestInterceptor(MeterRegistry meterRegistry, RestTemplateExchangeTagsProvider tagProvider,
|
|
||||||
String metricName, AutoTimer autoTimer) {
|
|
||||||
this.tagProvider = tagProvider;
|
|
||||||
this.meterRegistry = meterRegistry;
|
|
||||||
this.metricName = metricName;
|
|
||||||
this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
|
|
||||||
throws IOException {
|
|
||||||
if (!enabled()) {
|
|
||||||
return execution.execute(request, body);
|
|
||||||
}
|
|
||||||
long startTime = System.nanoTime();
|
|
||||||
ClientHttpResponse response = null;
|
|
||||||
try {
|
|
||||||
response = execution.execute(request, body);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
try {
|
|
||||||
getTimeBuilder(request, response).register(this.meterRegistry).record(System.nanoTime() - startTime,
|
|
||||||
TimeUnit.NANOSECONDS);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
logger.info("Failed to record metrics.", ex);
|
|
||||||
}
|
|
||||||
if (urlTemplate.get().isEmpty()) {
|
|
||||||
urlTemplate.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean enabled() {
|
|
||||||
return this.autoTimer.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
UriTemplateHandler createUriTemplateHandler(UriTemplateHandler delegate) {
|
|
||||||
if (delegate instanceof RootUriTemplateHandler rootHandler) {
|
|
||||||
return rootHandler.withHandlerWrapper(CapturingUriTemplateHandler::new);
|
|
||||||
}
|
|
||||||
return new CapturingUriTemplateHandler(delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Timer.Builder getTimeBuilder(HttpRequest request, ClientHttpResponse response) {
|
|
||||||
return this.autoTimer.builder(this.metricName)
|
|
||||||
.tags(this.tagProvider.getTags(urlTemplate.get().poll(), request, response))
|
|
||||||
.description("Timer of RestTemplate operation");
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class CapturingUriTemplateHandler implements UriTemplateHandler {
|
|
||||||
|
|
||||||
private final UriTemplateHandler delegate;
|
|
||||||
|
|
||||||
private CapturingUriTemplateHandler(UriTemplateHandler delegate) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public URI expand(String url, Map<String, ?> arguments) {
|
|
||||||
if (enabled()) {
|
|
||||||
urlTemplate.get().push(url);
|
|
||||||
}
|
|
||||||
return this.delegate.expand(url, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public URI expand(String url, Object... arguments) {
|
|
||||||
if (enabled()) {
|
|
||||||
urlTemplate.get().push(url);
|
|
||||||
}
|
|
||||||
return this.delegate.expand(url, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class UrlTemplateThreadLocal extends NamedThreadLocal<Deque<String>> {
|
|
||||||
|
|
||||||
private UrlTemplateThreadLocal() {
|
|
||||||
super("Rest Template URL Template");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Deque<String> initialValue() {
|
|
||||||
return new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2020 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 java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
|
||||||
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
import org.springframework.web.util.UriTemplateHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link RestTemplateCustomizer} that configures the {@link RestTemplate} to record
|
|
||||||
* request metrics.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
* @author Phillip Webb
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
public class MetricsRestTemplateCustomizer implements RestTemplateCustomizer {
|
|
||||||
|
|
||||||
private final MetricsClientHttpRequestInterceptor interceptor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new {@code MetricsRestTemplateInterceptor}. When {@code autoTimeRequests}
|
|
||||||
* is set to {@code true}, the interceptor records metrics using the given
|
|
||||||
* {@code meterRegistry} with tags provided by the given {@code tagProvider} and with
|
|
||||||
* {@link AutoTimer auto-timed configuration}.
|
|
||||||
* @param meterRegistry the meter registry
|
|
||||||
* @param tagProvider the tag provider
|
|
||||||
* @param metricName the name of the recorded metric
|
|
||||||
* @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing
|
|
||||||
* @since 2.2.0
|
|
||||||
*/
|
|
||||||
public MetricsRestTemplateCustomizer(MeterRegistry meterRegistry, RestTemplateExchangeTagsProvider tagProvider,
|
|
||||||
String metricName, AutoTimer autoTimer) {
|
|
||||||
this.interceptor = new MetricsClientHttpRequestInterceptor(meterRegistry, tagProvider, metricName, autoTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(RestTemplate restTemplate) {
|
|
||||||
UriTemplateHandler templateHandler = restTemplate.getUriTemplateHandler();
|
|
||||||
templateHandler = this.interceptor.createUriTemplateHandler(templateHandler);
|
|
||||||
restTemplate.setUriTemplateHandler(templateHandler);
|
|
||||||
List<ClientHttpRequestInterceptor> existingInterceptors = restTemplate.getInterceptors();
|
|
||||||
if (!existingInterceptors.contains(this.interceptor)) {
|
|
||||||
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
|
|
||||||
interceptors.add(this.interceptor);
|
|
||||||
interceptors.addAll(existingInterceptors);
|
|
||||||
restTemplate.setInterceptors(interceptors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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.RestTemplateCustomizer;
|
||||||
|
import org.springframework.http.client.observation.ClientHttpObservationConvention;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RestTemplateCustomizer} that configures the {@link RestTemplate} to record
|
||||||
|
* request observations.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 3.0.0
|
||||||
|
*/
|
||||||
|
public class ObservationRestTemplateCustomizer implements RestTemplateCustomizer {
|
||||||
|
|
||||||
|
private final ObservationRegistry observationRegistry;
|
||||||
|
|
||||||
|
private final ClientHttpObservationConvention observationConvention;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code ObservationRestTemplateCustomizer}.
|
||||||
|
* @param observationConvention the observation convention
|
||||||
|
* @param observationRegistry the observation registry
|
||||||
|
*/
|
||||||
|
public ObservationRestTemplateCustomizer(ObservationRegistry observationRegistry,
|
||||||
|
ClientHttpObservationConvention observationConvention) {
|
||||||
|
this.observationConvention = observationConvention;
|
||||||
|
this.observationRegistry = observationRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(RestTemplate restTemplate) {
|
||||||
|
restTemplate.setObservationConvention(this.observationConvention);
|
||||||
|
restTemplate.setObservationRegistry(this.observationRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,207 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2021 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 java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
|
||||||
import io.micrometer.core.instrument.MockClock;
|
|
||||||
import io.micrometer.core.instrument.Tag;
|
|
||||||
import io.micrometer.core.instrument.Timer.Builder;
|
|
||||||
import io.micrometer.core.instrument.simple.SimpleConfig;
|
|
||||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
|
||||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
|
||||||
import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler;
|
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
||||||
import org.springframework.boot.web.client.RootUriTemplateHandler;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.HttpRequest;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
|
||||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
|
||||||
import org.springframework.http.client.ClientHttpResponse;
|
|
||||||
import org.springframework.mock.env.MockEnvironment;
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer;
|
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers;
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for {@link MetricsRestTemplateCustomizer}.
|
|
||||||
*
|
|
||||||
* @author Jon Schneider
|
|
||||||
* @author Brian Clozel
|
|
||||||
*/
|
|
||||||
class MetricsRestTemplateCustomizerTests {
|
|
||||||
|
|
||||||
private MeterRegistry registry;
|
|
||||||
|
|
||||||
private RestTemplate restTemplate;
|
|
||||||
|
|
||||||
private MockRestServiceServer mockServer;
|
|
||||||
|
|
||||||
private MetricsRestTemplateCustomizer customizer;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setup() {
|
|
||||||
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
|
|
||||||
this.restTemplate = new RestTemplate();
|
|
||||||
this.mockServer = MockRestServiceServer.createServer(this.restTemplate);
|
|
||||||
this.customizer = new MetricsRestTemplateCustomizer(this.registry,
|
|
||||||
new DefaultRestTemplateExchangeTagsProvider(), "http.client.requests", AutoTimer.ENABLED);
|
|
||||||
this.customizer.customize(this.restTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void interceptRestTemplate() {
|
|
||||||
this.mockServer.expect(MockRestRequestMatchers.requestTo("/test/123"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
String result = this.restTemplate.getForObject("/test/{id}", String.class, 123);
|
|
||||||
assertThat(this.registry.find("http.client.requests").meters())
|
|
||||||
.anySatisfy((m) -> assertThat(m.getId().getTags().stream().map(Tag::getKey)).doesNotContain("bucket"));
|
|
||||||
assertThat(this.registry.get("http.client.requests").tags("method", "GET", "uri", "/test/{id}", "status", "200")
|
|
||||||
.timer().count()).isEqualTo(1);
|
|
||||||
assertThat(result).isEqualTo("OK");
|
|
||||||
this.mockServer.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void avoidDuplicateRegistration() {
|
|
||||||
this.customizer.customize(this.restTemplate);
|
|
||||||
assertThat(this.restTemplate.getInterceptors()).hasSize(1);
|
|
||||||
this.customizer.customize(this.restTemplate);
|
|
||||||
assertThat(this.restTemplate.getInterceptors()).hasSize(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void normalizeUriToContainLeadingSlash() {
|
|
||||||
this.mockServer.expect(MockRestRequestMatchers.requestTo("/test/123"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
String result = this.restTemplate.getForObject("test/{id}", String.class, 123);
|
|
||||||
this.registry.get("http.client.requests").tags("uri", "/test/{id}").timer();
|
|
||||||
assertThat(result).isEqualTo("OK");
|
|
||||||
this.mockServer.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void interceptRestTemplateWithUri() throws URISyntaxException {
|
|
||||||
this.mockServer.expect(MockRestRequestMatchers.requestTo("http://localhost/test/123"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
String result = this.restTemplate.getForObject(new URI("http://localhost/test/123"), String.class);
|
|
||||||
assertThat(result).isEqualTo("OK");
|
|
||||||
this.registry.get("http.client.requests").tags("uri", "/test/123").timer();
|
|
||||||
this.mockServer.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void interceptNestedRequest() {
|
|
||||||
this.mockServer.expect(MockRestRequestMatchers.requestTo("/test/123"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
RestTemplate nestedRestTemplate = new RestTemplate();
|
|
||||||
MockRestServiceServer nestedMockServer = MockRestServiceServer.createServer(nestedRestTemplate);
|
|
||||||
nestedMockServer.expect(MockRestRequestMatchers.requestTo("/nestedTest/124"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
this.customizer.customize(nestedRestTemplate);
|
|
||||||
|
|
||||||
TestInterceptor testInterceptor = new TestInterceptor(nestedRestTemplate);
|
|
||||||
this.restTemplate.getInterceptors().add(testInterceptor);
|
|
||||||
|
|
||||||
this.restTemplate.getForObject("/test/{id}", String.class, 123);
|
|
||||||
this.registry.get("http.client.requests").tags("uri", "/test/{id}").timer();
|
|
||||||
this.registry.get("http.client.requests").tags("uri", "/nestedTest/{nestedId}").timer();
|
|
||||||
|
|
||||||
this.mockServer.verify();
|
|
||||||
nestedMockServer.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void whenCustomizerAndLocalHostUriTemplateHandlerAreUsedTogetherThenRestTemplateBuilderCanBuild() {
|
|
||||||
MockEnvironment environment = new MockEnvironment();
|
|
||||||
environment.setProperty("local.server.port", "8443");
|
|
||||||
LocalHostUriTemplateHandler uriTemplateHandler = new LocalHostUriTemplateHandler(environment, "https");
|
|
||||||
RestTemplate restTemplate = new RestTemplateBuilder(this.customizer).uriTemplateHandler(uriTemplateHandler)
|
|
||||||
.build();
|
|
||||||
assertThat(restTemplate.getUriTemplateHandler())
|
|
||||||
.asInstanceOf(InstanceOfAssertFactories.type(RootUriTemplateHandler.class))
|
|
||||||
.extracting(RootUriTemplateHandler::getRootUri).isEqualTo("https://localhost:8443");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void whenAutoTimingIsDisabledUriTemplateHandlerDoesNotCaptureUris() {
|
|
||||||
AtomicBoolean enabled = new AtomicBoolean();
|
|
||||||
AutoTimer autoTimer = new AutoTimer() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return enabled.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply(Builder builder) {
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
RestTemplate restTemplate = new RestTemplateBuilder(new MetricsRestTemplateCustomizer(this.registry,
|
|
||||||
new DefaultRestTemplateExchangeTagsProvider(), "http.client.requests", autoTimer)).build();
|
|
||||||
MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
|
|
||||||
mockServer.expect(MockRestRequestMatchers.requestTo("/first/123"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
mockServer.expect(MockRestRequestMatchers.requestTo("/second/456"))
|
|
||||||
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
|
|
||||||
.andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON));
|
|
||||||
assertThat(restTemplate.getForObject("/first/{id}", String.class, 123)).isEqualTo("OK");
|
|
||||||
assertThat(this.registry.find("http.client.requests").timer()).isNull();
|
|
||||||
enabled.set(true);
|
|
||||||
assertThat(restTemplate.getForObject(URI.create("/second/456"), String.class)).isEqualTo("OK");
|
|
||||||
this.registry.get("http.client.requests").tags("uri", "/second/456").timer();
|
|
||||||
this.mockServer.verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TestInterceptor implements ClientHttpRequestInterceptor {
|
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
|
|
||||||
private TestInterceptor(RestTemplate restTemplate) {
|
|
||||||
this.restTemplate = restTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
|
|
||||||
throws IOException {
|
|
||||||
this.restTemplate.getForObject("/nestedTest/{nestedId}", String.class, 124);
|
|
||||||
return execution.execute(request, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2022 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.ObservationConvention;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.http.client.observation.DefaultClientHttpObservationConvention;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ObservationRestTemplateCustomizer}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class ObservationRestTemplateCustomizerTests {
|
||||||
|
|
||||||
|
private static final String TEST_METRIC_NAME = "http.test.metric.name";
|
||||||
|
|
||||||
|
private ObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||||
|
|
||||||
|
private RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
private ObservationRestTemplateCustomizer customizer = new ObservationRestTemplateCustomizer(
|
||||||
|
this.observationRegistry, new DefaultClientHttpObservationConvention(TEST_METRIC_NAME));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCustomizeObservationConfiguration() {
|
||||||
|
this.customizer.customize(this.restTemplate);
|
||||||
|
assertThat((ObservationRegistry) ReflectionTestUtils.getField(this.restTemplate, "observationRegistry"))
|
||||||
|
.isEqualTo(this.observationRegistry);
|
||||||
|
assertThat((ObservationConvention<?>) ReflectionTestUtils.getField(this.restTemplate, "observationConvention"))
|
||||||
|
.isInstanceOf(DefaultClientHttpObservationConvention.class).extracting("name")
|
||||||
|
.isEqualTo(TEST_METRIC_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue