Use Observation infrastructure for instrumenting WebClient
As of spring-projects/spring-framework#28341, `WebClient` is instrumented directly for `Observation`. This commit removes the custom `ExchangeFilterFunction` that previously instrumented the client for metrics. As a result, the relevant tag providers are now deprecated and adapted as `ObservationConvention` for the time being. Closes gh-32518pull/29403/merge
parent
f0e40bb00d
commit
8b4a20d6cd
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.reactive.client.WebClientExchangeTagsProvider;
|
||||
import org.springframework.web.reactive.function.client.ClientObservationContext;
|
||||
import org.springframework.web.reactive.function.client.ClientObservationConvention;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Adapter class that applies {@link WebClientExchangeTagsProvider} tags as a
|
||||
* {@link ClientObservationConvention}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class ClientObservationConventionAdapter implements ClientObservationConvention {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
|
||||
private final String metricName;
|
||||
|
||||
private final WebClientExchangeTagsProvider tagsProvider;
|
||||
|
||||
ClientObservationConventionAdapter(String metricName, WebClientExchangeTagsProvider tagsProvider) {
|
||||
this.metricName = metricName;
|
||||
this.tagsProvider = tagsProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsContext(Observation.Context context) {
|
||||
return context instanceof ClientObservationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(ClientObservationContext context) {
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
mutateClientRequest(context);
|
||||
Iterable<Tag> tags = this.tagsProvider.tags(context.getCarrier(), context.getResponse(),
|
||||
context.getError().orElse(null));
|
||||
for (Tag tag : tags) {
|
||||
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
/*
|
||||
* {@link WebClientExchangeTagsProvider} relies on a request attribute to get the URI
|
||||
* template, we need to adapt to that.
|
||||
*/
|
||||
private static void mutateClientRequest(ClientObservationContext context) {
|
||||
ClientRequest clientRequest = ClientRequest.from(context.getCarrier())
|
||||
.attribute(URI_TEMPLATE_ATTRIBUTE, context.getUriTemplate()).build();
|
||||
context.setCarrier(clientRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getHighCardinalityKeyValues(ClientObservationContext context) {
|
||||
return KeyValues.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.metricName;
|
||||
}
|
||||
|
||||
}
|
42
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientObservationsAutoConfiguration.java
42
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientObservationsAutoConfiguration.java
37
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfiguration.java
37
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfiguration.java
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.reactive.client.DefaultWebClientExchangeTagsProvider;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.client.ClientObservationContext;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientObservationConventionAdapter}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class ClientObservationConventionAdapterTests {
|
||||
|
||||
private static final String TEST_METRIC_NAME = "test.metric.name";
|
||||
|
||||
private ClientObservationConventionAdapter convention = new ClientObservationConventionAdapter(TEST_METRIC_NAME,
|
||||
new DefaultWebClientExchangeTagsProvider());
|
||||
|
||||
private ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/resource/test")).build();
|
||||
|
||||
private ClientResponse response = ClientResponse.create(HttpStatus.OK).body("foo").build();
|
||||
|
||||
private ClientObservationContext context;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.context = new ClientObservationContext();
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
61
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfigurationTests.java
61
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java → spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfigurationTests.java
@ -1,60 +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.reactive.client;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* {@link WebClientCustomizer} that configures the {@link WebClient} to record request
|
||||
* metrics.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public class MetricsWebClientCustomizer implements WebClientCustomizer {
|
||||
|
||||
private final MetricsWebClientFilterFunction filterFunction;
|
||||
|
||||
/**
|
||||
* Create a new {@code MetricsWebClientFilterFunction} that will record metrics using
|
||||
* the given {@code meterRegistry} with tags provided by the given
|
||||
* {@code tagProvider}.
|
||||
* @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 MetricsWebClientCustomizer(MeterRegistry meterRegistry, WebClientExchangeTagsProvider tagProvider,
|
||||
String metricName, AutoTimer autoTimer) {
|
||||
this.filterFunction = new MetricsWebClientFilterFunction(meterRegistry, tagProvider, metricName, autoTimer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(WebClient.Builder webClientBuilder) {
|
||||
webClientBuilder.filters((filterFunctions) -> {
|
||||
if (!filterFunctions.contains(this.filterFunction)) {
|
||||
filterFunctions.add(0, this.filterFunction);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,120 +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.reactive.client;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.util.context.Context;
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
|
||||
/**
|
||||
* {@link ExchangeFilterFunction} applied via a {@link MetricsWebClientCustomizer} to
|
||||
* record metrics.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Tadaya Tsuyukubo
|
||||
* @author Scott Frederick
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public class MetricsWebClientFilterFunction implements ExchangeFilterFunction {
|
||||
|
||||
private static final String METRICS_WEBCLIENT_START_TIME = MetricsWebClientFilterFunction.class.getName()
|
||||
+ ".START_TIME";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(MetricsWebClientFilterFunction.class);
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
private final WebClientExchangeTagsProvider tagProvider;
|
||||
|
||||
private final String metricName;
|
||||
|
||||
private final AutoTimer autoTimer;
|
||||
|
||||
/**
|
||||
* Create a new {@code MetricsWebClientFilterFunction}.
|
||||
* @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-timer configuration or {@code null} to disable
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public MetricsWebClientFilterFunction(MeterRegistry meterRegistry, WebClientExchangeTagsProvider tagProvider,
|
||||
String metricName, AutoTimer autoTimer) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.tagProvider = tagProvider;
|
||||
this.metricName = metricName;
|
||||
this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
if (!this.autoTimer.isEnabled()) {
|
||||
return next.exchange(request);
|
||||
}
|
||||
return next.exchange(request).as((responseMono) -> instrumentResponse(request, responseMono))
|
||||
.contextWrite(this::putStartTime);
|
||||
}
|
||||
|
||||
private Mono<ClientResponse> instrumentResponse(ClientRequest request, Mono<ClientResponse> responseMono) {
|
||||
final AtomicBoolean responseReceived = new AtomicBoolean();
|
||||
return Mono.deferContextual((ctx) -> responseMono.doOnEach((signal) -> {
|
||||
if (signal.isOnNext() || signal.isOnError()) {
|
||||
responseReceived.set(true);
|
||||
recordTimer(request, signal.get(), signal.getThrowable(), getStartTime(ctx));
|
||||
}
|
||||
}).doFinally((signalType) -> {
|
||||
if (!responseReceived.get() && SignalType.CANCEL.equals(signalType)) {
|
||||
recordTimer(request, null, null, getStartTime(ctx));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void recordTimer(ClientRequest request, ClientResponse response, Throwable error, Long startTime) {
|
||||
try {
|
||||
Iterable<Tag> tags = this.tagProvider.tags(request, response, error);
|
||||
this.autoTimer.builder(this.metricName).tags(tags).description("Timer of WebClient operation")
|
||||
.register(this.meterRegistry).record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.warn("Failed to record timer metrics", ex);
|
||||
// Allow request-response exchange to continue, unaffected by metrics problem
|
||||
}
|
||||
}
|
||||
|
||||
private Long getStartTime(ContextView context) {
|
||||
return context.get(METRICS_WEBCLIENT_START_TIME);
|
||||
}
|
||||
|
||||
private Context putStartTime(Context context) {
|
||||
return context.put(METRICS_WEBCLIENT_START_TIME, System.nanoTime());
|
||||
}
|
||||
|
||||
}
|
@ -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.reactive.client;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
import org.springframework.web.reactive.function.client.ClientObservationConvention;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* {@link WebClientCustomizer} that configures the {@link WebClient} to record request
|
||||
* observations.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class ObservationWebClientCustomizer implements WebClientCustomizer {
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
private final ClientObservationConvention observationConvention;
|
||||
|
||||
/**
|
||||
* Create a new {@code ObservationWebClientCustomizer} that will configure the
|
||||
* {@code Observation} setup on the client.
|
||||
* @param observationRegistry the registry to publish observations to
|
||||
* @param observationConvention the convention to use to populate observations
|
||||
*/
|
||||
public ObservationWebClientCustomizer(ObservationRegistry observationRegistry,
|
||||
ClientObservationConvention observationConvention) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
this.observationConvention = observationConvention;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(WebClient.Builder webClientBuilder) {
|
||||
webClientBuilder.observationRegistry(this.observationRegistry)
|
||||
.observationConvention(this.observationConvention);
|
||||
}
|
||||
|
||||
}
|
@ -1,64 +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.reactive.client;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link MetricsWebClientCustomizer}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class MetricsWebClientCustomizerTests {
|
||||
|
||||
private MetricsWebClientCustomizer customizer;
|
||||
|
||||
private WebClient.Builder clientBuilder;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.customizer = new MetricsWebClientCustomizer(mock(MeterRegistry.class),
|
||||
mock(WebClientExchangeTagsProvider.class), "test", null);
|
||||
this.clientBuilder = WebClient.builder();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeShouldAddFilterFunction() {
|
||||
this.clientBuilder.filter(mock(ExchangeFilterFunction.class));
|
||||
this.customizer.customize(this.clientBuilder);
|
||||
this.clientBuilder.filters(
|
||||
(filters) -> assertThat(filters).hasSize(2).first().isInstanceOf(MetricsWebClientFilterFunction.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeShouldNotAddDuplicateFilterFunction() {
|
||||
this.customizer.customize(this.clientBuilder);
|
||||
this.clientBuilder.filters((filters) -> assertThat(filters).hasSize(1));
|
||||
this.customizer.customize(this.clientBuilder);
|
||||
this.clientBuilder.filters(
|
||||
(filters) -> assertThat(filters).singleElement().isInstanceOf(MetricsWebClientFilterFunction.class));
|
||||
}
|
||||
|
||||
}
|
@ -1,194 +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.reactive.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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;
|
||||
import io.micrometer.core.instrument.search.MeterNotFoundException;
|
||||
import io.micrometer.core.instrument.simple.SimpleConfig;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.AutoTimer;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link MetricsWebClientFilterFunction}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class MetricsWebClientFilterFunctionTests {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
|
||||
private MeterRegistry registry;
|
||||
|
||||
private MetricsWebClientFilterFunction filterFunction;
|
||||
|
||||
private final FaultyTagsProvider tagsProvider = new FaultyTagsProvider();
|
||||
|
||||
private ClientResponse response;
|
||||
|
||||
private ExchangeFunction exchange;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
|
||||
this.filterFunction = new MetricsWebClientFilterFunction(this.registry, this.tagsProvider,
|
||||
"http.client.requests", AutoTimer.ENABLED);
|
||||
this.response = mock(ClientResponse.class);
|
||||
this.exchange = (r) -> Mono.just(this.response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5));
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer().count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenUriTemplatePresentShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot"))
|
||||
.attribute(URI_TEMPLATE_ATTRIBUTE, "/projects/{project}").build();
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5));
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/{project}", "status", "200").timer().count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenIoExceptionThrownShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
ExchangeFunction errorExchange = (r) -> Mono.error(new IOException());
|
||||
this.filterFunction.filter(request, errorExchange).onErrorResume(IOException.class, (t) -> Mono.empty())
|
||||
.block(Duration.ofSeconds(5));
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "IO_ERROR").timer().count())
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenExceptionThrownShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
ExchangeFunction exchange = (r) -> Mono.error(new IllegalArgumentException());
|
||||
this.filterFunction.filter(request, exchange).onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty())
|
||||
.block(Duration.ofSeconds(5));
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer().count())
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenCancelThrownShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
Mono<ClientResponse> filter = this.filterFunction.filter(request, this.exchange);
|
||||
StepVerifier.create(filter).thenCancel().verify(Duration.ofSeconds(5));
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer().count())
|
||||
.isEqualTo(1);
|
||||
assertThatThrownBy(() -> this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer())
|
||||
.isInstanceOf(MeterNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenCancelAfterResponseThrownShouldNotRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
Mono<ClientResponse> filter = this.filterFunction.filter(request, this.exchange);
|
||||
StepVerifier.create(filter).expectNextCount(1).thenCancel().verify(Duration.ofSeconds(5));
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer().count()).isEqualTo(1);
|
||||
assertThatThrownBy(() -> this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer())
|
||||
.isInstanceOf(MeterNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWhenExceptionAndRetryShouldNotAccumulateRecordTime() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
ExchangeFunction exchange = (r) -> Mono.error(new IllegalArgumentException())
|
||||
.delaySubscription(Duration.ofMillis(1000)).cast(ClientResponse.class);
|
||||
this.filterFunction.filter(request, exchange).retry(1)
|
||||
.onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty()).block(Duration.ofSeconds(5));
|
||||
Timer timer = this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer();
|
||||
assertThat(timer.count()).isEqualTo(2);
|
||||
assertThat(timer.max(TimeUnit.MILLISECONDS)).isLessThan(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMetricsRecordingFailsThenFilteringSucceeds() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
this.tagsProvider.failOnce();
|
||||
this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
static class FaultyTagsProvider extends DefaultWebClientExchangeTagsProvider {
|
||||
|
||||
private final AtomicBoolean fail = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public Iterable<Tag> tags(ClientRequest request, ClientResponse response, Throwable throwable) {
|
||||
if (this.fail.compareAndSet(true, false)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return super.tags(request, response, throwable);
|
||||
}
|
||||
|
||||
void failOnce() {
|
||||
this.fail.set(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.reactive.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.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.reactive.function.client.ClientObservationConvention;
|
||||
import org.springframework.web.reactive.function.client.DefaultClientObservationConvention;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ObservationWebClientCustomizer}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class ObservationWebClientCustomizerTests {
|
||||
|
||||
private static final String TEST_METRIC_NAME = "http.test.metric.name";
|
||||
|
||||
private TestObservationRegistry observationRegistry = TestObservationRegistry.create();
|
||||
|
||||
private ClientObservationConvention observationConvention = new DefaultClientObservationConvention(
|
||||
TEST_METRIC_NAME);
|
||||
|
||||
private ObservationWebClientCustomizer customizer = new ObservationWebClientCustomizer(this.observationRegistry,
|
||||
this.observationConvention);
|
||||
|
||||
private WebClient.Builder clientBuilder = WebClient.builder();
|
||||
|
||||
@Test
|
||||
void shouldCustomizeObservationConfiguration() {
|
||||
this.customizer.customize(this.clientBuilder);
|
||||
assertThat((ObservationRegistry) ReflectionTestUtils.getField(this.clientBuilder, "observationRegistry"))
|
||||
.isEqualTo(this.observationRegistry);
|
||||
assertThat((ObservationConvention<?>) ReflectionTestUtils.getField(this.clientBuilder, "observationConvention"))
|
||||
.isInstanceOf(DefaultClientObservationConvention.class).extracting("name").isEqualTo(TEST_METRIC_NAME);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue