Polish WebClient metrics support

This commit improves the for Actuator Metrics in WebClient.

Unlike the server couterpart of WebFlux, using a `retry` operator on a
`WebClient` pipeline does resubscribes to the whole chain.
The previous implementation recorded start time at the time of pipeline
build phase, but outside of it. This doesn't work since retrying the
same pipeline doesn't update the recorded start time and the duration of
sequential calls are cumulative.

This is now fixed using the Reactor `Context`, since we're now recording
the start time at subscription time and record metrics on `onNext` and
`onError` signals.

Closes gh-12228
This commit is contained in:
Brian Clozel
2018-07-05 15:58:34 +02:00
parent 337d2d8e9a
commit bdd95f09a4
2 changed files with 38 additions and 10 deletions

View File

@@ -37,6 +37,9 @@ import org.springframework.web.reactive.function.client.ExchangeFunction;
*/
public class MetricsWebClientFilterFunction implements ExchangeFilterFunction {
private static final String METRICS_WEBCLIENT_START_TIME = MetricsWebClientFilterFunction.class
.getName() + ".START_TIME";
private final MeterRegistry meterRegistry;
private final WebClientExchangeTagsProvider tagProvider;
@@ -53,16 +56,20 @@ public class MetricsWebClientFilterFunction implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest clientRequest,
ExchangeFunction exchangeFunction) {
long startTime = System.nanoTime();
return exchangeFunction.exchange(clientRequest)
.doOnSuccessOrError((clientResponse, throwable) -> {
Iterable<Tag> tags = this.tagProvider.tags(clientRequest,
clientResponse, throwable);
Timer.builder(this.metricName).tags(tags)
.description("Timer of WebClient operation")
.register(this.meterRegistry)
.record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
});
return exchangeFunction.exchange(clientRequest).doOnEach((signal) -> {
if (!signal.isOnComplete()) {
Long startTime = signal.getContext().get(METRICS_WEBCLIENT_START_TIME);
ClientResponse clientResponse = signal.get();
Throwable throwable = signal.getThrowable();
Iterable<Tag> tags = this.tagProvider.tags(clientRequest, clientResponse,
throwable);
Timer.builder(this.metricName).tags(tags)
.description("Timer of WebClient operation")
.register(this.meterRegistry)
.record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
}
}).subscriberContext((context) -> context.put(METRICS_WEBCLIENT_START_TIME,
System.nanoTime()));
}
}

View File

@@ -18,6 +18,8 @@ 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 io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.MockClock;
@@ -117,4 +119,23 @@ public class MetricsWebClientFilterFunctionTests {
.timer().count()).isEqualTo(1);
}
@Test
public void filterWhenExceptionAndRetryShouldNotCumulateRecordTime() {
ClientRequest request = ClientRequest.create(HttpMethod.GET,
URI.create("http://example.com/projects/spring-boot")).build();
ExchangeFunction exchange = (r) -> Mono.error(new IllegalArgumentException())
.delaySubscription(Duration.ofMillis(300)).cast(ClientResponse.class);
this.filterFunction.filter(request, exchange).retry(1)
.onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty())
.block();
assertThat(this.registry
.get("http.client.requests").tags("method", "GET", "uri",
"/projects/spring-boot", "status", "CLIENT_ERROR")
.timer().count()).isEqualTo(2);
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status",
"CLIENT_ERROR")
.timer().max(TimeUnit.MILLISECONDS)).isLessThan(600);
}
}