268

I have been using the Spring RestTemplate for a while and I consistently hit a wall when I'am trying to debug it's requests and responses. I'm basically looking to see the same things as I see when I use curl with the "verbose" option turned on. For example :

curl -v http://twitter.com/statuses/public_timeline.rss

Would display both the sent data and the received data (including the headers, cookies, etc.).

I have checked some related posts like : How do I log response in Spring RestTemplate? but I haven't managed to solve this issue.

One way to do this would be to actually change the RestTemplate source code and add some extra logging statements there, but I would find this approach really a last resort thing. There should be some way to tell Spring Web Client/RestTemplate to log everything in a much friendlier way.

My goal would be to be able to do this with code like :

restTemplate.put("http://someurl", objectToPut, urlPathValues);

and then to get the same type of debug information (as I get with curl) in the log file or in the console. I believe this would be extremely useful for anyone that uses the Spring RestTemplate and has problems. Using curl to debug your RestTemplate problems just doesn't work (in some cases).

Community
  • 1
  • 1
Paul Sabou
  • 3,097
  • 3
  • 16
  • 9
  • 46
    Warning to anyone reading in 2018: There isn't a simple answer to this! – davidfrancis Jan 16 '18 at 17:27
  • 4
    Most easy way is to use a breakpoint in write(...) method of AbstractHttpMessageConverter class, there is a outputMessage object where you could see the data. P.S. You can copy the value and then format it with online formatter. – Sergey Chepurnov Jan 25 '18 at 12:54
  • 2
    seems like this should be easy to do in Spring, but, judging from the answers here - not the case. So one other solution, would be to bypass Spring entirely and use a tool like Fiddler to capture the request/response. – michaelok Feb 13 '19 at 22:47
  • read the answer to this question from the following a link : [spring-resttemplate-how-to-enable-full-debugging-logging-of-requests-responses](https://stackoverflow.com/questions/3892018/how-do-i-log-response-in-spring-resttemplate/56003085#56003085) – Solanki Vaibhav May 06 '19 at 10:22
  • July 2019: As there is still no simple solution to this question, I tried to give a summary of the other 24 answers (so far) and their comments and discussions in [my own answer below](https://stackoverflow.com/a/57252427/349169). Hope it helps. – Chris Jul 29 '19 at 11:10
  • Nothing seemed to work with my Intellij/gradle/Spring Boot config, but I managed to fix my issue with the online [webhook.site](https://webhook.site/) logger. – MikaelF Aug 03 '19 at 20:00
  • I think it's time to walk down the path of "restTemplate alternatives" – Zombies May 04 '20 at 19:02

30 Answers30

236

Just to complete the example with a full implementation of ClientHttpRequestInterceptor to trace request and response:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    final static Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        traceResponse(response);
        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        log.info("===========================request begin================================================");
        log.debug("URI         : {}", request.getURI());
        log.debug("Method      : {}", request.getMethod());
        log.debug("Headers     : {}", request.getHeaders() );
        log.debug("Request body: {}", new String(body, "UTF-8"));
        log.info("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), "UTF-8"));
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            inputStringBuilder.append('\n');
            line = bufferedReader.readLine();
        }
        log.info("============================response begin==========================================");
        log.debug("Status code  : {}", response.getStatusCode());
        log.debug("Status text  : {}", response.getStatusText());
        log.debug("Headers      : {}", response.getHeaders());
        log.debug("Response body: {}", inputStringBuilder.toString());
        log.info("=======================response end=================================================");
    }

}

Then instantiate RestTemplate using a BufferingClientHttpRequestFactory and the LoggingRequestInterceptor:

RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new LoggingRequestInterceptor());
restTemplate.setInterceptors(interceptors);

The BufferingClientHttpRequestFactory is required as we want to use the response body both in the interceptor and for the initial calling code. The default implementation allows to read the response body only once.

sofiene zaghdoudi
  • 3,108
  • 2
  • 17
  • 11
  • 1
    Good answer! Though an issue is that if the response code is not a 200, traceResponse will throw. – Alex Dec 30 '15 at 21:50
  • 39
    This is wrong. If you read the stream, the application code will not be able to read the response. – James Watkins Mar 01 '16 at 20:32
  • 39
    we have given the RestTemplate a BufferingClientHttpRequestFactory so we can read the response twice. – sofiene zaghdoudi Mar 01 '16 at 22:17
  • 18
    We have been using this technique for about 3 months now. It only works with RestTemplate configured with a `BufferingClientHttpResponseWrapper` as @sofienezaghdoudi implies. However, it does not work when used in tests using spring's mockServer framework since `MockRestServiceServer.createServer(restTemplate)` overwrites the RequestFactory to `InterceptingClientHttpRequestFactory`. – RubesMN May 04 '16 at 19:38
  • 13
    The technique is good, implementation is wrong. 404 case, response.getBody() throw IOException -> you never get the log for out and even worst, it will become a ResourceAccessException in your further code, instead of a RestClientResponseException – MilacH Jan 26 '17 at 13:52
  • 1
    Side note: as of the time of commenting this will not work for OAuth2RestTemplate. See [Oauth2RestTemplate should propagate its requestFactory, messageConverters and interceptors to its accessTokenProvider](https://github.com/spring-projects/spring-security-oauth/issues/459) – Turcogj May 26 '17 at 04:21
  • 8
    thanks for the reply. But this is bad practice to have multiple "log.debug" as it could be spread over a lot of other logs. It's better to use a single log.debug instruction so you're sure everything is on the same place – user2447161 Aug 01 '17 at 10:38
  • 1
    How to use ClientHttpRequestInterceptor without RestTemplate? Using simple @RestController – user2602807 Oct 13 '17 at 09:47
  • 1
    Nice solution, except it crashes on 5xx, which might be undesirable. You can use StreamUtils.copyToString to copy response body and then print it out, instead of bufferedReader: String bodyText = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); log.debug("Response body: {}", bodyText); – stiebrs Dec 21 '17 at 10:30
  • @Turcogj This method doesn't work on Oauth2RestTemplate. As an alternative you can change the requestFactory according to this answer https://stackoverflow.com/questions/16573501/resttemplate-logging-post-data – Joshua H Jan 22 '18 at 03:08
  • 5
    new RestTemplate(new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory())) works better to me as long as is logs non-success responses as well. Note, that you need to add org.apache.httpcomponents:httpclient into dependencies – Pashec Apr 02 '18 at 10:29
  • 1
    As @MilacH already mentioned, the implementation leads to that the errorHandler is not invoked. – Tony Findeisen Sep 14 '18 at 10:56
  • 2
    Kind of amusing to see, with all the suggestions and comments here, that someone appears to have copied the above code lock, stock and barrel and passed it off without attribution. Oh, wait, they changed the C style logging from 'info' to 'debug' - call it good! #sad https://objectpartners.com/2018/03/01/log-your-resttemplate-request-and-response-without-destroying-the-body/ – michaelok Feb 26 '19 at 18:28
  • When I added new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()) to restTemplate I got new problem related https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch . Then I configured new HttpClient that added new fails in my integration test, because some behaviour related to cookies where changed. Actually, the easiest solution is to reuse(copy/paste) org.springframework.http.client.BufferingClientHttpResponseWrapper instead of configuring new requestFactory – Geniy Feb 27 '19 at 14:07
  • How do I write a Unit Test for this class? – Khwaja Sanjari Mar 21 '19 at 19:24
  • 2
    For people using `RestTemplateBuilder` after building better use `restTemplate.getInterceptors().add(new LoggingRequestInterceptor());` just to avoid loosing the other interceptors, like auth, ... . – PeMa Jul 19 '19 at 08:36
  • for performance and memory consumption reasons, it would be better avoiding reading body lines into memory when debug log level is not enabled – Frédéric Camblor Aug 27 '19 at 14:16
  • @Pashec your fix works for me! :) I saw the blog post and I was very annoyed : copy pasting without attribution + no update about this bug, what a waste of time ! – WorkInProgress Jan 29 '20 at 14:20
  • If I am using RestTemplateBuilder instead, how should I approach the instantiating of the requestFactory? – Coder Mar 25 '20 at 17:30
  • 1
    @michaelok an others raising this issue of plagiarism. The answerer posted this in 2015. The article that was linked was published in 2018. Also, if you pick a snippet of this code and google it, you will find more than a handful of hits. As much of a problem as answers without attribution is, also is the problem of SEO people ripping answers from SO and turning into a blog post also without attribution. – Thomas Carlisle Sep 04 '21 at 15:25
  • How can this be a solution? it can easily lead to potential memory leaks (because of BufferingClientHttpRequestFactory). Also, can easily exhaust the memory. – Shayan Ahmad Jul 13 '22 at 23:28
  • If we read it in the interceptor using stream, the body won’t be available for RestTemplate to deserialize it into object model – Tsvetoslav Tsvetkov Feb 07 '23 at 07:51
