Skip to content

Commit b484ab1

Browse files
committed
Record errors thrown by custom handler in RestTemplate observations
Prior to this commit, the `RestTemplate` observation instrumentation would only record `RestClientException` and `IOException` as errors in the observation. Other types of errors can be thrown by custom components, such as `ResponseErrorHandler` and in this case they aren't recorded with the observation. Also, the current instrumentation does not create any observation scope around the execution. While this would have a limited benefit as no application code is executed there, developers could set up custom components (such as, again, `ResponseErrorHandler`) that could use contextual logging with trace ids. This commit ensures that all `Throwable` are recorded as errors with the observations and that an observation `Scope` is created around the execution of the client exchange. Fixes gh-32063
1 parent c668473 commit b484ab1

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ protected <T> T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpM
855855
Observation observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(this.observationConvention,
856856
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).start();
857857
ClientHttpResponse response = null;
858-
try {
858+
try (Observation.Scope scope = observation.openScope()){
859859
if (requestCallback != null) {
860860
requestCallback.doWithRequest(request);
861861
}
@@ -869,7 +869,7 @@ protected <T> T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpM
869869
observation.error(exception);
870870
throw exception;
871871
}
872-
catch (RestClientException exc) {
872+
catch (Throwable exc) {
873873
observation.error(exc);
874874
throw exc;
875875
}

spring-web/src/test/java/org/springframework/web/client/RestTemplateObservationTests.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
import org.springframework.http.client.ClientHttpResponse;
4040
import org.springframework.http.client.observation.ClientRequestObservationContext;
4141
import org.springframework.http.converter.HttpMessageConverter;
42+
import org.springframework.lang.Nullable;
4243

4344
import static org.assertj.core.api.Assertions.assertThat;
4445
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
46+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
4547
import static org.mockito.ArgumentMatchers.any;
4648
import static org.mockito.ArgumentMatchers.eq;
4749
import static org.mockito.BDDMockito.given;
@@ -158,6 +160,31 @@ void executeWithIoExceptionAddsUnknownOutcome() throws Exception {
158160
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN");
159161
}
160162

163+
@Test // gh-32060
164+
void executeShouldRecordErrorsThrownByErrorHandler() throws Exception {
165+
mockSentRequest(GET, "https://example.org");
166+
mockResponseStatus(HttpStatus.OK);
167+
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
168+
given(errorHandler.hasError(any())).willThrow(new IllegalStateException("error handler"));
169+
170+
assertThatIllegalStateException().isThrownBy(() ->
171+
template.execute("https://example.org", GET, null, null));
172+
173+
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException");
174+
}
175+
176+
@Test // gh-32060
177+
void executeShouldCreateObservationScope() throws Exception {
178+
mockSentRequest(GET, "https://example.org");
179+
mockResponseStatus(HttpStatus.OK);
180+
mockResponseBody("Hello World", MediaType.TEXT_PLAIN);
181+
ObservationErrorHandler observationErrorHandler = new ObservationErrorHandler(observationRegistry);
182+
template.setErrorHandler(observationErrorHandler);
183+
184+
template.execute("https://example.org", GET, null, null);
185+
assertThat(observationErrorHandler.currentObservation).isNotNull();
186+
}
187+
161188

162189
private void mockSentRequest(HttpMethod method, String uri) throws Exception {
163190
mockSentRequest(method, uri, new HttpHeaders());
@@ -205,4 +232,26 @@ public void onStart(ClientRequestObservationContext context) {
205232
}
206233
}
207234

235+
static class ObservationErrorHandler implements ResponseErrorHandler {
236+
237+
final TestObservationRegistry observationRegistry;
238+
239+
@Nullable
240+
Observation currentObservation;
241+
242+
public ObservationErrorHandler(TestObservationRegistry observationRegistry) {
243+
this.observationRegistry = observationRegistry;
244+
}
245+
246+
@Override
247+
public boolean hasError(ClientHttpResponse response) throws IOException {
248+
return true;
249+
}
250+
251+
@Override
252+
public void handleError(ClientHttpResponse response) throws IOException {
253+
currentObservation = this.observationRegistry.getCurrentObservation();
254+
}
255+
}
256+
208257
}

0 commit comments

Comments
 (0)