Published on

How to Log Rest Requests and Responses With Spring REST Template

Authors

Throughout my career, I have integrated with a bunch of different services. Most have been SOAP services. Recently I have had to integrate more with REST services. In general, when integrating with a service it can be incredibly hard to debug issues when you cannot see what is being sent and received. REST is not special in this regard.

For the one fairly complex integration I was running into marshalling issues due to values not being in the response or being in a different JSON path. This can be debugged by using Postman with the same request, but this gets tedious very quickly.

After doing a bit of Googling I came across this answer. While I did not end up using the proposed solution, it set me on the right track. My solution consists of the following parts:

  • A REST Interceptor
  • A properties file to control when this interceptor is turned on/off (this check happens when the client is initialized on startup)
  • Use of the RestTemplateBuilder in the REST client to bring all this together
    • It is very important to use a BufferingClientHttpRequestFactory otherwise the stream will be empty/null after intercepting it (i.e. the consumer of the response will get back null)

The REST Interceptor

The terminology may sound all hardcore. But in reality, this is simply a piece of code that runs before or/and after every request and response.

Below I have my implementation of the interceptor which should work with basically any rest service. The only thing you may want to tweak is the log level as well as how and when items are logged. To skip logging simply pass the request/response straight through the implemented interface methods.

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.InputStreamReader


class RESTInterceptor : ClientHttpRequestInterceptor {
    override fun intercept(request: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
        traceRequest(request, body)
        val response = execution.execute(request, body)
        traceResponse(response)
        return response
    }

    private fun traceRequest(request: HttpRequest, body: ByteArray) {
        log.info("===========================request begin================================================")
        log.info("URI         : {}", request.uri)
        log.info("Method      : {}", request.method)
        log.info("Headers     : {}", request.headers)
        log.info("Request body: {}", String(body, Charsets.UTF_8))
        log.info("==========================request end================================================")
    }

    private fun traceResponse(response: ClientHttpResponse) {
        val inputStringBuilder = StringBuilder()
        val bufferedReader = BufferedReader(InputStreamReader(response.body, "UTF-8"))
        var line = bufferedReader.readLine()
        while (line != null) {
            inputStringBuilder.append(line)
            inputStringBuilder.append('\n')
            line = bufferedReader.readLine()
        }
        log.info("============================response begin==========================================")
        log.info("Status code  : {}", response.statusCode)
        log.info("Status text  : {}", response.statusText)
        log.info("Headers      : {}", response.headers)
        log.info("Response body: {}", inputStringBuilder.toString())
        log.info("=======================response end=================================================")
    }

    companion object {
        val log: Logger = LoggerFactory.getLogger(LoggingRequestInterceptor::class.java)
    }
}

The Properties File

You can name the property however you want. I normally just use the name of the service so that I can use this with different services independently. If you are using this in a sort of manual unit test you can simply construct your client with the bean of this property configured as you see fit.

my.rest.api.debug=yes

The REST Client

The below client is fairly standard except for the interceptor being conditionally added based on our property and authentication being added to every request header (this is only needed if the service you are integrating with needs authentication)

@Component
class MyRestClient(theRestClientsProperties: MyRestClientProperties) {

private val restTemplate: RestTemplate = buildRestTemplate(theRestClientsProperties)

private fun buildRestTemplate(theRestClientsProperties: MyRestClientProperties): RestTemplate {

    if (theRestClientsProperties.debug.toLowerCase() == "yes") {
        return RestTemplateBuilder()
                .requestFactory { BufferingClientHttpRequestFactory(HttpComponentsClientHttpRequestFactory()) }
                .rootUri(rootProperties.url)
                .basicAuthentication(theRestClientsProperties.user, theRestClientsProperties.password)
                .interceptors(RESTInterceptor())
                .build()
    }
    return RestTemplateBuilder()
            .rootUri(rootProperties.url)
            .requestFactory { HttpComponentsClientHttpRequestFactory() }
            .basicAuthentication(theRestClientsProperties.user, theRestClientsProperties.password)
            .build()
}

//...

}