Extend observations to RestClient ResponseSpec
Prior to this commit, the `RestClient` observations would be stopped as soon as the exchange function was called. This means that all errors related to response decoding or mapping would not be recorded by the obsevations. This commit extends the observation recording to the `ResponseSpec` DSL calls as well as custom exchange functions. Fixes gh-32575
This commit is contained in:
parent
97eddb769a
commit
da4547a27e
|
|
@ -192,7 +192,7 @@ final class DefaultRestClient implements RestClient {
|
||||||
@Nullable
|
@Nullable
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
private <T> T readWithMessageConverters(ClientHttpResponse clientResponse, Runnable callback, Type bodyType,
|
private <T> T readWithMessageConverters(ClientHttpResponse clientResponse, Runnable callback, Type bodyType,
|
||||||
Class<T> bodyClass) {
|
Class<T> bodyClass, @Nullable Observation observation) {
|
||||||
|
|
||||||
MediaType contentType = getContentType(clientResponse);
|
MediaType contentType = getContentType(clientResponse);
|
||||||
|
|
||||||
|
|
@ -220,9 +220,13 @@ final class DefaultRestClient implements RestClient {
|
||||||
return (T) messageConverter.read((Class)bodyClass, responseWrapper);
|
return (T) messageConverter.read((Class)bodyClass, responseWrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new UnknownContentTypeException(bodyType, contentType,
|
UnknownContentTypeException unknownContentTypeException = new UnknownContentTypeException(bodyType, contentType,
|
||||||
responseWrapper.getStatusCode(), responseWrapper.getStatusText(),
|
responseWrapper.getStatusCode(), responseWrapper.getStatusText(),
|
||||||
responseWrapper.getHeaders(), RestClientUtils.getBody(responseWrapper));
|
responseWrapper.getHeaders(), RestClientUtils.getBody(responseWrapper));
|
||||||
|
if (observation != null) {
|
||||||
|
observation.error(unknownContentTypeException);
|
||||||
|
}
|
||||||
|
throw unknownContentTypeException;
|
||||||
}
|
}
|
||||||
catch (UncheckedIOException | IOException | HttpMessageNotReadableException ex) {
|
catch (UncheckedIOException | IOException | HttpMessageNotReadableException ex) {
|
||||||
Throwable cause;
|
Throwable cause;
|
||||||
|
|
@ -232,8 +236,17 @@ final class DefaultRestClient implements RestClient {
|
||||||
else {
|
else {
|
||||||
cause = ex;
|
cause = ex;
|
||||||
}
|
}
|
||||||
throw new RestClientException("Error while extracting response for type [" +
|
RestClientException restClientException = new RestClientException("Error while extracting response for type [" +
|
||||||
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", cause);
|
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", cause);
|
||||||
|
if (observation != null) {
|
||||||
|
observation.error(restClientException);
|
||||||
|
}
|
||||||
|
throw restClientException;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (observation != null) {
|
||||||
|
observation.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,28 +488,30 @@ final class DefaultRestClient implements RestClient {
|
||||||
}
|
}
|
||||||
clientResponse = clientRequest.execute();
|
clientResponse = clientRequest.execute();
|
||||||
observationContext.setResponse(clientResponse);
|
observationContext.setResponse(clientResponse);
|
||||||
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse);
|
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse, observation);
|
||||||
return exchangeFunction.exchange(clientRequest, convertibleWrapper);
|
return exchangeFunction.exchange(clientRequest, convertibleWrapper);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
|
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
|
||||||
if (observation != null) {
|
if (observation != null) {
|
||||||
observation.error(resourceAccessException);
|
observation.error(resourceAccessException);
|
||||||
|
observation.stop();
|
||||||
}
|
}
|
||||||
throw resourceAccessException;
|
throw resourceAccessException;
|
||||||
}
|
}
|
||||||
catch (Throwable error) {
|
catch (Throwable error) {
|
||||||
if (observation != null) {
|
if (observation != null) {
|
||||||
observation.error(error);
|
observation.error(error);
|
||||||
|
observation.stop();
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if (close && clientResponse != null) {
|
if (close && clientResponse != null) {
|
||||||
clientResponse.close();
|
clientResponse.close();
|
||||||
}
|
if (observation != null) {
|
||||||
if (observation != null) {
|
observation.stop();
|
||||||
observation.stop();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -665,7 +680,7 @@ final class DefaultRestClient implements RestClient {
|
||||||
@Nullable
|
@Nullable
|
||||||
private <T> T readBody(Type bodyType, Class<T> bodyClass) {
|
private <T> T readBody(Type bodyType, Class<T> bodyClass) {
|
||||||
return DefaultRestClient.this.readWithMessageConverters(this.clientResponse, this::applyStatusHandlers,
|
return DefaultRestClient.this.readWithMessageConverters(this.clientResponse, this::applyStatusHandlers,
|
||||||
bodyType, bodyClass);
|
bodyType, bodyClass, getCurrentObservation());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -686,6 +701,15 @@ final class DefaultRestClient implements RestClient {
|
||||||
throw new UncheckedIOException(ex);
|
throw new UncheckedIOException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Observation getCurrentObservation() {
|
||||||
|
if (this.clientResponse instanceof DefaultConvertibleClientHttpResponse convertibleResponse) {
|
||||||
|
return convertibleResponse.observation;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -693,16 +717,19 @@ final class DefaultRestClient implements RestClient {
|
||||||
|
|
||||||
private final ClientHttpResponse delegate;
|
private final ClientHttpResponse delegate;
|
||||||
|
|
||||||
|
private final Observation observation;
|
||||||
|
|
||||||
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate) {
|
|
||||||
|
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate, Observation observation) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
|
this.observation = observation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public <T> T bodyTo(Class<T> bodyType) {
|
public <T> T bodyTo(Class<T> bodyType) {
|
||||||
return readWithMessageConverters(this.delegate, () -> {} , bodyType, bodyType);
|
return readWithMessageConverters(this.delegate, () -> {} , bodyType, bodyType, this.observation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -710,7 +737,7 @@ final class DefaultRestClient implements RestClient {
|
||||||
public <T> T bodyTo(ParameterizedTypeReference<T> bodyType) {
|
public <T> T bodyTo(ParameterizedTypeReference<T> bodyType) {
|
||||||
Type type = bodyType.getType();
|
Type type = bodyType.getType();
|
||||||
Class<T> bodyClass = bodyClass(type);
|
Class<T> bodyClass = bodyClass(type);
|
||||||
return readWithMessageConverters(this.delegate, () -> {} , type, bodyClass);
|
return readWithMessageConverters(this.delegate, () -> {}, type, bodyClass, this.observation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -736,6 +763,7 @@ final class DefaultRestClient implements RestClient {
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
this.delegate.close();
|
this.delegate.close();
|
||||||
|
this.observation.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package org.springframework.web.client;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
@ -40,6 +41,7 @@ import org.springframework.http.client.observation.ClientRequestObservationConte
|
||||||
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
import org.springframework.http.client.observation.ClientRequestObservationConvention;
|
||||||
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
@ -84,7 +86,7 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeVarArgsAddsUriTemplateAsKeyValue() throws Exception {
|
void shouldContributeTemplateWhenUriVariables() throws Exception {
|
||||||
mockSentRequest(GET, "https://example.com/hotels/42/bookings/21");
|
mockSentRequest(GET, "https://example.com/hotels/42/bookings/21");
|
||||||
mockResponseStatus(HttpStatus.OK);
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
|
||||||
|
|
@ -95,7 +97,7 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeArgsMapAddsUriTemplateAsKeyValue() throws Exception {
|
void shouldContributeTemplateWhenMap() throws Exception {
|
||||||
mockSentRequest(GET, "https://example.com/hotels/42/bookings/21");
|
mockSentRequest(GET, "https://example.com/hotels/42/bookings/21");
|
||||||
mockResponseStatus(HttpStatus.OK);
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
|
||||||
|
|
@ -107,9 +109,8 @@ class RestClientObservationTests {
|
||||||
assertThatHttpObservation().hasLowCardinalityKeyValue("uri", "/hotels/{hotel}/bookings/{booking}");
|
assertThatHttpObservation().hasLowCardinalityKeyValue("uri", "/hotels/{hotel}/bookings/{booking}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeAddsSuccessAsOutcome() throws Exception {
|
void shouldContributeSuccessOutcome() throws Exception {
|
||||||
mockSentRequest(GET, "https://example.org");
|
mockSentRequest(GET, "https://example.org");
|
||||||
mockResponseStatus(HttpStatus.OK);
|
mockResponseStatus(HttpStatus.OK);
|
||||||
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
|
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
|
||||||
|
|
@ -120,7 +121,7 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeAddsServerErrorAsOutcome() throws Exception {
|
void shouldContributeServerErrorOutcome() throws Exception {
|
||||||
String url = "https://example.org";
|
String url = "https://example.org";
|
||||||
mockSentRequest(GET, url);
|
mockSentRequest(GET, url);
|
||||||
mockResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR);
|
mockResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
|
@ -134,7 +135,7 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeAddsExceptionAsKeyValue() throws Exception {
|
void shouldContributeDecodingError() throws Exception {
|
||||||
mockSentRequest(POST, "https://example.org/resource");
|
mockSentRequest(POST, "https://example.org/resource");
|
||||||
mockResponseStatus(HttpStatus.OK);
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
|
||||||
|
|
@ -150,7 +151,7 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeWithIoExceptionAddsUnknownOutcome() throws Exception {
|
void shouldContributeIOError() throws Exception {
|
||||||
String url = "https://example.org/resource";
|
String url = "https://example.org/resource";
|
||||||
mockSentRequest(GET, url);
|
mockSentRequest(GET, url);
|
||||||
given(request.execute()).willThrow(new IOException("Socket failure"));
|
given(request.execute()).willThrow(new IOException("Socket failure"));
|
||||||
|
|
@ -161,7 +162,7 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void executeWithCustomConventionUsesCustomObservationName() throws Exception {
|
void shouldUseCustomConvention() throws Exception {
|
||||||
ClientRequestObservationConvention observationConvention =
|
ClientRequestObservationConvention observationConvention =
|
||||||
new DefaultClientRequestObservationConvention("custom.requests");
|
new DefaultClientRequestObservationConvention("custom.requests");
|
||||||
RestClient restClient = this.client.mutate().observationConvention(observationConvention).build();
|
RestClient restClient = this.client.mutate().observationConvention(observationConvention).build();
|
||||||
|
|
@ -174,6 +175,56 @@ class RestClientObservationTests {
|
||||||
.hasObservationWithNameEqualTo("custom.requests");
|
.hasObservationWithNameEqualTo("custom.requests");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAddClientDecodingErrorAsException() throws Exception {
|
||||||
|
String url = "https://example.org";
|
||||||
|
mockSentRequest(GET, url);
|
||||||
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
mockResponseBody("INVALID", MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(RestClientException.class).isThrownBy(() ->
|
||||||
|
client.get().uri(url).retrieve().body(User.class));
|
||||||
|
|
||||||
|
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "RestClientException");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAddUnknownContentTypeErrorAsException() throws Exception {
|
||||||
|
String url = "https://example.org";
|
||||||
|
mockSentRequest(GET, url);
|
||||||
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
mockResponseBody("Not Found", MediaType.TEXT_HTML);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(RestClientException.class).isThrownBy(() ->
|
||||||
|
client.get().uri(url).retrieve().body(User.class));
|
||||||
|
|
||||||
|
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "UnknownContentTypeException");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registerObservationWhenReadingBody() throws Exception {
|
||||||
|
mockSentRequest(GET, "https://example.org");
|
||||||
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
|
||||||
|
|
||||||
|
client.get().uri("https://example.org").exchange((request, response) -> response.bodyTo(String.class));
|
||||||
|
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registerObservationWhenReadingStream() throws Exception {
|
||||||
|
mockSentRequest(GET, "https://example.org");
|
||||||
|
mockResponseStatus(HttpStatus.OK);
|
||||||
|
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
|
||||||
|
|
||||||
|
client.get().uri("https://example.org").exchange((request, response) -> {
|
||||||
|
String result = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
|
||||||
|
response.close();
|
||||||
|
return result;
|
||||||
|
}, false);
|
||||||
|
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void mockSentRequest(HttpMethod method, String uri) throws Exception {
|
private void mockSentRequest(HttpMethod method, String uri) throws Exception {
|
||||||
mockSentRequest(method, uri, new HttpHeaders());
|
mockSentRequest(method, uri, new HttpHeaders());
|
||||||
|
|
@ -220,4 +271,8 @@ class RestClientObservationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record User(String name) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue