Skip to content

Instantly share code, notes, and snippets.

@jkuipers
Last active November 27, 2023 18:44
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save jkuipers/24ffbf8a5ba26c0177629e9aba492bfa to your computer and use it in GitHub Desktop.
Save jkuipers/24ffbf8a5ba26c0177629e9aba492bfa to your computer and use it in GitHub Desktop.
RestTemplate-interceptor that logs outgoing requests and resulting responses
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.MediaType;
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.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
/**
* Allows logging outgoing requests and the corresponding responses.
* Requires the use of a {@link org.springframework.http.client.BufferingClientHttpRequestFactory} to log
* the body of received responses.
*/
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
protected Logger requestLogger;
protected Logger responseLogger;
/**
* Creates an interceptor with request logger {@code spring.web.client.MessageTracing.sent}
* and response logger {@code spring.web.client.MessageTracing.received}, loosely following
* Spring-WS logger naming conventions.
*/
public LoggingClientHttpRequestInterceptor() {
this(LoggerFactory.getLogger("spring.web.client.MessageTracing.sent"),
LoggerFactory.getLogger("spring.web.client.MessageTracing.received"));
}
/**
* @param requestLogger the logger used to log sent requests
* @param responseLogger the logger used to log received responses
*/
public LoggingClientHttpRequestInterceptor(Logger requestLogger, Logger responseLogger) {
this.requestLogger = requestLogger;
this.responseLogger = responseLogger;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
logResponse(request, response);
return response;
}
protected void logRequest(HttpRequest request, byte[] body) {
if (requestLogger.isDebugEnabled()) {
StringBuilder builder = new StringBuilder("Sending ").append(request.getMethod()).append(" request to ").append(request.getURI());
if (body.length > 0 && hasTextBody(request.getHeaders())) {
String bodyText = new String(body, determineCharset(request.getHeaders()));
builder.append(": [").append(bodyText).append("]");
}
requestLogger.debug(builder.toString());
}
}
protected void logResponse(HttpRequest request, ClientHttpResponse response) {
if (responseLogger.isDebugEnabled()) {
try {
StringBuilder builder = new StringBuilder("Received \"")
.append(response.getRawStatusCode()).append(" ").append(response.getStatusText()).append("\" response for ")
.append(request.getMethod()).append(" request to ").append(request.getURI());
HttpHeaders responseHeaders = response.getHeaders();
long contentLength = responseHeaders.getContentLength();
if (contentLength != 0) {
if (hasTextBody(responseHeaders) && !isMockedResponse(response)) {
String bodyText = StreamUtils.copyToString(response.getBody(), determineCharset(responseHeaders));
builder.append(": [").append(bodyText).append("]");
} else {
if (contentLength == -1) {
builder.append(" with content of unknown length");
} else {
builder.append(" with content of length ").append(contentLength);
}
MediaType contentType = responseHeaders.getContentType();
if (contentType != null) {
builder.append(" and content type ").append(contentType);
} else {
builder.append(" and unknown content type");
}
}
}
responseLogger.debug(builder.toString());
} catch (IOException e) {
responseLogger.warn("Failed to log response for {} request to {}", request.getMethod(), request.getURI(), e);
}
}
}
protected boolean hasTextBody(HttpHeaders headers) {
MediaType contentType = headers.getContentType();
if (contentType != null) {
if ("text".equals(contentType.getType())) {
return true;
}
String subtype = contentType.getSubtype();
if (subtype != null) {
return "xml".equals(subtype) || "json".equals(subtype) ||
subtype.endsWith("+xml") || subtype.endsWith("+json");
}
}
return false;
}
protected Charset determineCharset(HttpHeaders headers) {
MediaType contentType = headers.getContentType();
if (contentType != null) {
try {
Charset charSet = contentType.getCharset();
if (charSet != null) {
return charSet;
}
} catch (UnsupportedCharsetException e) {
// ignore
}
}
return StandardCharsets.UTF_8;
}
private boolean isMockedResponse(ClientHttpResponse response) {
return "MockClientHttpResponse".equals(response.getClass().getSimpleName());
}
}
@marcosarshavin
Copy link

Thank you, it helped me a lot!

@tillmannheigel
Copy link

Thx 👍

@matschmann
Copy link

Thanks, this helped me a lot :-)

@maikelsperandio
Copy link

Thanks, this is so helpful!

@rmargam
Copy link

rmargam commented Aug 29, 2018

Hi, I have used a version of this code doing something like

log.info("Response body: {}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))

this exhausts the body and makes it so there is no body when the actual resources want to use it.
Is there a better way to do it ?

for example this spock test fails.

def 'fetch access token integration test'() {

    setup:
    RestTemplate rt = new RestTemplate()
    RequestResponseLoggingInterceptor ri = new RequestResponseLoggingInterceptor()
    rt.setInterceptors([ri])

    when:
    ResponseEntity<String> responseEntity= rt.exchange("http://google.com", HttpMethod.GET, null, String.class)

    then:
    responseEntity.hasBody()

}

@jkuipers
Copy link
Author

jkuipers commented Sep 4, 2018

@rmargam register a BufferingClientHttpRequestFactory with your RestTemplate as stated in the type-level JavaDoc to avoid that issue. It wraps the response in a class that buffers the response body internally, so that it can be consumed multiple times.
One way to automatically do that for all your RestTemplate beans is shown here, assuming you're using Spring Boot's RestTemplateBuilder: https://gist.github.com/jkuipers/cd462d163c2c1c81f34092de12f7bab2

@troeng
Copy link

troeng commented Sep 20, 2019

Thank you for this, so helpful. Finally a solution that works with the BufferingClientHttpRequestFactory that do not close the stream when reading.

@cakiciesranur
Copy link

hi, how can i use this interceptor in my project? I am trying to store response and request into mongodb, but i am confused how i can use this class. Can you help me please?

@venkat-cs428
Copy link

thank you @jkuipers .very helpful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment