Spring Boot RestTemplate Logging

Spring RestTemplate Logging: Print Requests and Responses for Better Insights

Learn how to use RestTemplate logging in Spring to print requests and responses, giving you better insights into your API calls.

1. Introduction

Logging HTTP requests and responses can be incredibly helpful for debugging and monitoring your Spring Boot applications. By enabling logging, you can track the data being sent and received, making it easier to troubleshoot issues, ensure data consistency, and gain visibility into the inner workings of your API calls.

In this guide, we’ll walk through a detailed approach to enable request and response logging with RestTemplate in Spring Boot. This includes simple setup steps, a clear example, and explanations to help you get started.

2. Why Log Requests and Responses?

RestTemplate logging gives you insight into your application’s interactions with external services. With this information, you can:

  • Debug errors in API requests or responses
  • Validate data integrity
  • Track HTTP request latency and response status
  • Improve observability in production environments


3. Setting Up RestTemplate Logging in Spring Boot

Spring Boot provides multiple ways to enable RestTemplate logging, from configuring properties to creating a custom interceptor. Here, we’ll explore both a configuration-based and a programmatic approach, with examples to make it easier for you to choose based on your needs.

3.1. Enabling Basic HTTP Logging with Spring Boot Properties

For basic HTTP logging, Spring Boot’s logging configuration properties make it easy. You can log basic request and response details without needing extra code by enabling specific logging levels for the org.springframework.web.client.RestTemplate package.

Step-by-Step Setup:

a. Add the following configuration in application.properties file to enable logging at the DEBUG level:

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

b. Restart your application. Now, you should see basic logs for requests and responses in your console.

Output:

DEBUG 6562 --- [spring-boot-rest-template-logging] [nio-8080-exec-1] o.s.web.client.RestTemplate              : HTTP POST https://jsonplaceholder.typicode.com/posts
DEBUG 6562 --- [spring-boot-rest-template-logging] [nio-8080-exec-1] o.s.web.client.RestTemplate              : Accept=[text/plain, application/json, application/*+json, */*]
DEBUG 6562 --- [spring-boot-rest-template-logging] [nio-8080-exec-1] o.s.web.client.RestTemplate              : Writing [{title=foo, body=bar, userId=1}] as "application/json"
DEBUG 6562 --- [spring-boot-rest-template-logging] [nio-8080-exec-1] o.s.web.client.RestTemplate              : Response 201 CREATED
DEBUG 6562 --- [spring-boot-rest-template-logging] [nio-8080-exec-1] o.s.web.client.RestTemplate              : Reading to [java.lang.String] as "application/json;charset=utf-8"
application.yml

The output shows that RestTemplate executed an HTTP POST request to https://jsonplaceholder.typicode.com/posts, logging the Accept header values, the JSON body being sent {title=foo, body=bar, userId=1}, and the subsequent response status of 201 CREATED, indicating that the resource was successfully created.

Limitations of Basic HTTP Logging

This setup provides limited information by primarily logging the HTTP method, URL, and some request details. While it captures the request’s content type and body, it may not include crucial information such as other request headers (like Authorization), the complete response body, or response headers. Consequently, for a more comprehensive view of API interactions, including headers and full body content, it’s recommended to implement a custom interceptor method, as explained below.



3.2. Detailed Logging with a Custom RestTemplate Interceptor

To capture request and response headers, bodies, and other details, we’ll create a custom ClientHttpRequestInterceptor. This custom interceptor logs both requests and responses, providing full control over what’s printed to the logs.

High-Level Steps:

  1. Create Logging Interceptor Class: Implement the ClientHttpRequestInterceptor interface to log requests and responses.
  2. Override the intercept Method: Log request data, execute the request, and log response data using a response wrapper.
  3. Create Response Wrapper Class: Implement ClientHttpResponse to buffer the response body for logging.
  4. Register the Interceptor: Configure a RestTemplate bean and add the logging interceptor.

Detailed Implementation Steps:

Step 1: Create the Logging Interceptor Class

Create a class named LoggingInterceptor that implements the ClientHttpRequestInterceptor interface. This class will be responsible for intercepting the HTTP requests and responses to log relevant information.

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 org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;

@Component
public class LoggingInterceptor implements ClientHttpRequestInterceptor {

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

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        logRequest(request, body);  // Log request details
        ClientHttpResponse response = execution.execute(request, body);  // Execute the request
        ClientHttpResponse bufferedResponse = new BufferingClientHttpResponseWrapper(response); // Wrap the response
        logResponse(bufferedResponse);  // Log response details
        return bufferedResponse; // Return the wrapped response
    }

    private void logRequest(HttpRequest request, byte[] body) throws IOException {
        // Log details of the HTTP request
        logger.info("Request URI: {}", request.getURI());
        logger.info("Request Method: {}", request.getMethod());
        logger.info("Request Headers: {}", request.getHeaders());
        logger.info("Request Body: {}", new String(body, StandardCharsets.UTF_8));
    }

    private void logResponse(ClientHttpResponse response) throws IOException {
        // Read and log the response body
        String responseBody = new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))
                .lines()
                .collect(Collectors.joining("\n"));
        logger.info("Response Status Code: {}", response.getStatusCode());
        logger.info("Response Headers: {}", response.getHeaders());
        logger.info("Response Body: {}", responseBody);
    }
}
LoggingInterceptor.java

Explanation:

  • Intercept Method: This method is overridden to implement the logging functionality:
    • It first logs the request details using logRequest.
    • Then it executes the HTTP request using execution.execute(request, body).
    • It wraps the response in a BufferingClientHttpResponseWrapper to allow reading the response body multiple times.
    • Finally, it logs the response details with logResponse.

Step 2: Create a Response Wrapper Class

Next, create a class named BufferingClientHttpResponseWrapper that implements ClientHttpResponse. This wrapper allows us to read the response body multiple times.

import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;

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

public class BufferingClientHttpResponseWrapper implements ClientHttpResponse {

    private final ClientHttpResponse response;  // Original response
    private byte[] responseBody;  // Buffered response body

    public BufferingClientHttpResponseWrapper(ClientHttpResponse response) throws IOException {
        this.response = response;
        this.responseBody = streamToByteArray(response.getBody());  // Buffer the response body
    }

    private byte[] streamToByteArray(InputStream inputStream) throws IOException {
        // Read the input stream into a byte array
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        inputStream.transferTo(baos);
        return baos.toByteArray(); // Return the byte array
    }

    @Override
    public InputStream getBody() {
        // Return an InputStream from the buffered response body
        return new ByteArrayInputStream(responseBody);
    }

    @Override
    public HttpStatusCode getStatusCode() throws IOException {
        return response.getStatusCode();  // Delegate to the original response
    }

    @Override
    public int getRawStatusCode() throws IOException {
        return response.getRawStatusCode();  // Delegate to the original response
    }

    @Override
    public String getStatusText() throws IOException {
        return response.getStatusText();  // Delegate to the original response
    }

    @Override
    public void close() {
        response.close();  // Close the original response
    }

    @Override
    public org.springframework.http.HttpHeaders getHeaders() {
        return response.getHeaders();  // Delegate to the original response
    }
}
BufferingClientHttpResponseWrapper.java

Explanation:

  • Response Body Buffering: The constructor reads the response body into a byte array, allowing us to access it multiple times.
  • Delegation: All other methods delegate calls to the original ClientHttpResponse, ensuring we maintain the original response behavior while adding the buffering functionality.

Why Custom Response Wrapper Class is Necessary?

When the response body is read once, it can’t be read again. This means that if we read the response body to log it, we can’t use it afterward, as the input stream will be exhausted. To overcome this, we copy the response body into a ByteArrayOutputStream with the help of BufferingClientHttpResponseWrapper class we created above, allowing us to log it and still send it to the client.



Step 3: Register the Interceptor with the RestTemplate Bean

Now, create a configuration class to register the LoggingInterceptor with your RestTemplate. This step ensures that the interceptor is applied to all requests made by the RestTemplate.

import com.bootcamptoprod.interceptor.LoggingInterceptor;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;

@Configuration
public class RestTemplateConfig {

    private final LoggingInterceptor loggingInterceptor;  // Dependency injection of the LoggingInterceptor

    public RestTemplateConfig(LoggingInterceptor loggingInterceptor) {
        this.loggingInterceptor = loggingInterceptor;  // Assigning the injected interceptor
    }

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        // Build a RestTemplate with the logging interceptor
        return builder
                .additionalInterceptors(Collections.singletonList(loggingInterceptor)) // Add the interceptor
                .build();  // Build and return the RestTemplate instance
    }
}
RestTemplateConfig.java

Explanation:

  • Configuration Class: This class is marked with @Configuration, indicating it provides Spring configuration.
  • RestTemplate Bean: The restTemplate method creates a RestTemplate instance:
    • The logging interceptor is added to the builder via additionalInterceptors.
    • Finally, the RestTemplate is built and returned.

Step 4: Create a Controller to Test the Logging

Finally, create a simple controller to test the logging functionality by making an HTTP request.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@RestController
public class RestTemplateLoggingController {

    @Autowired
    private RestTemplate restTemplate;  // Autowire the RestTemplate

    @GetMapping("/test-api")
    public ResponseEntity<String> callApi() {
        String url = "https://jsonplaceholder.typicode.com/posts";  // External API URL

        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", "application/json");  // Set content type

        // Create a request body
        Map<String, String> requestBody = new HashMap<>();
        requestBody.put("title", "foo");
        requestBody.put("body", "bar");
        requestBody.put("userId", "1");

        HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestBody, headers); // Wrap headers and body

        return restTemplate.exchange(url, HttpMethod.POST, entity, String.class);  // Execute the request
    }
}
RestTemplateLoggingController.java

Explanation:

  • Autowiring RestTemplate: The RestTemplate instance is injected using @Autowired.
  • API Call: The /test-api endpoint sends a POST request to an external API (jsonplaceholder.typicode.com) with a sample request body.
  • Response Handling: The response from the external API is returned to the caller.

Understanding Output:

When /test-api controller endpoint is called, the logs capture the request made to the jsonplaceholder API using RestTemplate, displaying the request URI, method, headers, and body. The response includes a status code of 201 CREATED, indicating successful resource creation, along with the response headers and body confirming the created resource’s details.

4. Source Code

The complete source code of the above example can be found here.



5. Things to Consider

Here are some important considerations to keep in mind while logging RestTemplate requests and responses:

  • Log Level Configuration: Choose appropriate log levels (DEBUG, INFO, WARN, ERROR) for different types of messages to ensure meaningful logs without overwhelming the log output.
  • Sensitive Data Protection: Ensure that sensitive information, such as passwords, credit card numbers, and personal identifiable information (PII), are not logged. Implement filters to mask or omit such data.
  • Performance Monitoring: Keep an eye on the performance impact of logging, especially if logging large payloads or logging every request/response in high-throughput applications.
  • Configuration Management: Consider managing logging configurations through properties files to enable or disable logging easily without code changes.
  • Error Handling: Implement proper error handling in your interceptor to avoid breaking the application flow due to logging errors.

6. FAQs

Can I log sensitive information using the interceptor?

How do I enable logging for specific HTTP methods only?

Can I configure logging for only certain API endpoints?



7. Conclusion

Implementing logging for requests and responses in a Spring RestTemplate can significantly enhance your application’s observability and debugging capabilities. You can gain insights into the data being sent and received from external APIs, making it easier to track down issues and monitor performance. However, it’s essential to consider logging best practices, such as managing sensitive information and monitoring performance impact, to ensure that your logging strategy remains effective and secure.

8. Learn More

#

Interested in learning more?

Checkout our blog on How to Use Brotli Compression in Spring Boot with Brotli4j



Add a Comment

Your email address will not be published.