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