170

in Spring Boot you can get the full request/response by setting this in properties (or other 12 factor method)

logging.level.org.apache.http=DEBUG

this outputs

-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connecting to localhost/127.0.0.1:41827
-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connection established 127.0.0.1:39546<->127.0.0.1:41827
-DEBUG o.a.http.impl.execchain.MainClientExec   : Executing request POST /v0/users HTTP/1.1
-DEBUG o.a.http.impl.execchain.MainClientExec   : Target auth state: UNCHALLENGED
-DEBUG o.a.http.impl.execchain.MainClientExec   : Proxy auth state: UNCHALLENGED
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> POST /v0/users HTTP/1.1
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Type: application/json;charset=UTF-8
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Length: 56
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Host: localhost:41827
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Connection: Keep-Alive
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Accept-Encoding: gzip,deflate
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "POST /v0/users HTTP/1.1[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Type: application/json;charset=UTF-8[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Length: 56[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Host: localhost:41827[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "{"id":null,"email":"xenoterracide@gmail.com","new":true}"

and response

-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connecting to localhost/127.0.0.1:41827
-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connection established 127.0.0.1:39546<->127.0.0.1:41827
-DEBUG o.a.http.impl.execchain.MainClientExec   : Executing request POST /v0/users HTTP/1.1
-DEBUG o.a.http.impl.execchain.MainClientExec   : Target auth state: UNCHALLENGED
-DEBUG o.a.http.impl.execchain.MainClientExec   : Proxy auth state: UNCHALLENGED
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> POST /v0/users HTTP/1.1
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Type: application/json;charset=UTF-8
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Length: 56
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Host: localhost:41827
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Connection: Keep-Alive
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Accept-Encoding: gzip,deflate
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "POST /v0/users HTTP/1.1[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Type: application/json;charset=UTF-8[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Length: 56[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Host: localhost:41827[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "{"id":null,"email":"xenoterracide@gmail.com","new":true}"

or just logging.level.org.apache.http.wire=DEBUG which seems to contain all of the relevant information

xenoterracide
  • 16,274
  • 24
  • 118
  • 243
  • 6
    This was the simplest thing that did what I wanted. I highly encourage including this in the accepted answer. – michaelavila Nov 30 '16 at 19:17
  • 26
    According to the javadoc of [RestTemplate](http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html) : `by default the RestTemplate relies on standard JDK facilities to establish HTTP connections. You can switch to use a different HTTP library such as Apache HttpComponents` – Ortomala Lokni Feb 01 '17 at 14:40
  • 26
    RestTemplate doesn't use these Apache classes be default, as pointed out by @OrtomalaLokni, so you should also include *how* to use them in addition to how to print the debug when they are being used. – Captain Man May 01 '17 at 19:34
  • I am getting like this : `http-outgoing-0 << "[0x1f][0x8b][0x8][0x0][0x0][0x0][0x0][0x0]` – Partha Sarathi Ghosh Aug 21 '18 at 09:36
  • 2
    @ParthaSarathiGhosh The content is probably gzip encoded which is why you're not seeing the raw text. – Matthew Buckett Jan 21 '19 at 11:33
  • 2
    This simple solution works if your app is configured to use Apache – tbraun Jun 27 '19 at 10:35
  • How should I use this if I using spring 4 – moussesj94 Aug 25 '20 at 00:11
  • In my case, each field of the request seems to be printing in a separate line... not sure why – firstpostcommenter Dec 23 '20 at 14:47
  • 1
    Is there a way log only the request body and not the response? – Raj Jul 04 '21 at 04:10
83

Extending @hstoerr answer with some code:


Create LoggingRequestInterceptor to log requests responses

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

        ClientHttpResponse response = execution.execute(request, body);

        log(request,body,response);

        return response;
    }

    private void log(HttpRequest request, byte[] body, ClientHttpResponse response) throws IOException {
        //do logging
    }
}

Setup RestTemplate

RestTemplate rt = new RestTemplate();

//set interceptors/requestFactory
ClientHttpRequestInterceptor ri = new LoggingRequestInterceptor();
List<ClientHttpRequestInterceptor> ris = new ArrayList<ClientHttpRequestInterceptor>();
ris.add(ri);
rt.setInterceptors(ris);
rt.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
Adam Michalik
  • 9,678
  • 13
  • 71
  • 102
mjj1409
  • 3,075
  • 6
  • 28
  • 28
  • This is not available until spring-3.1 version. – Gyan Jun 25 '14 at 11:55
  • 4
    it not answer the question of 'logging response', but leave a //do logging comment instead. – Jiang YD May 28 '15 at 03:51
  • 1
    doing the logging was easy, but this works only for requests, I don't see response bodies, suppose I have response object, but reading it's stream is not good idea. – Pavel Niedoba Jun 29 '15 at 09:33
  • @PavelNiedoba Why would you say that reading its stream is not a good idea? – Uhla Aug 01 '15 at 17:32
  • By the way, I was able to manage logging both request and response via this approach - requests were a bit tricky as they have the outputstream, which you have to parse as bytearrayoutputstream. I am using springTemplate.exchange method, because I need to send some authentication headers as well. – Uhla Aug 01 '15 at 17:33
  • Reading stream of response is not good because if you do it you actually consume the data and original receiver will not receive it. I solved it by employing king of stream splitter added to the filter chain – Pavel Niedoba Aug 02 '15 at 21:22
  • 11
    @PavelNiedoba The BufferClientHttpRequestFactory allows the response to be read more than one time. – mjj1409 Aug 03 '15 at 16:01
  • 2
    This works well if you need to store info about request/response in a database for debugging and regular logging doesn't suit your needs. – GameSalutes Jan 30 '16 at 16:19
  • The crux of this problem is how to log the response string. This answer conveniently skips over that detail. – James Watkins Mar 01 '16 at 20:33
49

Your best bet is to add logging.level.org.springframework.web.client.RestTemplate=DEBUG to the application.properties file.

Other solutions like setting log4j.logger.httpclient.wire will not always work because they assume you use log4j and Apache HttpClient, which is not always true.

Note, however, that this syntax will work only on latest versions of Spring Boot.

gamliela
  • 3,447
  • 1
  • 35
  • 41
41

You can use spring-rest-template-logger to log RestTemplate HTTP traffic.

Add a dependency to your Maven project:

<dependency>
    <groupId>org.hobsoft.spring</groupId>
    <artifactId>spring-rest-template-logger</artifactId>
    <version>2.0.0</version>
</dependency>

Then customize your RestTemplate as follows:

RestTemplate restTemplate = new RestTemplateBuilder()
    .customizers(new LoggingCustomizer())
    .build()

Ensure that debug logging is enabled in application.properties:

logging.level.org.hobsoft.spring.resttemplatelogger.LoggingCustomizer = DEBUG

Now all RestTemplate HTTP traffic will be logged to org.hobsoft.spring.resttemplatelogger.LoggingCustomizer at debug level.

DISCLAIMER: I wrote this library.

Mark Hobson
  • 1,591
  • 1
  • 12
  • 11
  • Why does this answer is downvoted? It helped me. Thanks, @Mark Hobson. – Raffael Bechara Rameh Feb 16 '18 at 15:41
  • 3
    Glad it helped @RaffaelBecharaRameh. It was initially downvoted because I didn't embed instructions from the linked project. Feel free to upvote if you found it useful! – Mark Hobson Feb 24 '18 at 20:43
  • 1
    Do you support via Gradle? – BlackHatSamurai Jun 05 '19 at 18:51
  • 1
    @BlackHatSamurai spring-rest-template-logger is a regular Maven artifact, so it should work fine with Gradle. – Mark Hobson Jun 06 '19 at 19:18
  • For Spring Boot 2, you need to add `logging.level.org.hobsoft.spring.resttemplatelogger.LoggingCustomizer=DEBUG` to `application.properties` to view the log. – attacomsian Feb 04 '20 at 19:18
  • Thanks @attacomsian - I've updated the answer and project README. – Mark Hobson Feb 05 '20 at 20:39
  • Hi Mark, thanks for developing this library, I would like to learn how did you handle inputstream ? did you use BufferingClientHttpRequestFactory ? – erhanasikoglu Feb 07 '20 at 05:50
  • 1
    Hi @erhanasikoglu, you're welcome! That's right, you can see it in use here: https://github.com/markhobson/spring-rest-template-logger/blob/master/src/main/java/org/hobsoft/spring/resttemplatelogger/LoggingCustomizer.java#L62 – Mark Hobson Feb 08 '20 at 09:55
33

The solution given by xenoterracide to use

logging.level.org.apache.http=DEBUG

is good but the problem is that by default Apache HttpComponents is not used.

To use Apache HttpComponents add to your pom.xml

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
</dependency>

and configure RestTemplate with :

RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());
Ortomala Lokni
  • 56,620
  • 24
  • 188
  • 240
32

None of these answers actually solve 100% of the problem. mjj1409 gets most of it, but conveniently avoids the issue of logging the response, which takes a bit more work. Paul Sabou provides a solution that seems realistic, but doesn't provide enough details to actually implement (and it didn't work at all for me). Sofiene got the logging but with a critical problem: the response is no longer readable because the input stream has already been consumed!

I recommend using a BufferingClientHttpResponseWrapper to wrap the response object to allow reading the response body multiple times:

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        ClientHttpResponse response = execution.execute(request, body);

        response = log(request, body, response);

        return response;
    }

    private ClientHttpResponse log(final HttpRequest request, final byte[] body, final ClientHttpResponse response) {
        final ClientHttpResponse responseCopy = new BufferingClientHttpResponseWrapper(response);
        logger.debug("Method: ", request.getMethod().toString());
        logger.debug("URI: ", , request.getURI().toString());
        logger.debug("Request Body: " + new String(body));
        logger.debug("Response body: " + IOUtils.toString(responseCopy.getBody()));
        return responseCopy;
    }

}

This will not consume the InputStream because the response body is loaded into memory and can be read multiple times. If you do not have the BufferingClientHttpResponseWrapper on your classpath, you can find the simple implementation here:

https://github.com/spring-projects/spring-android/blob/master/spring-android-rest-template/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java

For setting up the RestTemplate:

LoggingRequestInterceptor loggingInterceptor = new LoggingRequestInterceptor();
restTemplate.getInterceptors().add(loggingInterceptor);
James Watkins
  • 4,806
  • 5
  • 32
  • 42
  • same, responseCopy.getBody() throw IOexception in case of 404, so you never send back to your further code the response and the normally RestClientResponseException become a ResourceAccessException – MilacH Jan 26 '17 at 13:53
  • 1
    You should check `status==200`, before `responseCopy.getBody()` – Anand Rockzz Jan 29 '17 at 10:11
  • 4
    But it's package-private. Did you put your LoggingRequestInterceptor in package 'org.springframework.http.client'? – zbstof Jun 01 '17 at 12:40
  • 2
    what about `asyncRestTemplate`? It would require to return a `ListenableFuture` when you intercept it which is not possible to alter with `BufferingClientHttpResponseWrapper` in a callback. – Ömer Faruk Almalı Feb 03 '18 at 14:05
  • @ÖmerFarukAlmalı In that case you will need to use chain or transform depending on the version of Guava you're using. See: https://stackoverflow.com/questions/8191891/is-it-possible-to-chain-async-calls-using-guava – James Watkins Feb 05 '18 at 15:30
  • Note that this library also uses a buffer to keep the response in memory, causing potential memory issues with large responses. – Tobias Jul 05 '22 at 06:56
25

Logging RestTemplate

Option 1. Open debug logging.

Configurate RestTemplate

  • By default the RestTemplate relies on standard JDK facilities to establish HTTP connections. You can switch to use a different HTTP library such as Apache HttpComponents

    @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { RestTemplate restTemplate = builder.build(); return restTemplate; }

Configurate logging

  • application.yml

    logging: level: org.springframework.web.client.RestTemplate: DEBUG

Option 2. Using Interceptor

Wrapper Response

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;

public final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {

    private final ClientHttpResponse response;

    private byte[] body;


    BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
        this.response = response;
    }

    public HttpStatus getStatusCode() throws IOException {
        return this.response.getStatusCode();
    }

    public int getRawStatusCode() throws IOException {
        return this.response.getRawStatusCode();
    }

    public String getStatusText() throws IOException {
        return this.response.getStatusText();
    }

    public HttpHeaders getHeaders() {
        return this.response.getHeaders();
    }

    public InputStream getBody() throws IOException {
        if (this.body == null) {
            this.body = StreamUtils.copyToByteArray(this.response.getBody());
        }
        return new ByteArrayInputStream(this.body);
    }

    public void close() {
        this.response.close();
    }
}

Implement Interceptor

