Add WebClient metrics support
This commit adds support for Actuator Metrics for WebClient. This support mirrors the current behavior for `RestTemplate`, reusing the same metric name `"http.client.requests"` and tags. `WebClient` is instrumented by a `MetricsWebClientFilterFunction` which is applied by a `WebClientCustomizer`. This instrumentation happens automatically only if you create an instance of `WebClient` using an auto-configured `WebClient.Builder` bean. This infrastructure is reusing de facto the `MeterFilter` that has been added for `RestTemplate` in order to limit the "uri" tag cardinality. Closes gh-12228
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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.Arrays;
|
||||
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebClientExchangeTagsProvider}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public class DefaultWebClientExchangeTagsProvider
|
||||
implements WebClientExchangeTagsProvider {
|
||||
|
||||
@Override
|
||||
public Iterable<Tag> tags(ClientRequest request, ClientResponse response,
|
||||
Throwable throwable) {
|
||||
Tag method = WebClientExchangeTags.method(request);
|
||||
Tag uri = WebClientExchangeTags.uri(request);
|
||||
Tag clientName = WebClientExchangeTags.clientName(request);
|
||||
if (response != null) {
|
||||
return Arrays.asList(method, uri, clientName,
|
||||
WebClientExchangeTags.status(response));
|
||||
}
|
||||
else {
|
||||
return Arrays.asList(method, uri, clientName,
|
||||
WebClientExchangeTags.status(throwable));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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.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
|
||||
*/
|
||||
public MetricsWebClientCustomizer(MeterRegistry meterRegistry,
|
||||
WebClientExchangeTagsProvider tagProvider, String metricName) {
|
||||
this.filterFunction = new MetricsWebClientFilterFunction(meterRegistry,
|
||||
tagProvider, metricName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(WebClient.Builder webClientBuilder) {
|
||||
webClientBuilder.filters(filterFunctions -> {
|
||||
if (!filterFunctions.contains(this.filterFunction)) {
|
||||
filterFunctions.add(0, this.filterFunction);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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 io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
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
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public class MetricsWebClientFilterFunction implements ExchangeFilterFunction {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
private final WebClientExchangeTagsProvider tagProvider;
|
||||
|
||||
private final String metricName;
|
||||
|
||||
public MetricsWebClientFilterFunction(MeterRegistry meterRegistry,
|
||||
WebClientExchangeTagsProvider tagProvider, String metricName) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.tagProvider = tagProvider;
|
||||
this.metricName = metricName;
|
||||
}
|
||||
|
||||
@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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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 io.micrometer.core.instrument.Tag;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Factory methods for creating {@link Tag Tags} related to a request-response exchange
|
||||
* performed by a {@link WebClient}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public final class WebClientExchangeTags {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
|
||||
private static final Tag IO_ERROR = Tag.of("status", "IO_ERROR");
|
||||
|
||||
private static final Tag CLIENT_ERROR = Tag.of("status", "CLIENT_ERROR");
|
||||
|
||||
private WebClientExchangeTags() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code method} {@code Tag} for the
|
||||
* {@link ClientHttpRequest#getMethod() method} of the given {@code request}.
|
||||
* @param request the request
|
||||
* @return the method tag
|
||||
*/
|
||||
public static Tag method(ClientRequest request) {
|
||||
return Tag.of("method", request.method().name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code uri} {@code Tag} for the URI path of the given {@code request}.
|
||||
* @param request the request
|
||||
* @return the uri tag
|
||||
*/
|
||||
public static Tag uri(ClientRequest request) {
|
||||
String uri = (String) request.attribute(URI_TEMPLATE_ATTRIBUTE)
|
||||
.orElseGet(() -> request.url().getPath());
|
||||
return Tag.of("uri", extractPath(uri));
|
||||
}
|
||||
|
||||
private static String extractPath(String url) {
|
||||
String path = url.replaceFirst("^https?://[^/]+/", "");
|
||||
return path.startsWith("/") ? path : "/" + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code status} {@code Tag} derived from the
|
||||
* {@link ClientResponse#statusCode()} of the given {@code response}.
|
||||
* @param response the response
|
||||
* @return the status tag
|
||||
*/
|
||||
public static Tag status(ClientResponse response) {
|
||||
return Tag.of("status", response.statusCode().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code status} {@code Tag} derived from the
|
||||
* exception thrown by the client.
|
||||
* @param throwable the exception
|
||||
* @return the status tag
|
||||
*/
|
||||
public static Tag status(Throwable throwable) {
|
||||
return throwable instanceof IOException ? IO_ERROR : CLIENT_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code clientName} {@code Tag} derived from
|
||||
* the {@link java.net.URI#getHost host}
|
||||
* of the {@link ClientRequest#url() URL} of the given {@code request}.
|
||||
* @param request the request
|
||||
* @return the clientName tag
|
||||
*/
|
||||
public static Tag clientName(ClientRequest request) {
|
||||
String host = request.url().getHost();
|
||||
if (host == null) {
|
||||
host = "none";
|
||||
}
|
||||
return Tag.of("clientName", host);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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.Tag;
|
||||
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
/**
|
||||
* {@link Tag Tags} provider for an exchange performed by a
|
||||
* {@link org.springframework.web.reactive.function.client.WebClient}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface WebClientExchangeTagsProvider {
|
||||
|
||||
/**
|
||||
* Provide tags to be associated with metrics for the client exchange.
|
||||
* @param request the client request
|
||||
* @param response the server response (may be {@code null})
|
||||
* @param throwable the exception (may be {@code null})
|
||||
* @return tags to associate with metrics for the request and response exchange
|
||||
*/
|
||||
Iterable<Tag> tags(ClientRequest request, ClientResponse response, Throwable throwable);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actuator support for
|
||||
* {@link org.springframework.web.reactive.function.client.WebClient} metrics.
|
||||
*/
|
||||
package org.springframework.boot.actuate.metrics.web.reactive.client;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
* Copyright 2012-2018 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.
|
||||
@@ -33,7 +33,7 @@ public interface WebFluxTagsProvider {
|
||||
/**
|
||||
* Provides tags to be associated with metrics for the given {@code exchange}.
|
||||
* @param exchange the exchange
|
||||
* @param ex the current exception (may be {@code null}
|
||||
* @param ex the current exception (may be {@code null})
|
||||
* @return tags to associate with metrics for the request and response exchange
|
||||
*/
|
||||
Iterable<Tag> httpRequestTags(ServerWebExchange exchange, Throwable ex);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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 io.micrometer.core.instrument.Tag;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
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.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultWebClientExchangeTagsProvider}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class DefaultWebClientExchangeTagsProviderTests {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
|
||||
private WebClientExchangeTagsProvider tagsProvider = new DefaultWebClientExchangeTagsProvider();
|
||||
|
||||
private ClientRequest request;
|
||||
|
||||
private ClientResponse response;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot"))
|
||||
.attribute(URI_TEMPLATE_ATTRIBUTE, "http://example.org/projects/{project}")
|
||||
.build();
|
||||
this.response = mock(ClientResponse.class);
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tagsShouldBePopulated() {
|
||||
Iterable<Tag> tags = this.tagsProvider.tags(this.request, this.response, null);
|
||||
assertThat(tags).containsExactlyInAnyOrder(
|
||||
Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
|
||||
Tag.of("clientName", "example.org"), Tag.of("status", "200"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tagsWhenNoUriTemplateShouldProvideUriPath() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot"))
|
||||
.build();
|
||||
Iterable<Tag> tags = this.tagsProvider.tags(request, this.response, null);
|
||||
assertThat(tags).containsExactlyInAnyOrder(
|
||||
Tag.of("method", "GET"), Tag.of("uri", "/projects/spring-boot"),
|
||||
Tag.of("clientName", "example.org"), Tag.of("status", "200"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tagsWhenIoExceptionShouldReturnIoErrorStatus() {
|
||||
Iterable<Tag> tags = this.tagsProvider.tags(this.request,
|
||||
null, new IOException());
|
||||
assertThat(tags).containsExactlyInAnyOrder(
|
||||
Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
|
||||
Tag.of("clientName", "example.org"), Tag.of("status", "IO_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tagsWhenExceptionShouldReturnClientErrorStatus() {
|
||||
Iterable<Tag> tags = this.tagsProvider.tags(this.request,
|
||||
null, new IllegalArgumentException());
|
||||
assertThat(tags).containsExactlyInAnyOrder(
|
||||
Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
|
||||
Tag.of("clientName", "example.org"), Tag.of("status", "CLIENT_ERROR"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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.Before;
|
||||
import org.junit.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
|
||||
*/
|
||||
public class MetricsWebClientCustomizerTests {
|
||||
|
||||
private MetricsWebClientCustomizer customizer;
|
||||
|
||||
private WebClient.Builder clientBuilder;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.customizer = new MetricsWebClientCustomizer(mock(MeterRegistry.class),
|
||||
mock(WebClientExchangeTagsProvider.class), "test");
|
||||
this.clientBuilder = WebClient.builder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public 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
|
||||
public 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)
|
||||
.hasSize(1)
|
||||
.first().isInstanceOf(MetricsWebClientFilterFunction.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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 io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.MockClock;
|
||||
import io.micrometer.core.instrument.simple.SimpleConfig;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
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.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@link MetricsWebClientFilterFunction}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class MetricsWebClientFilterFunctionTests {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
|
||||
private MeterRegistry registry;
|
||||
|
||||
private MetricsWebClientFilterFunction filterFunction;
|
||||
|
||||
private ClientResponse response;
|
||||
|
||||
private ExchangeFunction exchange;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
|
||||
this.filterFunction = new MetricsWebClientFilterFunction(this.registry,
|
||||
new DefaultWebClientExchangeTagsProvider(), "http.client.requests");
|
||||
this.response = mock(ClientResponse.class);
|
||||
this.exchange = r -> Mono.just(this.response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://example.com/projects/spring-boot"))
|
||||
.build();
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
this.filterFunction.filter(request, this.exchange).block();
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer()
|
||||
.count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenUriTemplatePresentShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://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();
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/{project}", "status", "200").timer()
|
||||
.count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenIoExceptionThrownShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://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();
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "IO_ERROR").timer()
|
||||
.count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWhenExceptionThrownShouldRecordTimer() {
|
||||
ClientRequest request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://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();
|
||||
assertThat(this.registry.get("http.client.requests")
|
||||
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer()
|
||||
.count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2012-2018 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
|
||||
*
|
||||
* http://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 io.micrometer.core.instrument.Tag;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
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.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebClientExchangeTags}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class WebClientExchangeTagsTests {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
|
||||
private ClientRequest request;
|
||||
|
||||
private ClientResponse response;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot"))
|
||||
.attribute(URI_TEMPLATE_ATTRIBUTE, "http://example.org/projects/{project}")
|
||||
.build();
|
||||
this.response = mock(ClientResponse.class);
|
||||
given(this.response.statusCode()).willReturn(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void method() {
|
||||
assertThat(WebClientExchangeTags.method(this.request))
|
||||
.isEqualTo(Tag.of("method", "GET"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriWhenAbsoluteTemplateIsAvailableShouldReturnTemplate() {
|
||||
assertThat(WebClientExchangeTags.uri(this.request))
|
||||
.isEqualTo(Tag.of("uri", "/projects/{project}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriWhenRelativeTemplateIsAvailableShouldReturnTemplate() {
|
||||
this.request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot"))
|
||||
.attribute(URI_TEMPLATE_ATTRIBUTE, "/projects/{project}")
|
||||
.build();
|
||||
assertThat(WebClientExchangeTags.uri(this.request))
|
||||
.isEqualTo(Tag.of("uri", "/projects/{project}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uriWhenTemplateIsMissingShouldReturnPath() {
|
||||
this.request = ClientRequest
|
||||
.create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot"))
|
||||
.build();
|
||||
assertThat(WebClientExchangeTags.uri(this.request))
|
||||
.isEqualTo(Tag.of("uri", "/projects/spring-boot"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientName() {
|
||||
assertThat(WebClientExchangeTags.clientName(this.request))
|
||||
.isEqualTo(Tag.of("clientName", "example.org"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void status() {
|
||||
assertThat(WebClientExchangeTags.status(this.response))
|
||||
.isEqualTo(Tag.of("status", "200"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statusWhenIOException() {
|
||||
assertThat(WebClientExchangeTags.status(new IOException()))
|
||||
.isEqualTo(Tag.of("status", "IO_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statusWhenClientException() {
|
||||
assertThat(WebClientExchangeTags.status(new IllegalArgumentException()))
|
||||
.isEqualTo(Tag.of("status", "CLIENT_ERROR"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user