Spring Boot - How to log all requests and responses with exceptions in single place?
In a Spring Boot application, you can log all requests and responses, along with exceptions, in a single place by using a combination of Spring Boot's logging framework and request/response interceptors.
Introduction:
In a Spring Boot application, you can log all requests and responses, along with exceptions, in a single place by using a combination of Spring Boot's logging framework and request/response interceptors. Here's an example of how you can achieve this:
How to do it?
- Configure logging in your Spring Boot application. You can use a logging framework like Logback or Log4j2, which are supported by Spring Boot out of the box. You can configure logging properties in your
application.propertiesorapplication.ymlfile, such as log file location, log levels, and log patterns. For example:
# application.properties
logging.file.name=/path/to/logs/my-app.log
logging.level.org.springframework=INFO
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%nNote: This example uses jakarta.servlet imports, which require Spring Boot 3.0 or later.
- Create a custom request/response interceptor that logs requests, responses, and exceptions. You can implement the
HandlerInterceptorinterface provided by Spring MVC, which allows you to intercept incoming requests and outgoing responses. You can then log the relevant information using your configured logging framework. Here's an example:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
public class LoggingInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
private static final String WRAPPED_REQUEST_ATTR = "wrappedRequest";
private static final String WRAPPED_RESPONSE_ATTR = "wrappedResponse";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Wrap request/response to capture bodies
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
// Store wrappers in request attributes to access them in afterCompletion
request.setAttribute(WRAPPED_REQUEST_ATTR, wrappedRequest);
request.setAttribute(WRAPPED_RESPONSE_ATTR, wrappedResponse);
logger.info("Received request: {} {} from {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI(), wrappedRequest.getRemoteAddr());
byte[] requestContent = wrappedRequest.getContentAsByteArray();
if (requestContent.length > 0) {
logger.info("Request body: {}", new String(requestContent));
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request.getAttribute(WRAPPED_REQUEST_ATTR);
ContentCachingResponseWrapper wrappedResponse = (ContentCachingResponseWrapper) request.getAttribute(WRAPPED_RESPONSE_ATTR);
int status = 0;
try {
status = response.getStatus();
} catch (IllegalStateException e) {
status = 500;
}
logger.info("Sent response: {} {} with status {}",
wrappedRequest != null ? wrappedRequest.getMethod() : "UNKNOWN",
wrappedRequest != null ? wrappedRequest.getRequestURI() : "UNKNOWN", status);
if (wrappedResponse != null) {
byte[] responseContent = wrappedResponse.getContentAsByteArray();
if (responseContent.length > 0) {
logger.info("Response body: {}", new String(responseContent));
}
wrappedResponse.copyBodyToResponse(); // Crucial: copy body back to the actual response
}
if (ex != null) {
logger.error("Exception occurred: {}", ex.getMessage(), ex);
}
}
}- Register the custom interceptor in your Spring Boot application. You can do this by adding a
@Beanmethod in one of your configuration classes or by using theWebMvcConfigurerinterface to configure the interceptors. Here's an example:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor());
}
}With these steps, the LoggingInterceptor will intercept all incoming requests and outgoing responses in your Spring Boot application. You can then configure your logging framework to log the intercepted information according to your desired log format and level. Additionally, you can modify the LoggingInterceptor to suit your specific logging requirements, such as logging only specific types of requests or responses, or customizing the log messages. Remember to also handle exceptions appropriately in your interceptor to avoid disrupting the normal flow of request processing.
⚠️ Performance Warning: Logging full request and response bodies can significantly impact application performance and memory usage, especially for large payloads or high-traffic endpoints. Consider limiting body logging to debug environments, adding size thresholds, or using sampling strategies in production.