package com.example.logging;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class LoggingRestTemplate implements ClientHttpRequestInterceptor {

    private final static Logger LOGGER = LoggerFactory.getLogger(LoggingRestTemplate.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution execution) throws IOException {
        traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        return traceResponse(response);
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        if (!LOGGER.isDebugEnabled()) {
            return;
        }
        LOGGER.debug(
                "==========================request begin==============================================");
        LOGGER.debug("URI                 : {}", request.getURI());
        LOGGER.debug("Method            : {}", request.getMethod());
        LOGGER.debug("Headers         : {}", request.getHeaders());
        LOGGER.debug("Request body: {}", new String(body, "UTF-8"));
        LOGGER.debug(
                "==========================request end================================================");
    }

    private ClientHttpResponse traceResponse(ClientHttpResponse response) throws IOException {
        if (!LOGGER.isDebugEnabled()) {
            return response;
        }
        final ClientHttpResponse responseWrapper = new BufferingClientHttpResponseWrapper(response);
        StringBuilder inputStringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(responseWrapper.getBody(), "UTF-8"));
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            inputStringBuilder.append('\n');
            line = bufferedReader.readLine();
        }
        LOGGER.debug(
                "==========================response begin=============================================");
        LOGGER.debug("Status code    : {}", responseWrapper.getStatusCode());
        LOGGER.debug("Status text    : {}", responseWrapper.getStatusText());
        LOGGER.debug("Headers            : {}", responseWrapper.getHeaders());
        LOGGER.debug("Response body: {}", inputStringBuilder.toString());
        LOGGER.debug(
                "==========================response end===============================================");
        return responseWrapper;
    }

}

Configurate RestTemplate

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    RestTemplate restTemplate = builder.build();
    restTemplate.setInterceptors(Collections.singletonList(new LoggingRestTemplate()));
    return restTemplate;
}

Configurate logging

  • Check the package of LoggingRestTemplate, for example in application.yml:

    logging: level: com.example.logging: DEBUG

Option 3. Using httpcomponent

Import httpcomponent dependency

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpasyncclient</artifactId>

Configurate RestTemplate

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    RestTemplate restTemplate = builder.build();
    restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());
    return restTemplate;
}

Configurate logging

  • Check the package of LoggingRestTemplate, for example in application.yml:

    logging: level: org.apache.http: DEBUG

user2746033
  • 23
  • 2
  • 6
  • Just note: if you'd like to configure `TestRestTemplate`, configure `RestTemplateBuilder`: @Bean public RestTemplateBuilder restTemplateBuilder() { return new RestTemplateBuilder().additionalInterceptors(Collections.singletonList(new LoggingRestTemplate())); } – kingoleg Sep 21 '18 at 15:55
  • Note also that new InputStreamReader(responseWrapper.getBody(), StandardCharsets.UTF_8)); can throw an error if the "other end" returned an error. You may want to place it into a try block. – PeterS Oct 09 '18 at 12:18
25

I finally found a way to do this in the right way. Most of the solution comes from How do I configure Spring and SLF4J so that I can get logging?

It seems there are two things that need to be done :

  1. Add the following line in log4j.properties : log4j.logger.httpclient.wire=DEBUG
  2. Make sure spring doesn't ignore your logging config

The second issue happens mostly to spring environments where slf4j is used (as it was my case). As such, when slf4j is used make sure that the following two things happen :

  1. There is no commons-logging library in your classpath : this can be done by adding the exclusion descriptors in your pom :

            <exclusions><exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    
  2. The log4j.properties file is stored somewhere in the classpath where spring can find/see it. If you have problems with this, a last resort solution would be to put the log4j.properties file in the default package (not a good practice but just to see that things work as you expect)

Community
  • 1
  • 1
Paul Sabou
  • 3,097
  • 3
  • 16
  • 9
  • 8
    This doesn't work for me, I did both things. I don't understand why do I need to put log4j.properties when it's not used anyway in my project (checked by mvn dependency:tree) – Pavel Niedoba Jun 19 '15 at 14:35
  • This doesn't work for me either. I even tried setting the root logger to Debug mode and still nothing. – James Watkins Mar 01 '16 at 18:06
  • "httpclient.wire.content" and "httpclient.wire.header" are logger names from the Axis2 framework. They can be used to log e.g. SOAP requests in a Spring project *if* those are done using Axis2. – lathspell Mar 10 '17 at 15:46
  • 12
    `httpclient.wire` is actually from the Apache HttpComponents HttpClient library (see https://hc.apache.org/httpcomponents-client-ga/logging.html). This technique will only work if you have `RestTemplate` configured to use the `HttpComponentsClientHttpRequestFactory` – Scott Frederick Jun 09 '17 at 19:58
24

---- July 2019 ----

(using Spring Boot)

I was surprised that Spring Boot, with all it's Zero Configuration magic, doesn't provide an easy way to inspect or log a simple JSON response body with RestTemplate. I looked through the various answers and comments provided here, and am sharing my own distilled version of what (still) works and seems to me like a reasonable solution, given the current options (I'm using Spring Boot 2.1.6 with Gradle 4.4)

1. Using Fiddler as http proxy

This is actually quite an elegant solution, as it bypasses all the cumbersome efforts of creating your own interceptor or changing the underlying http client to apache (see below).

Install and run Fiddler

and then

add -DproxySet=true -Dhttp.proxyHost=localhost -Dhttp.proxyPort=8888 to your VM Options

2. Using Apache HttpClient

Add Apache HttpClient to your Maven or Gradle dependencies.

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.9</version>
</dependency>

Use HttpComponentsClientHttpRequestFactory as RequestFactory for RestTemplate. The simplest way to do that would be:

RestTemplate restTemplate = new RestTemplate();

restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

Enable DEBUG in your application.properties file (if you're using Spring Boot)

logging.level.org.apache.http=DEBUG

If you're using Spring Boot, you'll need to make sure you have a logging framework set up, e.g. by using a spring-boot-starter dependency that includes spring-boot-starter-logging.

3. Use an Interceptor

I'll let you read through the proposals, counter-proposals, and gotchas in the other answers and comments and decide for yourself if you want to go down that path.

4. Log URL and Response Status without Body

Although this doesn't meet the stated requirements of logging the body, it's a quick and simple way to start logging your REST calls. It displays the full URL and response status.

Simply add the following line to your application.properties file (assuming you're using Spring Boot, and assuming you are using a spring boot starter dependency that includes spring-boot-starter-logging)

logging.level.org.springframework.web.client.RestTemplate=DEBUG

The output will look something like this:

2019-07-29 11:53:50.265 DEBUG o.s.web.client.RestTemplate : HTTP GET http://www.myrestservice.com/Endpoint?myQueryParam=myValue
2019-07-29 11:53:50.276 DEBUG o.s.web.client.RestTemplate : Accept=[application/json]
2019-07-29 11:53:50.584 DEBUG o.s.web.client.RestTemplate : Response 200 OK
2019-07-29 11:53:50.585 DEBUG o.s.web.client.RestTemplate : Reading to [org.mynamespace.MyJsonModelClass]
Community
  • 1
  • 1
Chris
  • 4,212
  • 5
  • 37
  • 52
19

As stated in the other responses, the response body needs special treatment so it can be read repeatedly (by default, its contents get consumed on the first read).

Instead of using the BufferingClientHttpRequestFactory when setting up the request, the interceptor itself can wrap the response and make sure the content is retained and can be repeatedly read (by the logger as well as by the consumer of the response):

My interceptor, which

  • buffers the response body using a wrapper
  • logs in a more compact way
  • logs the status code identifier as well (e.g. 201 Created)
  • includes a request sequence number allowing to easily distinguish concurrent log entries from multiple threads

Code:

public class LoggingInterceptor implements ClientHttpRequestInterceptor {

    private final Logger log = LoggerFactory.getLogger(getClass());
    private AtomicInteger requestNumberSequence = new AtomicInteger(0);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        int requestNumber = requestNumberSequence.incrementAndGet();
        logRequest(requestNumber, request, body);
        ClientHttpResponse response = execution.execute(request, body);
        response = new BufferedClientHttpResponse(response);
        logResponse(requestNumber, response);
        return response;
    }

    private void logRequest(int requestNumber, HttpRequest request, byte[] body) {
        if (log.isDebugEnabled()) {
            String prefix = requestNumber + " > ";
            log.debug("{} Request: {} {}", prefix, request.getMethod(), request.getURI());
            log.debug("{} Headers: {}", prefix, request.getHeaders());
            if (body.length > 0) {
                log.debug("{} Body: \n{}", prefix, new String(body, StandardCharsets.UTF_8));
            }
        }
    }

    private void logResponse(int requestNumber, ClientHttpResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            String prefix = requestNumber + " < ";
            log.debug("{} Response: {} {} {}", prefix, response.getStatusCode(), response.getStatusCode().name(), response.getStatusText());
            log.debug("{} Headers: {}", prefix, response.getHeaders());
            String body = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
            if (body.length() > 0) {
                log.debug("{} Body: \n{}", prefix, body);
            }
        }
    }

    /**
     * Wrapper around ClientHttpResponse, buffers the body so it can be read repeatedly (for logging & consuming the result).
     */
    private static class BufferedClientHttpResponse implements ClientHttpResponse {

        private final ClientHttpResponse response;
        private byte[] body;

        public BufferedClientHttpResponse(ClientHttpResponse response) {
            this.response = response;
        }

        @Override
        public HttpStatus getStatusCode() throws IOException {
            return response.getStatusCode();
        }

        @Override
        public int getRawStatusCode() throws IOException {
            return response.getRawStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return response.getStatusText();
        }

        @Override
        public void close() {
            response.close();
        }

        @Override
        public InputStream getBody() throws IOException {
            if (body == null) {
                body = StreamUtils.copyToByteArray(response.getBody());
            }
            return new ByteArrayInputStream(body);
        }

        @Override
        public HttpHeaders getHeaders() {
            return response.getHeaders();
        }
    }
}

Configuration:

 @Bean
    public RestTemplateBuilder restTemplateBuilder() {
        return new RestTemplateBuilder()
                .additionalInterceptors(Collections.singletonList(new LoggingInterceptor()));
    }

Example log output:

2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 >  Request: POST http://localhost:53969/payment/v4/private/payment-lists/10022/templates
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 >  Headers: {Accept=[application/json, application/json], Content-Type=[application/json;charset=UTF-8], Content-Length=[986]}
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 >  Body: 
{"idKey":null, ...}
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 <  Response: 200 OK 
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 <  Headers: {Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Mon, 08 Oct 2018 08:58:53 GMT]}
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 <  Body: 
{ "idKey" : "10022", ...  }
Peter Walser
  • 15,208
  • 4
  • 51
  • 78
  • 1
    This one works with the 2019 Spring version keeping the body intact. – Udo Held Sep 19 '19 at 02:13
  • 1
    Works on Spring 2.1.10 :) Thanks – Moler May 14 '20 at 12:45
  • 1
    I was struggling when trying this solution, I thought it wasn't working, then I figured out that the logs were added as Debug, so after I set my log level to DEBUG, the logs showed up. I know it's kinda obvious (my bad I didn't notice that), but I think it's worth to include this info on your solution, which is pretty good, BTW. For me, this info would have saved some investigation time lol. After that, I could find out what I was doing wrong in my request body. Excellent approach! Thanks. :) – Bruno T. Abrahão Jun 02 '22 at 21:08
  • `BufferedClientHttpResponse` causes a memory leak because you have a reference to response objet – Zeuzif Mar 21 '23 at 11:53
12

Besides the HttpClient logging described in the other answer, you can also introduce a ClientHttpRequestInterceptor that reads the body of the request and the response and logs it. You might want to do this if other stuff also uses the HttpClient, or if you want a custom logging format. Caution: you will want to give the RestTemplate a BufferingClientHttpRequestFactory such that you can read the response twice.

Community
  • 1
  • 1
Dr. Hans-Peter Störr
  • 25,298
  • 30
  • 102
  • 139
10

This might not be the correct way to do it, but I think this is the most simple approach to print requests and responses without filling too much in logs.

By adding below 2 lines application.properties logs all requests and responses 1st line in order to log the requests and 2nd line to log the responses.

logging.level.org.springframework.web.client.RestTemplate=DEBUG
logging.level.org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor=DEBUG
Dherik
  • 17,757
  • 11
  • 115
  • 164
Viggi
  • 215
  • 3
  • 7
10

application.properties

logging.level.org.springframework.web.client=DEBUG

application.yml

logging:
  level:  
    root: WARN
    org.springframework.web.client: DEBUG
Arpit Aggarwal
  • 27,626
  • 16
  • 90
  • 108
Elton Sandré
  • 372
  • 3
  • 7
6

Assuming RestTemplate is configured to use HttpClient 4.x, you can read up on HttpClient's logging documentation here. The loggers are different than those specified in the other answers.

The logging configuration for HttpClient 3.x is available here.

Emerson Farrugia
  • 11,153
  • 5
  • 43
  • 51
5

So many responses here require coding changes and customized classes and it really is not necessary. Gte a debugging proxy such as fiddler and set your java environment to use the proxy on the command line (-Dhttp.proxyHost and -Dhttp.proxyPort) then run fiddler and you can see the requests and responses in their entirety. Also comes with many ancillary advantages such as the ability to tinker with the results and responses before and after they are sent to run experiments before committing to modification of the server.

Last bit of an issue that can come up is if you must use HTTPS, you will need to export the SSL cert from fiddler and import it into the java keystore (cacerts) hint: default java keystore password is usually "changeit".

Lee Burch
  • 51
  • 1
  • 2
  • 1
    This worked for me using intellij and the regular install of fiddle. I edited the Run Configuration and set VM options to `-DproxySet=true -Dhttp.proxyHost=localhost -Dhttp.proxyPort=8888`. – J.D Apr 09 '19 at 16:49
  • Thanks! This is a pretty elegant solution compared to writing your own Interceptor. – Chris Jul 29 '19 at 09:19
  • Yes, this is the direction we went and haven't gone back to messing w/ Spring / Interceptors / logging and so on. Though not Fiddler - we've been using the TCP/IP Monitor eclipse plugin, likely there is something similar in IntelliJ. For me Fiddler is great, but causes various issues with certificates, VPNs, so, depending on your environment, it might not be the ideal solution. – michaelok Sep 19 '21 at 18:39
5

For logging to Logback with help from Apache HttpClient:

You need Apache HttpClient in classpath:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.10</version>
</dependency>

Configure your RestTemplate to use HttpClient:

restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

To log requests and responses, add to Logback configuration file:

<logger name="org.apache.http.wire" level="DEBUG"/>

Or to log even more:

<logger name="org.apache.http" level="DEBUG"/>
holmis83
  • 15,922
  • 5
  • 82
  • 83
2

Adding to above discussion this only represents Happy scenarios. probably you will not be able to log the response if an Error comes .

In this case plus all the cases above you must override DefaultResponseErrorHandler and set it like below

restTemplate.setErrorHandler(new DefaultResponseErrorHandlerImpl());
user666
  • 1,104
  • 12
  • 20
2

Strangely, none of these solutions work as RestTemplate does not seem to return the response on some client and server 500x errors. In which case, you will have log those as well by implementing ResponseErrorHandler as follows. Here is a draft code, but you get the point:

You can set the same interceptor as the error handler:

restTemplate.getInterceptors().add(interceptor);
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
restTemplate.setErrorHandler(interceptor);

And the intercept implements both interfaces:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus.Series;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.ResponseErrorHandler;

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor, ResponseErrorHandler {
    static final Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);
    static final DefaultResponseErrorHandler defaultResponseErrorHandler = new DefaultResponseErrorHandler();
    final Set<Series> loggableStatuses = new HashSet();

    public LoggingRequestInterceptor() {
    }

    public LoggingRequestInterceptor(Set<Series> loggableStatuses) {
        loggableStatuses.addAll(loggableStatuses);
    }

    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        this.traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        if(response != null) {
            this.traceResponse(response);
        }

        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        log.debug("===========================request begin================================================");
        log.debug("URI         : {}", request.getURI());
        log.debug("Method      : {}", request.getMethod());
        log.debug("Headers     : {}", request.getHeaders());
        log.debug("Request body: {}", new String(body, "UTF-8"));
        log.debug("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        if(this.loggableStatuses.isEmpty() || this.loggableStatuses.contains(response.getStatusCode().series())) {
            StringBuilder inputStringBuilder = new StringBuilder();

            try {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), "UTF-8"));

                for(String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) {
                    inputStringBuilder.append(line);
                    inputStringBuilder.append('\n');
                }
            } catch (Throwable var5) {
                log.error("cannot read response due to error", var5);
            }

            log.debug("============================response begin==========================================");
            log.debug("Status code  : {}", response.getStatusCode());
            log.debug("Status text  : {}", response.getStatusText());
            log.debug("Headers      : {}", response.getHeaders());
            log.debug("Response body: {}", inputStringBuilder.toString());
            log.debug("=======================response end=================================================");
        }

    }

    public boolean hasError(ClientHttpResponse response) throws IOException {
        return defaultResponseErrorHandler.hasError(response);
    }

    public void handleError(ClientHttpResponse response) throws IOException {
        this.traceResponse(response);
        defaultResponseErrorHandler.handleError(response);
    }
}
kisna
  • 2,869
  • 1
  • 25
  • 30
  • What if body is multipart/form-data, is there an easy way to filter out binary data (file content) from the log ? – Luke Jul 30 '18 at 17:50
1

The trick of configuring your RestTemplate with a BufferingClientHttpRequestFactory doesn't work if you are using any ClientHttpRequestInterceptor, which you will if you are trying to log via interceptors. This is due to the way that InterceptingHttpAccessor (which RestTemplate subclasses) works.

Long story short... just use this class in place of RestTemplate (note this uses the SLF4J logging API, edit as needed):

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;

/**
 * A {@link RestTemplate} that logs every request and response.
 */
public class LoggingRestTemplate extends RestTemplate {

    // Bleh, this class is not public
    private static final String RESPONSE_WRAPPER_CLASS = "org.springframework.http.client.BufferingClientHttpResponseWrapper";

    private Logger log = LoggerFactory.getLogger(this.getClass());

    private boolean hideAuthorizationHeaders = true;
    private Class<?> wrapperClass;
    private Constructor<?> wrapperConstructor;

    /**
     * Configure the logger to log requests and responses to.
     *
     * @param log log destination, or null to disable
     */
    public void setLogger(Logger log) {
        this.log = log;
    }

    /**
     * Configure the logger to log requests and responses to by name.
     *
     * @param name name of the log destination, or null to disable
     */
    public void setLoggerName(String name) {
        this.setLogger(name != null ? LoggerFactory.getLogger(name) : null);
    }

    /**
     * Configure whether to hide the contents of {@code Authorization} headers.
     *
     * <p>
     * Default true.
     *
     * @param hideAuthorizationHeaders true to hide, otherwise false
     */
    public void setHideAuthorizationHeaders(boolean hideAuthorizationHeaders) {
        this.hideAuthorizationHeaders = hideAuthorizationHeaders;
    }

    /**
     * Log a request.
     */
    protected void traceRequest(HttpRequest request, byte[] body) {
        this.log.debug("xmit: {} {}\n{}{}", request.getMethod(), request.getURI(), this.toString(request.getHeaders()),
          body != null && body.length > 0 ? "\n\n" + new String(body, StandardCharsets.UTF_8) : "");
    }

    /**
     * Log a response.
     */
    protected void traceResponse(ClientHttpResponse response) {
        final ByteArrayOutputStream bodyBuf = new ByteArrayOutputStream();
        HttpStatus statusCode = null;
        try {
            statusCode = response.getStatusCode();
        } catch (IOException e) {
            // ignore
        }
        String statusText = null;
        try {
            statusText = response.getStatusText();
        } catch (IOException e) {
            // ignore
        }
        try (final InputStream input = response.getBody()) {
            byte[] b = new byte[1024];
            int r;
            while ((r = input.read(b)) != -1)
                bodyBuf.write(b, 0, r);
        } catch (IOException e) {
            // ignore
        }
        this.log.debug("recv: {} {}\n{}{}", statusCode, statusText, this.toString(response.getHeaders()),
          bodyBuf.size() > 0 ? "\n\n" + new String(bodyBuf.toByteArray(), StandardCharsets.UTF_8) : "");
    }

    @PostConstruct
    private void addLoggingInterceptor() {
        this.getInterceptors().add(new ClientHttpRequestInterceptor() {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
              throws IOException {

                // Log request
                if (LoggingRestTemplate.this.log != null && LoggingRestTemplate.this.log.isDebugEnabled())
                    LoggingRestTemplate.this.traceRequest(request, body);

                // Perform request
                ClientHttpResponse response = execution.execute(request, body);

                // Log response
                if (LoggingRestTemplate.this.log != null && LoggingRestTemplate.this.log.isDebugEnabled()) {
                    final ClientHttpResponse bufferedResponse = LoggingRestTemplate.this.ensureBuffered(response);
                    if (bufferedResponse != null) {
                        LoggingRestTemplate.this.traceResponse(bufferedResponse);
                        response = bufferedResponse;
                    }
                }

                // Done
                return response;
            }
        });
    }

    private ClientHttpResponse ensureBuffered(ClientHttpResponse response) {
        try {
            if (this.wrapperClass == null)
                this.wrapperClass = Class.forName(RESPONSE_WRAPPER_CLASS, false, ClientHttpResponse.class.getClassLoader());
            if (!this.wrapperClass.isInstance(response)) {
                if (this.wrapperConstructor == null) {
                    this.wrapperConstructor = this.wrapperClass.getDeclaredConstructor(ClientHttpResponse.class);
                    this.wrapperConstructor.setAccessible(true);
                }
                response = (ClientHttpResponse)this.wrapperConstructor.newInstance(response);
            }
            return response;
        } catch (Exception e) {
            this.log.error("error creating {} instance: {}", RESPONSE_WRAPPER_CLASS, e);
            return null;
        }
    }

    private String toString(HttpHeaders headers) {
        final StringBuilder headerBuf = new StringBuilder();
        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
            if (headerBuf.length() > 0)
                headerBuf.append('\n');
            final String name = entry.getKey();
            for (String value : entry.getValue()) {
                if (this.hideAuthorizationHeaders && name.equalsIgnoreCase(HttpHeaders.AUTHORIZATION))
                    value = "[omitted]";
                headerBuf.append(name).append(": ").append(value);
            }
        }
        return headerBuf.toString();
    }
}

I agree it's silly that it takes this much work just to do this.

Archie
  • 4,959
  • 1
  • 30
  • 36
1

As @MilacH pointed out, there is an error in the implementation. If an statusCode > 400 is returned a IOException is thrown, as the errorHandler is not invoked, from interceptors. The exception can be ignored and is then caught again in the handler method.

package net.sprd.fulfillment.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import static java.nio.charset.StandardCharsets.UTF_8;

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    final static Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @SuppressWarnings("HardcodedLineSeparator")
    public static final char LINE_BREAK = '\n';

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        try {
            traceRequest(request, body);
        } catch (Exception e) {
            log.warn("Exception in LoggingRequestInterceptor while tracing request", e);
        }

        ClientHttpResponse response = execution.execute(request, body);

        try {
            traceResponse(response);
        } catch (IOException e) {
            // ignore the exception here, as it will be handled by the error handler of the restTemplate
            log.warn("Exception in LoggingRequestInterceptor", e);
        }
        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) {
        log.info("===========================request begin================================================");
        log.info("URI         : {}", request.getURI());
        log.info("Method      : {}", request.getMethod());
        log.info("Headers     : {}", request.getHeaders());
        log.info("Request body: {}", new String(body, UTF_8));
        log.info("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), UTF_8))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append(LINE_BREAK);
                line = bufferedReader.readLine();
            }
        }

        log.info("============================response begin==========================================");
        log.info("Status code  : {}", response.getStatusCode());
        log.info("Status text  : {}", response.getStatusText());
        log.info("Headers      : {}", response.getHeaders());
        log.info("Response body: {}", inputStringBuilder);
        log.info("=======================response end=================================================");
    }

}
Tony Findeisen
  • 380
  • 4
  • 5
1

org.apache.http.wire gives too unreadable logs, so I use logbook to log application Servlet and RestTemplate requests & responses with payloads.

build.gradle:

compile group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.6.2'

or Maven dependency:

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-spring-boot-starter</artifactId>
    <version>2.6.2</version>
</dependency>

application.properties (or trough YAML):

logging.level.org.zalando.logbook = TRACE

RestTemplate.java:

import java.util.function.Supplier;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.zalando.logbook.httpclient.LogbookHttpRequestInterceptor;
import org.zalando.logbook.httpclient.LogbookHttpResponseInterceptor;

@Configuration
public class RestTemplateConfiguration {
    private final LogbookHttpRequestInterceptor logbookHttpRequestInterceptor;
    private final LogbookHttpResponseInterceptor logbookHttpResponseInterceptor;

    public RestTemplateConfiguration(LogbookHttpRequestInterceptor logbookHttpRequestInterceptor,
            LogbookHttpResponseInterceptor logbookHttpResponseInterceptor) {
        this.logbookHttpRequestInterceptor = logbookHttpRequestInterceptor;
        this.logbookHttpResponseInterceptor = logbookHttpResponseInterceptor;
    }

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .requestFactory(new MyRequestFactorySupplier())
                .build();
    }

    class MyRequestFactorySupplier implements Supplier<ClientHttpRequestFactory> {
        @Override
        public ClientHttpRequestFactory get() {
            // Using Apache HTTP client
            CloseableHttpClient client = HttpClientBuilder.create()
                    .addInterceptorFirst(logbookHttpRequestInterceptor)
                    .addInterceptorFirst(logbookHttpResponseInterceptor)
                    .build();
            return new HttpComponentsClientHttpRequestFactory(client);
        }
    }
}
Doc Davluz
  • 4,154
  • 5
  • 30
  • 32
panser
  • 1,949
  • 22
  • 16
1

An easy way to solve the problem:

  1. Create a Bean of RestTemplate using RestTemplateBuilder: It will give you more control over connection time and reading time.
@Configuration
public class RestTemplateConfig {
  @Bean
  public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder
      .setConnectTimeout(Duration.ofMillis(60000))
      .setReadTimeout(Duration.ofMillis(60000))
      .build();
  }

}
  1. Add this line to the resources/application.properties file: logging.level.org.springframework.web.client.RestTemplate=DEBUG
    Hope the problem will be resolved!
Md. Shahariar Hossen
  • 1,367
  • 10
  • 11
1

Most of the above solution doesn't work properly when RestTemplate is used and there is a 4xx or 5xx response type as ClientHttpResponse's body is empty. Here is the solution that I used to log the whole HTTP request/response in RestTemplate without losing the response body information in all cases. The Spring boot version is <version>2.7.5</version>

1.Create LoggingInterceptor class

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;

import java.io.*;

@Component
@Slf4j
public class LoggingInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    traceRequest(request, body);
    ClientHttpResponse response = execution.execute(request, body);
    response = traceResponse(response);
    return response;
}

private void traceRequest(HttpRequest request, byte[] body) throws IOException {
    if (!log.isDebugEnabled()) {
        return;
    }
    log.debug("=========================== Request Begin ===========================");
    log.debug("URI          : " + request.getURI());
    log.debug("Method       : " + request.getMethod());
    log.debug("Headers      : " + request.getHeaders());
    log.debug("Body : " + new String(body, "utf-8"));
    log.debug("============================ Request End ============================");

}

private ClientHttpResponse traceResponse(ClientHttpResponse response) throws IOException {
    if (!log.isDebugEnabled()) {
        return response;
    }
    ClientHttpResponse newCopiedResponse = new BufferingClientHttpResponseWrapper(response);
    StringBuilder inputStringBuilder = new StringBuilder();
    // ClientHttpResponse there is no body in response in case of 4xx or 5xx code, so we skip the body part
    if (isSuccessStatus(response.getRawStatusCode())) {
        inputStringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(newCopiedResponse.getBody(), "UTF-8"));
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            line = bufferedReader.readLine();
        }
    }

    log.debug("=========================== Response Begin ===========================");
    log.debug("Status code   : {}", response.getStatusCode());
    log.debug("Status text   : {}", response.getStatusText());
    log.debug("Headers       : {}", response.getHeaders());
    if (isSuccessStatus(response.getRawStatusCode())) {
        log.debug("Response Body : {}", inputStringBuilder.toString());
        log.debug("============================ Response End ============================");
    }

    return newCopiedResponse;

}

private static boolean isSuccessStatus(int statusCode) {
    return (statusCode / 100) == 2;
}


/**
 * Wrapper around ClientHttpResponse, buffers the body so it can be read repeatedly (for logging & consuming the result).
 */
private static class BufferingClientHttpResponseWrapper implements ClientHttpResponse {

    private final ClientHttpResponse response;
    private byte[] body;

    public BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
        this.response = response;
    }

    @Override
    public InputStream getBody() throws IOException {
        if (body == null) {
            body = StreamUtils.copyToByteArray(response.getBody());
        }
        return new ByteArrayInputStream(body);
    }

    @Override
    public HttpStatus getStatusCode() throws IOException {
        return this.response.getStatusCode();
    }

    @Override
    public int getRawStatusCode() throws IOException {
        return this.response.getRawStatusCode();
    }

    @Override
    public String getStatusText() throws IOException {
        return this.response.getStatusText();
    }

    @Override
    public HttpHeaders getHeaders() {
        return this.response.getHeaders();
    }

    @Override
    public void close() {
        this.response.close();
    }
}

}

2.Attach it to the RestTemplate bean

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate createRestTemplate(LoggingInterceptor loggingInterceptor) {

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(loggingInterceptor));

        return restTemplate;
    }
}

3.Apply the appropriate level in the application

logging:
  level:
    com:
      test: DEBUG
0

Best solution now, just add dependency :

<dependency>
  <groupId>com.github.zg2pro</groupId>
  <artifactId>spring-rest-basis</artifactId>
  <version>v.x</version>
</dependency>

It contains a LoggingRequestInterceptor class you can add that way to your RestTemplate:

integrate this utility by adding it as an interceptor to a spring RestTemplate, in the following manner:

restTemplate.setRequestFactory(LoggingRequestFactoryFactory.build());

and add an slf4j implementation to your framework like log4j.

or directly use "Zg2proRestTemplate". The "best answer" by @PaulSabou looks so so, since httpclient and all apache.http libs are not necessarily loaded when using a spring RestTemplate.

  • what is the released version? – qwazer Aug 22 '17 at 10:11
  • released version now is 0.2 – Moses Meyer Aug 24 '17 at 13:10
  • 1
    ease of use is great, but it lacks headers – WrRaThY Dec 14 '17 at 13:29
  • additionally: all useful methods in LoggingRequestInterceptor are private, which is a problem when it comes to extension (could be protected) – WrRaThY Dec 14 '17 at 13:33
  • sadly, I can't edit comments after 5 minutes. All you have to do to log headers is this: `log("Headers: {}", request.headers)` in `LoggingRequestInterceptor:traceRequest` and `log("Headers: {}", response.headers)` in `LoggingRequestInterceptor:logResponse`. You might want to think about adding some flags for logging headers and body. Also - you may want to check body content type for logging (for example log only application/json*). This should be also configurable. all in all, with those little tweaks you'll have a nice library to spread. good work :) – WrRaThY Dec 14 '17 at 13:50
0

Wanted to add my implementation of this as well. I apologize for all the missing semi-colons, this is written in Groovy.

I needed something more configurable than the accepted answer provided. Here's a rest template bean that's very agile and will log everything like the OP is looking for.

Custom Logging Interceptor Class:

import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.util.StreamUtils

import java.nio.charset.Charset

class HttpLoggingInterceptor implements ClientHttpRequestInterceptor {

    private final static Logger log = LoggerFactory.getLogger(HttpLoggingInterceptor.class)

    @Override
    ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        logRequest(request, body)
        ClientHttpResponse response = execution.execute(request, body)
        logResponse(response)
        return response
    }

    private void logRequest(HttpRequest request, byte[] body) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("===========================request begin================================================")
            log.debug("URI         : {}", request.getURI())
            log.debug("Method      : {}", request.getMethod())
            log.debug("Headers     : {}", request.getHeaders())
            log.debug("Request body: {}", new String(body, "UTF-8"))
            log.debug("==========================request end================================================")
        }
    }

    private void logResponse(ClientHttpResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("============================response begin==========================================")
            log.debug("Status code  : {}", response.getStatusCode())
            log.debug("Status text  : {}", response.getStatusText())
            log.debug("Headers      : {}", response.getHeaders())
            log.debug("Response body: {}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))
            log.debug("=======================response end=================================================")
        }
    }
}

Rest Template Bean Definition:

@Bean(name = 'myRestTemplate')
RestTemplate myRestTemplate(RestTemplateBuilder builder) {

    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(10 * 1000) // 10 seconds
            .setSocketTimeout(300 * 1000) // 300 seconds
            .build()

    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager()
    connectionManager.setMaxTotal(10)
    connectionManager.closeIdleConnections(5, TimeUnit.MINUTES)

    CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setDefaultRequestConfig(requestConfig)
            .disableRedirectHandling()
            .build()

    RestTemplate restTemplate = builder
            .rootUri("https://domain.server.com")
            .basicAuthorization("username", "password")
            .requestFactory(new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)))
            .interceptors(new HttpLoggingInterceptor())
            .build()

    return restTemplate
}

Implementation:

@Component
class RestService {

    private final RestTemplate restTemplate
    private final static Logger log = LoggerFactory.getLogger(RestService.class)

    @Autowired
    RestService(
            @Qualifier("myRestTemplate") RestTemplate restTemplate
    ) {
        this.restTemplate = restTemplate
    }

    // add specific methods to your service that access the GET and PUT methods

    private <T> T getForObject(String path, Class<T> object, Map<String, ?> params = [:]) {
        try {
            return restTemplate.getForObject(path, object, params)
        } catch (HttpClientErrorException e) {
            log.warn("Client Error (${path}): ${e.responseBodyAsString}")
        } catch (HttpServerErrorException e) {
            String msg = "Server Error (${path}): ${e.responseBodyAsString}"
            log.error(msg, e)
        } catch (RestClientException e) {
            String msg = "Error (${path})"
            log.error(msg, e)
        }
        return null
    }

    private <T> T putForObject(String path, T object) {
        try {
            HttpEntity<T> request = new HttpEntity<>(object)
            HttpEntity<T> response = restTemplate.exchange(path, HttpMethod.PUT, request, T)
            return response.getBody()
        } catch (HttpClientErrorException e) {
            log.warn("Error (${path}): ${e.responseBodyAsString}")
        } catch (HttpServerErrorException e) {
            String msg = "Error (${path}): ${e.responseBodyAsString}"
            log.error(msg, e)
        } catch (RestClientException e) {
            String msg = "Error (${path})"
            log.error(msg, e)
        }
        return null
    }
}
Jason Slobotski
  • 1,386
  • 14
  • 18
0

Refer the Q/A for logging the request and response for the rest template by enabling the multiple reads on the HttpInputStream

Why my custom ClientHttpRequestInterceptor with empty response

maya16
  • 593
  • 1
  • 6
  • 19
0

I went through all answers, In case you need to set authentication type or connection time out, then you can do like this :

SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Integer.valueOf(YOUR_VALUE));
factory.setReadTimeout(Integer.valueOf(YOUR_VALUE));

RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(factory));
restTemplate.getInterceptors().add(new LoggingRequestInterceptor());
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(USER_NAME,PASSWORD,StandardCharsets.UTF_8));
return restTemplate;
Mostafa
  • 3,002
  • 10
  • 52
  • 79
-1

Related to the response using ClientHttpInterceptor, I found a way of keeping the whole response without Buffering factories. Just store the response body input stream inside byte array using some utils method that will copy that array from body, but important, surround this method with try catch because it will break if response is empty (that is the cause of Resource Access Exception) and in catch just create empty byte array, and than just create anonymous inner class of ClientHttpResponse using that array and other parameters from the original response. Than you can return that new ClientHttpResponse object to the rest template execution chain and you can log response using body byte array that is previously stored. That way you will avoid consuming InputStream in the actual response and you can use Rest Template response as it is. Note, this may be dangerous if your's response is too big

NenadTzar
  • 1
  • 1
-3

my logger config used xml

<logger name="org.springframework.web.client.RestTemplate">
    <level value="trace"/>
</logger>

then you will get something like below:

DEBUG org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:92) : Reading [com.test.java.MyClass] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@604525f1]

through HttpMessageConverterExtractor.java:92,you need continue to debug,and in my case,i got this:

genericMessageConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);

and this:

outputMessage.getBody().flush();

outputMessage.getBody() contains the message http(post type) sends