Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RestClient observations are stopped before ResponseSpec calls #32575

Closed
hadjiski opened this issue Apr 4, 2024 · 16 comments
Closed

RestClient observations are stopped before ResponseSpec calls #32575

hadjiski opened this issue Apr 4, 2024 · 16 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: observability An issue related to observability and tracing type: bug A general bug
Milestone

Comments

@hadjiski
Copy link

hadjiski commented Apr 4, 2024

Debugging a RestTemplate shows that execute() bad responses are properly throwing exceptions, which when caught, are setting the error to the observation:

response = request.execute();
			observationContext.setResponse(response);
			handleResponse(url, method, response);
			return (responseExtractor != null ? responseExtractor.extractData(response) : null);
		}
		catch (IOException ex) {
			ResourceAccessException accessEx = createResourceAccessException(url, method, ex);
			observation.error(accessEx);
			throw accessEx;
		}
		catch (Throwable ex) {
			observation.error(ex);
			throw ex;
		}

Doing the same for the DefaultRestClient retrieve method shows that the clientRequest.execute() call is not throwing any exception, thus no error is recorded to the observation (later during the toEntity call the exception is thrown, but at that time the client observation is already stopped).

clientResponse = clientRequest.execute();
				observationContext.setResponse(clientResponse);
				ConvertibleClientHttpResponse convertibleWrapper = 
new DefaultConvertibleClientHttpResponse(clientResponse);
				return exchangeFunction.exchange(clientRequest, convertibleWrapper);
			}
			catch (IOException ex) {
				ResourceAccessException resourceAccessException = 
createResourceAccessException(uri, this.httpMethod, ex);
				if (observation != null) {
					observation.error(resourceAccessException);
				}
				throw resourceAccessException;
			}
			catch (Throwable error) {
				if (observation != null) {
					observation.error(error);
				}
				throw error;
			}

What is the idea of the new RestClient implementation, how is it supposed to propagate the exception to the observation?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Apr 4, 2024
@bclozel bclozel self-assigned this Apr 4, 2024
@bclozel bclozel added the theme: observability An issue related to observability and tracing label Apr 4, 2024
@bclozel
Copy link
Member

bclozel commented Apr 4, 2024

Can you share a minimal code snippet showing a case (for example against httpbin.org) of an error not being recorded in the observation?

@bclozel bclozel added the status: waiting-for-feedback We need additional information before we can continue label Apr 4, 2024
@hadjiski
Copy link
Author

hadjiski commented Apr 4, 2024

Currently on the road, will provide something after some hours, but in general nothing special, just following any of the trivial examples out there

The observation monitoring appears to reside within the retrieve() method, but the http exceptions are thrown in the toEntity() method

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 4, 2024
@hadjiski
Copy link
Author

hadjiski commented Apr 4, 2024

@bclozel here a running scratch:

import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;

class Scratch {

    public static void main(String[] args) {
        //curl -X POST "https://httpbin.org/status/400" -H "accept: text/plain"

        ResponseEntity<String> entity = RestClient.create("https://httpbin.org/status/400")
                .post()
                .retrieve()
                .toEntity(String.class);
    }
}

As mentioned above, Inside retrieve() the observation is started and stopped, including exception catching and setting of the error, but the exception is thrown inside the toEntity method

@jhoeller jhoeller added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Apr 5, 2024
@bclozel
Copy link
Member

bclozel commented Apr 5, 2024

I think this behavior differs from RestTemplate because we're not using the template pattern here. RestTemplate offers many method variants directly on its type - they all perform the request and all are terminal operations. With RestClient, retrieve() returns a ResponseSpec that you can use to extract information in various fashion and even perform error handling. We instrumented WebClient in the exact same way (in the retrieve() method).

In general, we try to instrument client and servers as close as possible to the transport level:

  • for servers, this includes the entire web framework, error handling, right down to the Servlet level
  • for clients, this means being as close as possible to the client call; the template-style API of RestTemplate doesn't give much flexibility here

I'm declining this issue as a result.

@bclozel bclozel closed this as not planned Won't fix, can't repro, duplicate, stale Apr 5, 2024
@bclozel bclozel added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Apr 5, 2024
@hadjiski
Copy link
Author

hadjiski commented Apr 5, 2024

Thanks for the explanation, I understand, but then do you see any condition, under which any of the catch blocks are ever triggered inside the retrieve method?
We switched our code from rest template to the "more modern" RestClient - now this limitation.
Are you actually saying that officially the http.client.requests metric will not get the error/exception tag properly populated when using WebClient or RestClient, but it will work when using the "old" RestTemplate?

@bclozel
Copy link
Member

bclozel commented Apr 5, 2024

Thanks for the explanation, I understand, but then do you see any condition, under which any of the catch blocks are ever triggered inside the retrieve method?

Yes, those are checked exceptions thrown by the code in the retrieve method. This can happen while serializing the body or during the actual HTTP exchange for any other reason.

We switched our code from rest template to the "more modern" RestClient - now this limitation.

I understand that the behavior is different, but the API is different too. If RestTemplate is working fine for your case you should stick to it, we are not deprecating it.

Are you actually saying that officially the http.client.requests metric will not get the error/exception tag properly populated when using WebClient or RestClient, but it will work when using the "old" RestTemplate?

It depends on the behavior. As you can see in the code, it can be populated.

I think we could extend the error handling to the ResponseSpec here, but other community members could argue that this would make things inconsistent with. WebClient as they would have a different behavior.

@hadjiski
Copy link
Author

hadjiski commented Apr 5, 2024

This would solve it, otherwise all the 4xx/5xx are never recorded right. That appears surprising to me

@bclozel bclozel reopened this Apr 5, 2024
@bclozel bclozel added status: waiting-for-triage An issue we've not yet triaged or decided on and removed status: declined A suggestion or change that we don't feel we should currently apply labels Apr 5, 2024
@bclozel bclozel changed the title New RestClient is not properly propagating exceptions to the http.client.requests observation RestClient observations are stopped before ResponseSpec calls Apr 18, 2024
@bclozel bclozel added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Apr 18, 2024
@bclozel bclozel added this to the 6.1.7 milestone Apr 18, 2024
@bclozel
Copy link
Member

bclozel commented Apr 18, 2024

@hadjiski this is now available in 6.1.7-SNAPSHOT versions, it would be great if you could test it with your application before we cut the release next month. Thanks!

@hadjiski
Copy link
Author

hadjiski commented Apr 20, 2024

@bclozel just tried it and unfortunately it did not work, it goes out earlier with an exception and it does not reach the statements. I was using the spring-web-6.1.7-20240418.195003-12.jar so your fix should be inside.
The use case I tested was throwing standard 4xx exception, which was coming from here and the exception is not caught to set the error to the observation.
Maybe your fix would work, when the callee side is not well known and the content does not fit any message converter, not sure.

Update:
image

@bclozel bclozel reopened this Apr 20, 2024
@bclozel
Copy link
Member

bclozel commented Apr 20, 2024

@hadjiski Thanks for testing SNAPSHOTs! I think I got it right this time, the tests setup was initially too invasive and hiding this case. This should be available in SNAPSHOTs shortly.

@hadjiski
Copy link
Author

yes, I can confirm, this catch block made it, thanks

bclozel added a commit that referenced this issue Jun 19, 2024
Prior to this commit, the fix for gh-32575 introduced cases where the
client observation would be stopped twice.

This commit ensures that `RestClient` observations are stopped only once
when the response is closed, or before throwing an unhanlded exception.

Fixes gh-33068
@shakhar
Copy link

shakhar commented Aug 12, 2024

@poutsma Since this change I started to get no http.client.requests metrics for my RestClient when I call retrieve().
When I debugged the code, I noticed that on version 6.1.6 the observation stopped in the finally block and now nothing stops it and we don't get any metrics.

@bclozel
Copy link
Member

bclozel commented Aug 12, 2024

@shakhar have you tried the latest SNAPSHOT version? If so, please create a new issue with a minimal application reproducing the problem. Thanks!

@shakhar
Copy link

shakhar commented Aug 12, 2024

Yes, tried the latest SNAPSHOT version. I found out that the issue is that I am using retrieve() without toEntity() and just after calling toEntity() it works.
Before I am opening an issue, I just want to ask if it is an expected behavior? Can't I use retrieve() without toEntity() and still get metrics?

@bclozel
Copy link
Member

bclozel commented Aug 12, 2024

Let's ask this question differently: what are you expecting from a ResponseSpec instance if you're not calling any method on it? What is the goal?

@shakhar
Copy link

shakhar commented Aug 12, 2024

If I need just to send a warmup request without a need to get the actual response data for example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: observability An issue related to observability and tracing type: bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants