Spring Boot - handle exception wrapped with BindException - java

I am looking for a way to handle custom exception thrown during binding of request parameter to DTO field.
I have a cantroller in Spring Boot application as follows
#GetMapping("/some/url")
public OutputDTO filterEntities(InputDTO inputDTO) {
return service.getOutput(inputDTO);
}
input DTO has few fields, one of which is of enum type
public class InputDTO {
private EnumClass enumField;
private String otherField;
/**
* more fields
*/
}
user will hit the URL in ths way
localhost:8081/some/url?enumField=wrongValue&otherField=anyValue
Now if user sends wrong value for enumField, I would like to throw my CustomException with particular message. Process of enum instance creation and throwing of exception is implemented in binder
#InitBinder
public void initEnumClassBinder(final WebDataBinder webdataBinder) {
webdataBinder.registerCustomEditor(
EnumClass.class,
new PropertyEditorSupport() {
#Override
public void setAsText(final String text) throws IllegalArgumentException {
try {
setValue(EnumClass.valueOf(text.toUpperCase()));
} catch (Exception exception) {
throw new CustomException("Exception while deserializing EnumClass from " + text, exception);
}
}
}
);
}
Problem is that when exception is thrown it is impossible to handle it with
#ExceptionHandler(CustomException.class)
public String handleException(CustomException exception) {
// log exception
return exception.getMessage();
}
Spring wraps initial exception with BindException. That instance contains my initial error message, but concatenated with other text which is redundant for me. I don't think that parsing and substringing that message is good...
Am I missing something? What is the proper way to get message from initial
CustomException here?

You will not be able to handle exceptions thrown before entering your controller method by using #ExceptionHandler annotated methods. Spring handles these exceptions before entering the controller, by registering DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver handler.
This is the case of BindingException, thrown when Spring cannot bind request parameters to match your InputDTO object.
What you can do is to register your own handler (create a Component implementing HandlerExceptionResolver and Ordered interfaces), give it the highest priority in handling errors and play with exceptions as needed.
You have also to pay attention to BindException as it wrappes your custom exception, CustomException.class
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import yourpackage.CustomException;
#Component()
public class BindingExceptionResolver implements HandlerExceptionResolver, Ordered {
private static final Logger logger = LoggerFactory.getLogger(BindingExceptionResolver.class);
public BindingExceptionResolver() {
}
private ModelAndView handleException(ObjectError objectError, HttpServletResponse response){
if (objectError == null) return null;
try {
if(objectError.contains(CustomException.class)) {
CustomException ex = objectError.unwrap(CustomException.class);
logger.error(ex.getMessage(), ex);
return handleCustomException(ex, response);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return null;
}
protected ModelAndView handleCustomException(CustomException ex, HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof org.springframework.validation.BindException) {
BindException be = (BindException) ex;
logger.debug("Binding exception in {} :: ({}) :: ({})=({})", be.getObjectName(), be.getBindingResult().getTarget().getClass(), be.getFieldError().getField(), be.getFieldError().getRejectedValue());
return be.getAllErrors().stream()
.filter(o->o.contains(Exception.class))
.map(o ->handleException(o, response))
.filter(mv ->mv !=null)
.findFirst().orElse(null);
}
} catch (Exception handlerException) {
logger.error("Could not handle exception", handlerException);
}
return null;
}
#Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
Hope it helps

Related

How do I handle a Callable in my AOP Around advice?

I have a controller in my Spring Boot application. In my controller, I have an endpoint where I need to timeout the call if too much time elapses. I do this by returning a Callable from this method and including the config spring.mvc.async.request-timeout in my application.yml. This seems to work well for our purposes.
I also have an Aspect class in this application that contains a method that is triggered whenever a method in my controller is called. The point of this method is to log details such as the amount of time taken for an endpoint, what the response code was, and etc. This works well when the response of the method is not a Callable (ie. a ResponseEntity) since I can get response information from the return type without issue. However, I cannot get this response information when the method returns a Callable without invoking ((Callable) ProceedingJoinPoint.proceed()).call() from the aspect class. This makes API calls longer, and I believe that's because it invokes call() twice. Is there any way that I can get the response information without having to use call() in the Aspect class?
Here is a simple example of what I have so far in my aspect class:
#Around("...")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
if (!(result instanceof Callable<?>)) {
// Do logging using result, which is a ResponseEntity...
} else {
Object callableResult = ((Callable<?>) result).call();
// Do logging using callableResult, which is a ResponseEntity...
}
return result;
}
Thank you.
I encountered the same situation at work a while ago and the following seems to solve it: Log response body after asynchronous Spring MVC controller method
Note: Try logging the async response in a class annotated with #ControllerAdvice and implements ResponseBodyAdvice instead. This should capture the real response instead of callable.
You could have a class annotated with #Aspects for logging request and #ControllerAdvice for logging response together.
e.g.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicLong;
#Aspect
#ControllerAdvice
public class LoggingAdvice implements ResponseBodyAdvice {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingAdvice.class);
private static final AtomicLong ID = new AtomicLong();
#Before("within(com.example.demo.controller..*)")
public void endpointBefore(JoinPoint p) {
LOGGER.info(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
if (signatureArgs != null && signatureArgs.length > 0) {
LOGGER.info("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
} else {
LOGGER.info("request object is empty");
}
} catch (JsonProcessingException e) {
}
}
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
long id = ID.incrementAndGet();
ServletServerHttpResponse responseToUse = (ServletServerHttpResponse) response;
HttpMessageConverter httpMessageConverter;
LoggingHttpOutboundMessageWrapper httpOutputMessage = new LoggingHttpOutboundMessageWrapper();
try {
httpMessageConverter = (HttpMessageConverter) selectedConverterType.newInstance();
httpMessageConverter.write(body, selectedContentType, httpOutputMessage);
LOGGER.info("response {}, {}, {}, {}, {}", id, responseToUse.getServletResponse().getStatus(), responseToUse.getServletResponse().getContentType(),
responseToUse.getHeaders(), httpOutputMessage.getResponseBodyInString());
} catch (InstantiationException | IllegalAccessException | IOException e) {
e.printStackTrace();
}
return body;
}
private static final class LoggingHttpOutboundMessageWrapper implements HttpOutputMessage {
private HttpHeaders httpHeaders = new HttpHeaders();
private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
#Override
public OutputStream getBody() throws IOException {
return byteArrayOutputStream;
}
#Override
public HttpHeaders getHeaders() {
return httpHeaders;
}
public String getResponseBodyInString() {
return new String(byteArrayOutputStream.toByteArray());
}
}
}

How can we postHandle an exception in HandlerInterceptorAdapter?

I am currently trying to implement a customized error handler for spring boot and I have done it with the following:
public class ExceptionHandler extends HandlerInterceptorAdapter {
public static Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, #Nullable ModelAndView modelAndView) throws Exception {
try {
log.info("Service {} Calling {} on {} finished with status {}",request.getRemoteUser(), request.getMethod(), request.getRequestURI(), HttpStatus.valueOf(response.getStatus()));
} catch (Exception e) {
// Do nothing
} finally {
log.error("[Spring Boot Interceptor] {} returned with {}", handler, HttpStatus.valueOf(response.getStatus()));
}
}
Somehow this does not work, and the exception is still thrown to the client, is there some way to catch the exception thrown by the method and ignore it for example.
A good way to manage the exception is using #ControllerAdvice, using this you may handle any kind of exception and customize the response as required.
As said in the comment, you have to add InterceptorRegistry to register the interceptor.
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 WebConfig implements WebMvcConfigurer {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Interceptor()).addPathPatterns("/**");
}
}
The catch block inside postHandle will only be executed if an exception occurred inside the try-catch block as below,
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, #Nullable ModelAndView modelAndView) throws Exception {
try {
int error = 1/0;
} catch (Exception e) {
log.info("Exception will be handled inside catch block");
}
}
Now let's explore the #ControllerAdvice to manage the exception within the application. These two APIs will generate the exceptions and we will manage the exceptions using #ExceptionHandler
#GetMapping("/exception/404")
public void generateResourceNotFound() {
throw new ResourceNotFoundException("resource not found");
}
#GetMapping("/exception/403")
public void generateAccessDenied() {
throw new AccessDeniedException("access denied");
}
GlobalExceptionHandler.java
import com.learning.annotations.controller.ResourceNotFoundException;
import com.learning.annotations.dto.ErrorResponseDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
public Logger log = LoggerFactory.getLogger(Interceptor.class);
#ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponseDTO> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) {
ErrorResponseDTO response = new ErrorResponseDTO();
response.setError(ex.getMessage());
response.setMessage("You don't have authority to access the resource");
return new ResponseEntity<>(response, HttpStatus.FORBIDDEN);
}
#ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponseDTO> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorResponseDTO response = new ErrorResponseDTO();
response.setError(ex.getMessage());
response.setMessage("Resource might be moved temporary or not available");
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
}
To customize the response we can create error response DTO as follows,
import lombok.Data;
#Data
public class ErrorResponseDTO {
private String message;
private String error;
}

Exception handling in StreamingResponseBody

I'm trying to catch an exception thrown in my implementation of StreamingResponseBody, I can see the exception being thrown inside the class however the thrown exception isn't visible to the method body or my Controller Advice. So none of my handling seems to work, just interested to know which is the correct way to handle exceptions in this case.
#GetMapping(path = "/test", produces = "application/json")
public StreamingResponseBody test(#RequestParam(value = "var1") final String test)
throws IOException{
return new StreamingResponseBody() {
#Override
public void writeTo(final OutputStream outputStream) throws IOException{
try {
// Some operations..
} catch (final SomeCustomException e) {
throw new IOException(e);
}
}
};
}
I would expect my ControllerAdvice to return an ResponseEntity with a Http Status of 500.
The best way I discovered to handle errors/exceptions in the web environment is to create your custom exception with the disabled stack trace, and handle it with #ControllerAdvice.
import lombok.Getter;
import org.springframework.http.HttpStatus;
public class MyException extends RuntimeException {
#Getter private HttpStatus httpStatus;
public MyException(String message) {
this(message, HttpStatus.INTERNAL_SERVER_ERROR);
}
public MyException(String message, HttpStatus status) {
super(message, null, false, false);
this.httpStatus = status;
}
}
And then handle it in #ControllerAdvice like this:
#ExceptionHandler(MyException.class)
public ResponseEntity handleMyException(MyException exception) {
return ResponseEntity.status(exception.getHttpStatus()).body(
ErrorDTO.builder()
.message(exception.getMessage())
.description(exception.getHttpStatus().getReasonPhrase())
.build());
}
where ErrorDTO is just a simple DTO with two fields:
#Value
#Builder
public class ErrorDTO {
private final String message;
private final String description;
}

How to manage exceptions thrown in filters in Spring?

I want to use generic way to manage 5xx error codes, let's say specifically the case when the db is down across my whole spring application. I want a pretty error json instead of a stack trace.
For the controllers I have a #ControllerAdvice class for the different exceptions and this is also catching the case that the db is stopping in the middle of the request. But this is not all. I also happen to have a custom CorsFilter extending OncePerRequestFilter and there when i call doFilter i get the CannotGetJdbcConnectionException and it will not be managed by the #ControllerAdvice. I read several things online that only made me more confused.
So I have a lot of questions:
Do i need to implement a custom filter? I found the ExceptionTranslationFilter but this only handles AuthenticationException or AccessDeniedException.
I thought of implementing my own HandlerExceptionResolver, but this made me doubt, I don't have any custom exception to manage, there must be a more obvious way than this. I also tried to add a try/catch and call an implementation of the HandlerExceptionResolver (should be good enough, my exception is nothing special) but this is not returning anything in the response, i get a status 200 and an empty body.
Is there any good way to deal with this? Thanks
So this is what I did:
I read the basics about filters here and I figured out that I need to create a custom filter that will be first in the filter chain and will have a try catch to catch all runtime exceptions that might occur there. Then i need to create the json manually and put it in the response.
So here is my custom filter:
public class ExceptionHandlerFilter extends OncePerRequestFilter {
#Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (RuntimeException e) {
// custom error response class used across my project
ErrorResponse errorResponse = new ErrorResponse(e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write(convertObjectToJson(errorResponse));
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}
And then i added it in the web.xml before the CorsFilter. And it works!
<filter>
<filter-name>exceptionHandlerFilter</filter-name>
<filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>exceptionHandlerFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
I wanted to provide a solution based on the answer of #kopelitsa. The main differences being:
Reusing the controller exception handling by using the HandlerExceptionResolver.
Using Java config over XML config
First, you need to make sure, that you have a class that handles exceptions occurring in a regular RestController/Controller (a class annotated with #RestControllerAdvice or #ControllerAdvice and method(s) annotated with #ExceptionHandler). This handles your exceptions occurring in a controller. Here is an example using the RestControllerAdvice:
#RestControllerAdvice
public class ExceptionTranslator {
#ExceptionHandler(RuntimeException.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorDTO processRuntimeException(RuntimeException e) {
return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
}
private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
(...)
}
}
To reuse this behavior in the Spring Security filter chain, you need to define a Filter and hook it into your security configuration. The filter needs to redirect the exception to the above defined exception handling. Here is an example:
#Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {
private final Logger log = LoggerFactory.getLogger(getClass());
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Spring Security Filter Chain Exception:", e);
resolver.resolveException(request, response, null, e);
}
}
}
The created filter then needs to be added to the SecurityConfiguration. You need to hook it into the chain very early, because all preceding filter's exceptions won't be caught. In my case, it was reasonable to add it before the LogoutFilter. See the default filter chain and its order in the official docs. Here is an example:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private FilterChainExceptionHandler filterChainExceptionHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
(...)
}
}
I come across this issue myself and I performed the steps below to reuse my ExceptionController that is annotated with #ControllerAdvise for Exceptions thrown in a registered Filter.
There are obviously many ways to handle exception but, in my case, I wanted the exception to be handled by my ExceptionController because I am stubborn and also because I don't want to copy/paste the same code (i.e. I have some processing/logging code in ExceptionController). I would like to return the beautiful JSON response just like the rest of the exceptions thrown not from a Filter.
{
"status": 400,
"message": "some exception thrown when executing the request"
}
Anyway, I managed to make use of my ExceptionHandler and I had to do a little bit of extra as shown below in steps:
Steps
You have a custom filter that may or may not throw an exception
You have a Spring controller that handles exceptions using #ControllerAdvise i.e. MyExceptionController
Sample code
//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
//Spring Controller annotated with #ControllerAdvise which has handlers
//for exceptions
private MyExceptionController myExceptionController;
#Override
public void destroy() {
// TODO Auto-generated method stub
}
#Override
public void init(FilterConfig arg0) throws ServletException {
//Manually get an instance of MyExceptionController
ApplicationContext ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(arg0.getServletContext());
//MyExceptionHanlder is now accessible because I loaded it manually
this.myExceptionController = ctx.getBean(MyExceptionController.class);
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
try {
//code that throws exception
} catch(Exception ex) {
//MyObject is whatever the output of the below method
MyObject errorDTO = myExceptionController.handleMyException(req, ex);
//set the response object
res.setStatus(errorDTO .getStatus());
res.setContentType("application/json");
//pass down the actual obj that exception handler normally send
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = res.getWriter();
out.print(mapper.writeValueAsString(errorDTO ));
out.flush();
return;
}
//proceed normally otherwise
chain.doFilter(request, response);
}
}
And now the sample Spring Controller that handles Exception in normal cases (i.e. exceptions that are not usually thrown in Filter level, the one we want to use for exceptions thrown in a Filter)
//sample SpringController
#ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {
//sample handler
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ExceptionHandler(SQLException.class)
public #ResponseBody MyObject handleSQLException(HttpServletRequest request,
Exception ex){
ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
+ "executing the request.");
return response;
}
//other handlers
}
Sharing the solution with those who wish to use ExceptionController for Exceptions thrown in a Filter.
So, here's what I did based on an amalgamation of the above answers... We already had a GlobalExceptionHandler annotated with #ControllerAdvice and I also wanted to find a way to re-use that code to handle exceptions that come from filters.
The simplest solution I could find was to leave the exception handler alone, and implement an error controller as follows:
#Controller
public class ErrorControllerImpl implements ErrorController {
#RequestMapping("/error")
public void handleError(HttpServletRequest request) throws Throwable {
if (request.getAttribute("javax.servlet.error.exception") != null) {
throw (Throwable) request.getAttribute("javax.servlet.error.exception");
}
}
}
So, any errors caused by exceptions first pass through the ErrorController and are re-directed off to the exception handler by rethrowing them from within a #Controller context, whereas any other errors (not caused directly by an exception) pass through the ErrorController without modification.
Any reasons why this is actually a bad idea?
If you want a generic way, you can define an error page in web.xml:
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/500</location>
</error-page>
And add mapping in Spring MVC:
#Controller
public class ErrorController {
#RequestMapping(value="/500")
public #ResponseBody String handleException(HttpServletRequest req) {
// you can get the exception thrown
Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");
// customize response to what you want
return "Internal server error.";
}
}
This is my solution by overriding default Spring Boot /error handler
package com.mypackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* This controller is vital in order to handle exceptions thrown in Filters.
*/
#RestController
#RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {
private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);
private final ErrorAttributes errorAttributes;
#Autowired
public ErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
#Override
public String getErrorPath() {
return "/error";
}
#RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
Map<String, Object> result = this.errorAttributes.getErrorAttributes(requestAttributes, false);
Throwable error = this.errorAttributes.getError(requestAttributes);
ResponseStatus annotation = AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;
result.put("status", statusCode.value());
result.put("error", statusCode.getReasonPhrase());
LOGGER.error(result.toString());
return new ResponseEntity<>(result, statusCode) ;
}
}
Just to complement the other fine answers provided, as I too recently wanted a single error/exception handling component in a simple SpringBoot app containing filters that may throw exceptions, with other exceptions potentially thrown from controller methods.
Fortunately, it seems there is nothing to prevent you from combining your controller advice with an override of Spring's default error handler to provide consistent response payloads, allow you to share logic, inspect exceptions from filters, trap specific service-thrown exceptions, etc.
E.g.
#ControllerAdvice
#RestController
public class GlobalErrorHandler implements ErrorController {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(ValidationException.class)
public Error handleValidationException(
final ValidationException validationException) {
return new Error("400", "Incorrect params"); // whatever
}
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(Exception.class)
public Error handleUnknownException(final Exception exception) {
return new Error("500", "Unexpected error processing request");
}
#RequestMapping("/error")
public ResponseEntity handleError(final HttpServletRequest request,
final HttpServletResponse response) {
Object exception = request.getAttribute("javax.servlet.error.exception");
// TODO: Logic to inspect exception thrown from Filters...
return ResponseEntity.badRequest().body(new Error(/* whatever */));
}
#Override
public String getErrorPath() {
return "/error";
}
}
When you want to test a state of application and in case of a problem return HTTP error I would suggest a filter. The filter below handles all HTTP requests. The shortest solution in Spring Boot with a javax filter.
In the implementation can be various conditions. In my case the applicationManager testing if the application is ready.
import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#Component
public class SystemIsReadyFilter implements Filter {
#Autowired
private ApplicationManager applicationManager;
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!applicationManager.isApplicationReady()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
} else {
chain.doFilter(request, response);
}
}
#Override
public void destroy() {}
}
After reading through different methods suggested in the above answers, I decided to handle the authentication exceptions by using a custom filter. I was able to handle the response status and codes using an error response class using the following method.
I created a custom filter and modified my security config by using the addFilterAfter method and added after the CorsFilter class.
#Component
public class AuthFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//Cast the servlet request and response to HttpServletRequest and HttpServletResponse
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// Grab the exception from the request attribute
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
//Set response content type to application/json
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
//check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(convertObjectToJson(errorResponse));
writer.flush();
return;
}
// If exception instance cannot be determined, then throw a nice exception and desired response code.
else if(exception!=null){
ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(convertObjectToJson(errorResponse));
writer.flush();
return;
}
else {
// proceed with the initial request if no exception is thrown.
chain.doFilter(httpServletRequest,httpServletResponse);
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}
SecurityConfig class
#Configuration
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
AuthFilter authenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
.cors(); //........
return http;
}
}
ErrorResponse class
public class ErrorResponse {
private final String message;
private final String description;
public ErrorResponse(String description, String message) {
this.message = message;
this.description = description;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}}
You can use the following method inside the catch block:
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid token")
Notice that you can use any HttpStatus code and a custom message.
I had the same issue in webflux, going on the theme that someone is looking to resuse there #ControllerAdvice, you do not want to throw a direct exception or return a mono error in the webfilter, however you want to set the response to be the mono error.
public class YourFilter implements WebFilter {
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
exchange.getResponse().writeWith(Mono.error(new YouException()));
return chain.filter(exchange)
}
}
In Filters, we don't have a control with #ControllerAdvice or #RestControllerAdvice to handle our exceptions that could occur at the time of doing the authentication. Because, DispatcherServlet will only come into picture after the Controller class hits.
So, we need to do the following.
we need to have
HttpServletResponse httpResponse = (HttpServletResponse) response;
"response" object we can pass it from public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) of GenericFilterBean.java implementation class.
2) We can use the below utility class to write or print our error JSON model or String object into the ServletResponse output stream.
public static void handleUnAuthorizedError(ServletResponse response,Exception e)
{
ErrorModel error = null;
if(e!=null)
error = new ErrorModel(ErrorCodes.ACCOUNT_UNAUTHORIZED, e.getMessage());
else
error = new ErrorModel(ErrorCodes.ACCOUNT_UNAUTHORIZED, ApplicationConstants.UNAUTHORIZED);
JsonUtils jsonUtils = new JsonUtils();
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
httpResponse.getOutputStream().println(jsonUtils.convertToJSON(error));
} catch (IOException ex) {
ex.printStackTrace();
}
}
public String convertToJSON(Object inputObj) {
ObjectMapper objectMapper = new ObjectMapper();
String orderJson = null;
try {
orderJson = objectMapper.writeValueAsString(inputObj);
}
catch(Exception e){
e.printStackTrace();
}
return orderJson;
}
Late to the party but we can also use it like this:
#ApiIgnore
#RestControllerAdvice
public class ExceptionHandlerController {
#Autowired
private MessageSource messageSource;
And in the filter:
#Component
public class MyFilter extends OncePerRequestFilter {
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver exceptionResolver;
#Override
protected void doFilterInternal(HttpServletRequest request, #NotNull HttpServletResponse response, #NotNull FilterChain filterChain) {
try {
// Some exception
} catch (Exception e) {
this.exceptionResolver.resolveException(request, response, null, e);
}
}
Global Default Exception Handlers will work only at Controller or Service level. They will not work at filter level. I found below solution working fine with Spring Boot Security - JWT filter
https://www.jvt.me/posts/2022/01/17/spring-servlet-filter-error-handling/
Below is the snippet I added
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.getWriter().write("{\"error\":\"invalid_token\",\"error_description\":\"Invalid Token\"}");
You do not need to create a custom Filter for this. We solved this by creating custom exceptions that extend ServletException (which is thrown from the doFilter method, shown in the declaration). These are then caught and handled by our global error handler.
edit: grammar
It's strange because #ControllerAdvice should works, are you catching the correct Exception?
#ControllerAdvice
public class GlobalDefaultExceptionHandler {
#ResponseBody
#ExceptionHandler(value = DataAccessException.class)
public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}
}
Also try to catch this exception in CorsFilter and send 500 error, something like this
#ExceptionHandler(DataAccessException.class)
#ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}

Spring Boot - How to log all requests and responses with exceptions in single place?

I'm working on REST API with spring boot. I need to log all requests with input params (with methods, eg. GET, POST, etc.), request path, query string, corresponding class method of this request, also response of this action, both success and errors. For example:
Successful request:
http://example.com/api/users/1
Log should look something like this:
{
HttpStatus: 200,
path: "api/users/1",
method: "GET",
clientIp: "0.0.0.0",
accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
method: "UsersController.getUser",
arguments: {
id: 1
},
response: {
user: {
id: 1,
username: "user123",
email: "user123#example.com"
}
},
exceptions: []
}
Or request with error:
http://example.com/api/users/9999
Log should be something like this:
{
HttpStatus: 404,
errorCode: 101,
path: "api/users/9999",
method: "GET",
clientIp: "0.0.0.0",
accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
method: "UsersController.getUser",
arguments: {
id: 9999
},
returns: {
},
exceptions: [
{
exception: "UserNotFoundException",
message: "User with id 9999 not found",
exceptionId: "adhaskldjaso98d7324kjh989",
stacktrace: ...................
]
}
I want Request/Response to be a single entity, with custom information related to this entity, both in successful and error cases.
What is best practice in spring to achieve this, may be with filters? if yes, can you provide concrete example?
I've played with #ControllerAdvice and #ExceptionHandler, but as I mentioned, I need to handle all success and error requests in single place (and single log).
Don't write any Interceptors, Filters, Components, Aspects, etc., this is a very common problem and has been solved many times over.
Spring Boot has a modules called Actuator, which provides HTTP request logging out of the box. There's an endpoint mapped to /trace (SB1.x) or /actuator/httptrace (SB2.0+) which will show you last 100 HTTP requests. You can customize it to log each request, or write to a DB.
To get the endpoints you want, you'll need the spring-boot-starter-actuator dependency, and also to "whitelist" the endpoints you're looking for, and possibly setup or disable security for it.
Also, where will this application run? Will you be using a PaaS? Hosting providers, Heroku for example, provide request logging as part of their service and you don't need to do any coding whatsoever then.
Spring already provides a filter that does this job. Add following bean to your config
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true);
loggingFilter.setIncludeQueryString(true);
loggingFilter.setIncludePayload(true);
loggingFilter.setMaxPayloadLength(64000);
return loggingFilter;
}
Don't forget to change log level of org.springframework.web.filter.CommonsRequestLoggingFilter to DEBUG.
You could use javax.servlet.Filter if there wasn't a requirement to log java method that been executed.
But with this requirement you have to access information stored in handlerMapping of DispatcherServlet. That said, you can override DispatcherServlet to accomplish logging of request/response pair.
Below is an example of idea that can be further enhanced and adopted to your needs.
public class LoggableDispatcherServlet extends DispatcherServlet {
private final Log logger = LogFactory.getLog(getClass());
#Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
HandlerExecutionChain handler = getHandler(request);
try {
super.doDispatch(request, response);
} finally {
log(request, response, handler);
updateResponse(response);
}
}
private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
LogMessage log = new LogMessage();
log.setHttpStatus(responseToCache.getStatus());
log.setHttpMethod(requestToCache.getMethod());
log.setPath(requestToCache.getRequestURI());
log.setClientIp(requestToCache.getRemoteAddr());
log.setJavaMethod(handler.toString());
log.setResponse(getResponsePayload(responseToCache));
logger.info(log);
}
private String getResponsePayload(HttpServletResponse response) {
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, 5120);
try {
return new String(buf, 0, length, wrapper.getCharacterEncoding());
}
catch (UnsupportedEncodingException ex) {
// NOOP
}
}
}
return "[unknown]";
}
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
responseWrapper.copyBodyToResponse();
}
}
HandlerExecutionChain - contains the information about request handler.
You then can register this dispatcher as following:
#Bean
public ServletRegistrationBean dispatcherRegistration() {
return new ServletRegistrationBean(dispatcherServlet());
}
#Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
return new LoggableDispatcherServlet();
}
And here's the sample of logs:
http http://localhost:8090/settings/test
i.g.m.s.s.LoggableDispatcherServlet : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}
http http://localhost:8090/settings/params
i.g.m.s.s.LoggableDispatcherServlet : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}
http http://localhost:8090/123
i.g.m.s.s.LoggableDispatcherServlet : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}
UPDATE
In case of errors Spring does automatic error handling. Therefore, BasicErrorController#error is shown as request handler. If you want to preserve original request handler, then you can override this behavior at spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971 before #processDispatchResult is called, to cache original handler.
The Logbook library is specifically made for logging HTTP requests and responses. It supports Spring Boot using a special starter library.
To enable logging in Spring Boot all you need to do is adding the library to your project's dependencies. For example assuming you are using Maven:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
By default the logging output looks like this:
{
"origin" : "local",
"correlation" : "52e19498-890c-4f75-a06c-06ddcf20836e",
"status" : 200,
"headers" : {
"X-Application-Context" : [
"application:8088"
],
"Content-Type" : [
"application/json;charset=UTF-8"
],
"Transfer-Encoding" : [
"chunked"
],
"Date" : [
"Sun, 24 Dec 2017 13:10:45 GMT"
]
},
"body" : {
"thekey" : "some_example"
},
"duration" : 105,
"protocol" : "HTTP/1.1",
"type" : "response"
}
It does however not output the class name that is handling the request. The library does have some interfaces for writing custom loggers.
Notes
In the meantime the library has significantly evolved, current version is 2.4.1, see https://github.com/zalando/logbook/releases. E.g. the default ouput format has changed, and can be configured, filtered, etc.
Do NOT forget to set the log level to TRACE, else you won't see anything:
logging:
level:
org.zalando.logbook: TRACE
I had defined logging level in application.properties to print requests/responses, method url in the log file
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=INFO
logging.file=D:/log/myapp.log
I had used Spring Boot.
Here is how I do it in spring data rest
by using
org.springframework.web.util.ContentCachingRequestWrapper and
org.springframework.web.util.ContentCachingResponseWrapper
/**
* Doogies very cool HTTP request logging
*
* There is also {#link org.springframework.web.filter.CommonsRequestLoggingFilter} but it cannot log request method
* And it cannot easily be extended.
*
* https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
* http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
*/
public class DoogiesRequestLogger extends OncePerRequestFilter {
private boolean includeResponsePayload = true;
private int maxPayloadLength = 1000;
private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
if (buf == null || buf.length == 0) return "";
int length = Math.min(buf.length, this.maxPayloadLength);
try {
return new String(buf, 0, length, charsetName);
} catch (UnsupportedEncodingException ex) {
return "Unsupported Encoding";
}
}
/**
* Log each request and respponse with full Request URI, content payload and duration of the request in ms.
* #param request the request
* #param response the response
* #param filterChain chain of filters
* #throws ServletException
* #throws IOException
*/
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
StringBuffer reqInfo = new StringBuffer()
.append("[")
.append(startTime % 10000) // request ID
.append("] ")
.append(request.getMethod())
.append(" ")
.append(request.getRequestURL());
String queryString = request.getQueryString();
if (queryString != null) {
reqInfo.append("?").append(queryString);
}
if (request.getAuthType() != null) {
reqInfo.append(", authType=")
.append(request.getAuthType());
}
if (request.getUserPrincipal() != null) {
reqInfo.append(", principalName=")
.append(request.getUserPrincipal().getName());
}
this.logger.debug("=> " + reqInfo);
// ========= Log request and response payload ("body") ========
// We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
// String reqBody = DoogiesUtil._stream2String(request.getInputStream()); // THIS WOULD NOT WORK!
// So we need to apply some stronger magic here :-)
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse); // ======== This performs the actual request!
long duration = System.currentTimeMillis() - startTime;
// I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
if (requestBody.length() > 0) {
this.logger.debug(" Request body:\n" +requestBody);
}
this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
if (includeResponsePayload) {
byte[] buf = wrappedResponse.getContentAsByteArray();
this.logger.debug(" Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
}
wrappedResponse.copyBodyToResponse(); // IMPORTANT: copy content of response back into original response
}
}
This code works for me in a Spring Boot application - just register it as a filter
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.output.TeeOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
#Component
public class HttpLoggingFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(HttpLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Map<String, String> requestMap = this
.getTypesafeRequestMap(httpServletRequest);
BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
httpServletRequest);
BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
httpServletResponse);
final StringBuilder logMessage = new StringBuilder(
"REST Request - ").append("[HTTP METHOD:")
.append(httpServletRequest.getMethod())
.append("] [PATH INFO:")
.append(httpServletRequest.getServletPath())
.append("] [REQUEST PARAMETERS:").append(requestMap)
.append("] [REQUEST BODY:")
.append(bufferedRequest.getRequestBody())
.append("] [REMOTE ADDRESS:")
.append(httpServletRequest.getRemoteAddr()).append("]");
chain.doFilter(bufferedRequest, bufferedResponse);
logMessage.append(" [RESPONSE:")
.append(bufferedResponse.getContent()).append("]");
log.debug(logMessage.toString());
} catch (Throwable a) {
log.error(a.getMessage());
}
}
private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
Map<String, String> typesafeRequestMap = new HashMap<String, String>();
Enumeration<?> requestParamNames = request.getParameterNames();
while (requestParamNames.hasMoreElements()) {
String requestParamName = (String) requestParamNames.nextElement();
String requestParamValue;
if (requestParamName.equalsIgnoreCase("password")) {
requestParamValue = "********";
} else {
requestParamValue = request.getParameter(requestParamName);
}
typesafeRequestMap.put(requestParamName, requestParamValue);
}
return typesafeRequestMap;
}
#Override
public void destroy() {
}
private static final class BufferedRequestWrapper extends
HttpServletRequestWrapper {
private ByteArrayInputStream bais = null;
private ByteArrayOutputStream baos = null;
private BufferedServletInputStream bsis = null;
private byte[] buffer = null;
public BufferedRequestWrapper(HttpServletRequest req)
throws IOException {
super(req);
// Read InputStream and store its content in a buffer.
InputStream is = req.getInputStream();
this.baos = new ByteArrayOutputStream();
byte buf[] = new byte[1024];
int read;
while ((read = is.read(buf)) > 0) {
this.baos.write(buf, 0, read);
}
this.buffer = this.baos.toByteArray();
}
#Override
public ServletInputStream getInputStream() {
this.bais = new ByteArrayInputStream(this.buffer);
this.bsis = new BufferedServletInputStream(this.bais);
return this.bsis;
}
String getRequestBody() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
this.getInputStream()));
String line = null;
StringBuilder inputBuffer = new StringBuilder();
do {
line = reader.readLine();
if (null != line) {
inputBuffer.append(line.trim());
}
} while (line != null);
reader.close();
return inputBuffer.toString().trim();
}
}
private static final class BufferedServletInputStream extends
ServletInputStream {
private ByteArrayInputStream bais;
public BufferedServletInputStream(ByteArrayInputStream bais) {
this.bais = bais;
}
#Override
public int available() {
return this.bais.available();
}
#Override
public int read() {
return this.bais.read();
}
#Override
public int read(byte[] buf, int off, int len) {
return this.bais.read(buf, off, len);
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener readListener) {
}
}
public class TeeServletOutputStream extends ServletOutputStream {
private final TeeOutputStream targetStream;
public TeeServletOutputStream(OutputStream one, OutputStream two) {
targetStream = new TeeOutputStream(one, two);
}
#Override
public void write(int arg0) throws IOException {
this.targetStream.write(arg0);
}
public void flush() throws IOException {
super.flush();
this.targetStream.flush();
}
public void close() throws IOException {
super.close();
this.targetStream.close();
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setWriteListener(WriteListener writeListener) {
}
}
public class BufferedResponseWrapper implements HttpServletResponse {
HttpServletResponse original;
TeeServletOutputStream tee;
ByteArrayOutputStream bos;
public BufferedResponseWrapper(HttpServletResponse response) {
original = response;
}
public String getContent() {
return bos.toString();
}
public PrintWriter getWriter() throws IOException {
return original.getWriter();
}
public ServletOutputStream getOutputStream() throws IOException {
if (tee == null) {
bos = new ByteArrayOutputStream();
tee = new TeeServletOutputStream(original.getOutputStream(),
bos);
}
return tee;
}
#Override
public String getCharacterEncoding() {
return original.getCharacterEncoding();
}
#Override
public String getContentType() {
return original.getContentType();
}
#Override
public void setCharacterEncoding(String charset) {
original.setCharacterEncoding(charset);
}
#Override
public void setContentLength(int len) {
original.setContentLength(len);
}
#Override
public void setContentLengthLong(long l) {
original.setContentLengthLong(l);
}
#Override
public void setContentType(String type) {
original.setContentType(type);
}
#Override
public void setBufferSize(int size) {
original.setBufferSize(size);
}
#Override
public int getBufferSize() {
return original.getBufferSize();
}
#Override
public void flushBuffer() throws IOException {
tee.flush();
}
#Override
public void resetBuffer() {
original.resetBuffer();
}
#Override
public boolean isCommitted() {
return original.isCommitted();
}
#Override
public void reset() {
original.reset();
}
#Override
public void setLocale(Locale loc) {
original.setLocale(loc);
}
#Override
public Locale getLocale() {
return original.getLocale();
}
#Override
public void addCookie(Cookie cookie) {
original.addCookie(cookie);
}
#Override
public boolean containsHeader(String name) {
return original.containsHeader(name);
}
#Override
public String encodeURL(String url) {
return original.encodeURL(url);
}
#Override
public String encodeRedirectURL(String url) {
return original.encodeRedirectURL(url);
}
#SuppressWarnings("deprecation")
#Override
public String encodeUrl(String url) {
return original.encodeUrl(url);
}
#SuppressWarnings("deprecation")
#Override
public String encodeRedirectUrl(String url) {
return original.encodeRedirectUrl(url);
}
#Override
public void sendError(int sc, String msg) throws IOException {
original.sendError(sc, msg);
}
#Override
public void sendError(int sc) throws IOException {
original.sendError(sc);
}
#Override
public void sendRedirect(String location) throws IOException {
original.sendRedirect(location);
}
#Override
public void setDateHeader(String name, long date) {
original.setDateHeader(name, date);
}
#Override
public void addDateHeader(String name, long date) {
original.addDateHeader(name, date);
}
#Override
public void setHeader(String name, String value) {
original.setHeader(name, value);
}
#Override
public void addHeader(String name, String value) {
original.addHeader(name, value);
}
#Override
public void setIntHeader(String name, int value) {
original.setIntHeader(name, value);
}
#Override
public void addIntHeader(String name, int value) {
original.addIntHeader(name, value);
}
#Override
public void setStatus(int sc) {
original.setStatus(sc);
}
#SuppressWarnings("deprecation")
#Override
public void setStatus(int sc, String sm) {
original.setStatus(sc, sm);
}
#Override
public String getHeader(String arg0) {
return original.getHeader(arg0);
}
#Override
public Collection<String> getHeaderNames() {
return original.getHeaderNames();
}
#Override
public Collection<String> getHeaders(String arg0) {
return original.getHeaders(arg0);
}
#Override
public int getStatus() {
return original.getStatus();
}
}
}
If you dont mind trying Spring AOP, this is something I have been exploring for logging purposes and it works pretty well for me. It wont log requests that have not been defined and failed request attempts though.
Add these three dependencies
spring-aop, aspectjrt, aspectjweaver
Add this to your xml config file <aop:aspectj-autoproxy/>
Create an annotation which can be used as a pointcut
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD,ElementType.TYPE})
public #interface EnableLogging {
ActionType actionType();
}
Now annotate all your rest API methods which you want to log
#EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
#Override
public Response getEmployees(RequestDto req, final String param) {
...
}
Now on to the Aspect. component-scan the package which this class is in.
#Aspect
#Component
public class Aspects {
#AfterReturning(pointcut = "execution(#co.xyz.aspect.EnableLogging * *(..)) && #annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
if (result instanceof Response) {
Response responseObj = (Response) result;
String requestUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
+ "?" + request.getQueryString();
String clientIp = request.getRemoteAddr();
String clientRequest = reqArg.toString();
int httpResponseStatus = responseObj.getStatus();
responseObj.getEntity();
// Can log whatever stuff from here in a single spot.
}
#AfterThrowing(pointcut = "execution(#co.xyz.aspect.EnableLogging * *(..)) && #annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
String requestUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
+ "?" + request.getQueryString();
exception.getMessage();
exception.getCause();
exception.printStackTrace();
exception.getLocalizedMessage();
// Can log whatever exceptions, requests, etc from here in a single spot.
}
}
#AfterReturning advice runs when a matched method execution returns
normally.
#AfterThrowing advice runs when a matched method execution exits by
throwing an exception.
If you want to read in detail read through this.
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
Currently Spring Boot has the Actuator feature to get the logs of requests and responses.
But you can also get the logs using Aspect(AOP).
Aspect provides you with annotations like: #Before, #AfterReturning, #AfterThrowing etc.
#Before logs the request, #AfterReturning logs the response and #AfterThrowing logs the error message,
You may not need all endpoints' log, so you can apply some filters on the packages.
Here are some examples:
For Request:
#Before("within(your.package.where.endpoints.are..*)")
public void endpointBefore(JoinPoint p) {
if (log.isTraceEnabled()) {
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
if (signatureArgs[0] != null) {
log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
}
} catch (JsonProcessingException e) {
}
}
}
Here #Before("within(your.package.where.endpoints.are..*)") has the package path. All endpoints within this package will generate the log.
For Response:
#AfterReturning(value = ("within(your.package.where.endpoints.are..*)"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) {
if (log.isTraceEnabled()) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
}
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
}
}
Here #AfterReturning("within(your.package.where.endpoints.are..*)") has the package path. All endpoints within this package will generate the log. Also Object returnValue contains the response.
For Exception:
#AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws DmoneyException {
if (log.isTraceEnabled()) {
System.out.println(e.getMessage());
e.printStackTrace();
log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
}
}
Here #AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") has the package path. All endpoints within this package will generate the log. Also Exception e contains the error response.
Here is the full code:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
#Aspect
#Order(1)
#Component
#ConditionalOnExpression("${endpoint.aspect.enabled:true}")
public class EndpointAspect {
static Logger log = Logger.getLogger(EndpointAspect.class);
#Before("within(your.package.where.is.endpoint..*)")
public void endpointBefore(JoinPoint p) {
if (log.isTraceEnabled()) {
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
if (signatureArgs[0] != null) {
log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
}
} catch (JsonProcessingException e) {
}
}
}
#AfterReturning(value = ("within(your.package.where.is.endpoint..*)"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) {
if (log.isTraceEnabled()) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
}
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
}
}
#AfterThrowing(pointcut = ("within(your.package.where.is.endpoint..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws Exception {
if (log.isTraceEnabled()) {
System.out.println(e.getMessage());
e.printStackTrace();
log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
}
}
}
Here, using #ConditionalOnExpression("${endpoint.aspect.enabled:true}") you can enable/disable the log. just add endpoint.aspect.enabled:true into the application.property and control the log
More info about AOP visit here:
Spring docs about AOP
Sample article about AOP
After adding Actuators to the spring boot bassed application you have /trace endpoint available with latest requests informations. This endpoint is working based on TraceRepository and default implementation is InMemoryTraceRepository that saves last 100 calls. You can change this by implementing this interface by yourself and make it available as a Spring bean. For example to log all requests to log (and still use default implementation as a basic storage for serving info on /trace endpoint) I'm using this kind of implementation:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
#Component
public class LoggingTraceRepository implements TraceRepository {
private static final Logger LOG = LoggerFactory.getLogger(LoggingTraceRepository.class);
private final TraceRepository delegate = new InMemoryTraceRepository();
#Override
public List<Trace> findAll() {
return delegate.findAll();
}
#Override
public void add(Map<String, Object> traceInfo) {
LOG.info(traceInfo.toString());
this.delegate.add(traceInfo);
}
}
This traceInfo map contains basic informations about request and response in this kind of form:
{method=GET, path=/api/hello/John, headers={request={host=localhost:8080, user-agent=curl/7.51.0, accept=*/*}, response={X-Application-Context=application, Content-Type=text/plain;charset=UTF-8, Content-Length=10, Date=Wed, 29 Mar 2017 20:41:21 GMT, status=200}}}. There is NO response content here.
EDIT! Logging POST data
You can access POST data by overriding WebRequestTraceFilter, but don't think it is a good idea (e.g. all uploaded file content will go to logs)
Here is sample code, but don't use it:
package info.fingo.nuntius.acuate.trace;
import org.apache.commons.io.IOUtils;
import org.springframework.boot.actuate.trace.TraceProperties;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;
#Component
public class CustomWebTraceFilter extends WebRequestTraceFilter {
public CustomWebTraceFilter(TraceRepository repository, TraceProperties properties) {
super(repository, properties);
}
#Override
protected Map<String, Object> getTrace(HttpServletRequest request) {
Map<String, Object> trace = super.getTrace(request);
String multipartHeader = request.getHeader("content-type");
if (multipartHeader != null && multipartHeader.startsWith("multipart/form-data")) {
Map<String, Object> parts = new LinkedHashMap<>();
try {
request.getParts().forEach(
part -> {
try {
parts.put(part.getName(), IOUtils.toString(part.getInputStream(), Charset.forName("UTF-8")));
} catch (IOException e) {
e.printStackTrace();
}
}
);
} catch (IOException | ServletException e) {
e.printStackTrace();
}
if (!parts.isEmpty()) {
trace.put("multipart-content-map", parts);
}
}
return trace;
}
}
Please refer to below link for actual answer
https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba
Made some changes from above referred solution , request and response will log in console and in file too if logger level is info. we can print either in console or file.
#Component
public class LoggingFilter extends OncePerRequestFilter {
private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
MediaType.valueOf("text/*"),
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_XML,
MediaType.valueOf("application/*+json"),
MediaType.valueOf("application/*+xml"),
MediaType.MULTIPART_FORM_DATA
);
Logger log = LoggerFactory.getLogger(ReqAndResLoggingFilter.class);
private static final Path path = Paths.get("/home/ramesh/loggerReq.txt");
private static BufferedWriter writer = null;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"));
if (isAsyncDispatch(request)) {
filterChain.doFilter(request, response);
} else {
doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
}
}finally {
writer.close();
}
}
protected void doFilterWrapped(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain) throws ServletException, IOException {
try {
beforeRequest(request, response);
filterChain.doFilter(request, response);
}
finally {
afterRequest(request, response);
response.copyBodyToResponse();
}
}
protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
if (log.isInfoEnabled()) {
logRequestHeader(request, request.getRemoteAddr() + "|>");
}
}
protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
if (log.isInfoEnabled()) {
logRequestBody(request, request.getRemoteAddr() + "|>");
logResponse(response, request.getRemoteAddr() + "|<");
}
}
private void logRequestHeader(ContentCachingRequestWrapper request, String prefix) throws IOException {
String queryString = request.getQueryString();
if (queryString == null) {
printLines(prefix,request.getMethod(),request.getRequestURI());
log.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
} else {
printLines(prefix,request.getMethod(),request.getRequestURI(),queryString);
log.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
}
Collections.list(request.getHeaderNames()).forEach(headerName ->
Collections.list(request.getHeaders(headerName)).forEach(headerValue ->
log.info("{} {}: {}", prefix, headerName, headerValue)));
printLines(prefix);
printLines(RequestContextHolder.currentRequestAttributes().getSessionId());
log.info("{}", prefix);
log.info(" Session ID: ", RequestContextHolder.currentRequestAttributes().getSessionId());
}
private void printLines(String ...args) throws IOException {
try {
for(String varArgs:args) {
writer.write(varArgs);
writer.newLine();
}
}catch(IOException ex){
ex.printStackTrace();
}
}
private void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
byte[] content = request.getContentAsByteArray();
if (content.length > 0) {
logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
}
}
private void logResponse(ContentCachingResponseWrapper response, String prefix) throws IOException {
int status = response.getStatus();
printLines(prefix, String.valueOf(status), HttpStatus.valueOf(status).getReasonPhrase());
log.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
response.getHeaderNames().forEach(headerName ->
response.getHeaders(headerName).forEach(headerValue ->
log.info("{} {}: {}", prefix, headerName, headerValue)));
printLines(prefix);
log.info("{}", prefix);
byte[] content = response.getContentAsByteArray();
if (content.length > 0) {
logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
}
}
private void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
MediaType mediaType = MediaType.valueOf(contentType);
boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
if (visible) {
try {
String contentString = new String(content, contentEncoding);
Stream.of(contentString.split("\r\n|\r|\n")).forEach(line -> {
try {
printLines(line);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
// log.info("{} {}", prefix, line));
} catch (UnsupportedEncodingException e) {
log.info("{} [{} bytes content]", prefix, content.length);
}
} else {
log.info("{} [{} bytes content]", prefix, content.length);
}
}
private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
if (request instanceof ContentCachingRequestWrapper) {
return (ContentCachingRequestWrapper) request;
} else {
return new ContentCachingRequestWrapper(request);
}
}
private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
if (response instanceof ContentCachingResponseWrapper) {
return (ContentCachingResponseWrapper) response;
} else {
return new ContentCachingResponseWrapper(response);
}
}
}
Output in File:
127.0.0.1|>
POST
/createUser
127.0.0.1|>
session Id:C0793464532E7F0C7154913CBA018B2B
Request:
{
"name": "asdasdas",
"birthDate": "2018-06-21T17:11:15.679+0000"
}
127.0.0.1|<
200
OK
127.0.0.1|<
Response:
{"name":"asdasdas","birthDate":"2018-06-21T17:11:15.679+0000","id":4}
Here my solution (Spring 2.0.x)
Add the maven dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Edit the application.properties and add the following line:
management.endpoints.web.exposure.include=*
Once your spring boot application is started you can track the latest 100 http requests by calling this url:
http://localhost:8070/actuator/httptrace
You can also configure a custom Spring interceptor HandlerInterceptorAdapter for a simplified implementation of pre-only/post-only interceptors:
#Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle (final HttpServletRequest request, final HttpServletResponse response,
final Object handler)
throws Exception {
// Logs here
return super.preHandle(request, response, handler);
}
#Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) {
// Logs here
}
}
Then, you register as many interceptors as you want:
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
#Autowired
CustomHttpInterceptor customHttpInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customHttpInterceptor).addPathPatterns("/endpoints");
}
}
Note: just like stated by #Robert, you need to pay attention to the specific implementations of HttpServletRequest and HttpServletResponse your application is using.
For example, for apps using the ShallowEtagHeaderFilter, the response implementation would be a ContentCachingResponseWrapper, so you'd have:
#Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomHttpInterceptor.class);
private static final int MAX_PAYLOAD_LENGTH = 1000;
#Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) {
final byte[] contentAsByteArray = ((ContentCachingResponseWrapper) response).getContentAsByteArray();
LOGGER.info("Request body:\n" + getContentAsString(contentAsByteArray, response.getCharacterEncoding()));
}
private String getContentAsString(byte[] buf, String charsetName) {
if (buf == null || buf.length == 0) {
return "";
}
try {
int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);
return new String(buf, 0, length, charsetName);
} catch (UnsupportedEncodingException ex) {
return "Unsupported Encoding";
}
}
}
the code pasted below works with my tests and can be downloaded from my [github project][1], sharing after applying a solution based on that on a production project.
#Configuration
public class LoggingFilter extends GenericFilterBean {
/**
* It's important that you actually register your filter this way rather then just annotating it
* as #Component as you need to be able to set for which "DispatcherType"s to enable the filter
* (see point *1*)
*
* #return
*/
#Bean
public FilterRegistrationBean<LoggingFilter> initFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());
// *1* make sure you sett all dispatcher types if you want the filter to log upon
registrationBean.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
// *2* this should put your filter above any other filter
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingRequestWrapper wreq =
new ContentCachingRequestWrapper(
(HttpServletRequest) request);
ContentCachingResponseWrapper wres =
new ContentCachingResponseWrapper(
(HttpServletResponse) response);
try {
// let it be ...
chain.doFilter(wreq, wres);
// makes sure that the input is read (e.g. in 404 it may not be)
while (wreq.getInputStream().read() >= 0);
System.out.printf("=== REQUEST%n%s%n=== end request%n",
new String(wreq.getContentAsByteArray()));
// Do whatever logging you wish here, in this case I'm writing request
// and response to system out which is probably not what you wish to do
System.out.printf("=== RESPONSE%n%s%n=== end response%n",
new String(wres.getContentAsByteArray()));
// this is specific of the "ContentCachingResponseWrapper" we are relying on,
// make sure you call it after you read the content from the response
wres.copyBodyToResponse();
// One more point, in case of redirect this will be called twice! beware to handle that
// somewhat
} catch (Throwable t) {
// Do whatever logging you whish here, too
// here you should also be logging the error!!!
throw t;
}
}
}
If somebody still need it here is simple implementation with Spring HttpTrace Actuator. But as they have told upper it doesn't log bodies.
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.stereotype.Repository;
#Slf4j
#Repository
public class LoggingInMemoryHttpTraceRepository extends InMemoryHttpTraceRepository {
public void add(HttpTrace trace) {
super.add(trace);
log.info("Trace:" + ToStringBuilder.reflectionToString(trace));
log.info("Request:" + ToStringBuilder.reflectionToString(trace.getRequest()));
log.info("Response:" + ToStringBuilder.reflectionToString(trace.getResponse()));
}
}
#hahn's answer required a bit of modification for it to work for me, but it is by far the most customizable thing I could get.
It didn't work for me, probably because I also have a HandlerInterceptorAdapter[??] but I kept getting a bad response from the server in that version. Here's my modification of it.
public class LoggableDispatcherServlet extends DispatcherServlet {
private final Log logger = LogFactory.getLog(getClass());
#Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
long startTime = System.currentTimeMillis();
try {
super.doDispatch(request, response);
} finally {
log(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response),
System.currentTimeMillis() - startTime);
}
}
private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, long timeTaken) {
int status = responseToCache.getStatus();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("httpStatus", status);
jsonObject.addProperty("path", requestToCache.getRequestURI());
jsonObject.addProperty("httpMethod", requestToCache.getMethod());
jsonObject.addProperty("timeTakenMs", timeTaken);
jsonObject.addProperty("clientIP", requestToCache.getRemoteAddr());
if (status > 299) {
String requestBody = null;
try {
requestBody = requestToCache.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
} catch (IOException e) {
e.printStackTrace();
}
jsonObject.addProperty("requestBody", requestBody);
jsonObject.addProperty("requestParams", requestToCache.getQueryString());
jsonObject.addProperty("tokenExpiringHeader",
responseToCache.getHeader(ResponseHeaderModifierInterceptor.HEADER_TOKEN_EXPIRING));
}
logger.info(jsonObject);
}
}
Has there been any development with Actuator HTTP Trace since the initial question was posted i.e. is there a way to enrich it with the response body?
What about enriching it with custom metadata from MDC or from Spring-Sleuth or Zipkin, such as traceId and spanId?
Also for me Actuator HTTP Trace didn't work Spring Boot 2.2.3, and I found the fix here: https://juplo.de/actuator-httptrace-does-not-work-with-spring-boot-2-2/
pom.xml
<dependency>
<groupId>org.springframework.boot
<artifactId>spring-boot-starter-actuator
</dependency>
application.properties
management.endpoints.web.exposure.include=httptrace
The fix:
The simple fix for this problem is, to add a #Bean of type
InMemoryHttpTraceRepository to your #Configuration-class:
#Bean
public HttpTraceRepository htttpTraceRepository()
{
return new InMemoryHttpTraceRepository();
}
The Explanation:
The cause of this problem is not a bug, but a legitimate change in the
default configuration. Unfortunately, this change is not noted in the
according section of the documentation. Instead it is burried in the
Upgrade Notes for Spring Boot 2.2
The default-implementation stores the captured data in memory. Hence,
it consumes much memory, without the user knowing, or even worse:
needing it. This is especially undesirable in cluster environments,
where memory is a precious good. And remember: Spring Boot was
invented to simplify cluster deployments!
That is, why this feature is now turned of by default and has to be
turned on by the user explicitly, if needed.
If you are seeing only part of your request payload, you need to call the setMaxPayloadLength function as it defaults to showing only 50 characters in your request body. Also, setting setIncludeHeaders to false is a good idea if you don't want to log your auth headers!
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(false);
loggingFilter.setIncludeQueryString(false);
loggingFilter.setIncludePayload(true);
loggingFilter.setIncludeHeaders(false);
loggingFilter.setMaxPayloadLength(500);
return loggingFilter;
}
As suggested previously, Logbook is just about perfect for this, but I did have a little trouble setting it up when using Java modules, due to a split package between logbook-api and logbook-core.
For my Gradle + Spring Boot project, I needed
build.gradle
dependencies {
compileOnly group: 'org.zalando', name: 'logbook-api', version: '2.4.1'
runtimeOnly group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.4.1'
//...
}
logback-spring.xml
<configuration>
<!-- HTTP Requests and Responses -->
<logger name="org.zalando.logbook" level="trace" />
</configuration>
In order to log all the requests with input parameters and body, we can use filters and interceptors. But while using a filter or interceptor, we cannot print the request body multiple times.
The better way is we can use spring-AOP. By using this we can decouple the logging mechanism from the application. AOP can be used for logging Input and output of each method in the application.
My solution is:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
#Aspect
#Component
public class LoggingAdvice {
private static final Logger logger =
LoggerFactory.getLogger(LoggingAdvice.class);
//here we can provide any methodName, packageName, className
#Pointcut(value = "execution(* com.package.name.*.*.*(..) )")
public void myPointcut() {
}
#Around("myPointcut()")
public Object applicationLogger(ProceedingJoinPoint pjt) throws Throwable {
ObjectMapper mapper = new ObjectMapper();
String methodName = pjt.getSignature().getName();
String className = pjt.getTarget().getClass().toString();
String inputParams = this.getInputArgs(pjt ,mapper);
logger.info("method invoked from " + className + " : " + methodName + "--Request Payload::::"+inputParams);
Object object = pjt.proceed();
try {
logger.info("Response Object---" + mapper.writeValueAsString(object));
} catch (Exception e) {
}
return object;
}
private String getInputArgs(ProceedingJoinPoint pjt,ObjectMapper mapper) {
Object[] array = pjt.getArgs();
CodeSignature signature = (CodeSignature) pjt.getSignature();
StringBuilder sb = new StringBuilder();
sb.append("{");
int i = 0;
String[] parameterNames = signature.getParameterNames();
int maxArgs = parameterNames.length;
for (String name : signature.getParameterNames()) {
sb.append("[").append(name).append(":");
try {
sb.append(mapper.writeValueAsString(array[i])).append("]");
if(i != maxArgs -1 ) {
sb.append(",");
}
} catch (Exception e) {
sb.append("],");
}
i++;
}
return sb.append("}").toString();
}
}
Note
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
...
}
approach will not work with spring security filter chains.
You have to manually add CommonsRequestLoggingFilter like
protected void configure(HttpSecurity http) throws Exception {
HttpSecurity filter = http
.cors().and().addFilterBefore(new CommonsRequestLoggingFilter(), CorsFilter.class);
}
if you use Tomcat in your boot app here is org.apache.catalina.filters.RequestDumperFilter in a class path for you. (but it will not provide you "with exceptions in single place").
If you have Spring boot Config server configured then just enable Debug logger for class :
Http11InputBuffer.Http11InputBuffer.java
Debugs will log all the requests and responses for every request
I created a file called LoggingConfig.java with contents:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
#Configuration
public class LoggingConfig {
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
final CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true);
loggingFilter.setIncludeQueryString(true);
loggingFilter.setIncludePayload(true);
loggingFilter.setMaxPayloadLength(32768);
return loggingFilter;
}
}
In application.properties I added:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
Logging Request + payload in a CUSTOM FORMAT:
For a custom format, just override the super implementation of the Spring logger Bean - org/springframework/web/filter/AbstractRequestLoggingFilter.java
Let's say we whish skipping GET requests and only tracing write requests (PUT, PATCH, DELETE, etc) with INFO log level:
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter logFilter = new CommonsRequestLoggingFilter() {
#Override
protected boolean shouldLog(HttpServletRequest request) {
return logger.isInfoEnabled() && !Objects.equals(request.getMethod(), "GET");
}
#Override
protected void beforeRequest(HttpServletRequest request, String message) {
// Do nothing if you need logging payload.
// As, Before the Request, the payload is not read from the input-stream, yet.
}
#Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.info(message); // Or log to a file here, as OP asks.
}
#Override
protected #NonNull String createMessage(HttpServletRequest request, #NonNull String prefix, #NonNull String suffix) {
// Output: [PUT][/api/my-entity], user:[my-loging], payload was:[{ "id": 33, "value": 777.00}]
StringBuilder msg = new StringBuilder()
.append(prefix)
.append("[").append(request.getMethod()).append("]")
.append("[").append(request.getRequestURI()).append("]");
String user = request.getRemoteUser();
msg.append(", user:[").append(null == user ? "" : user).append("]");
String payload = getMessagePayload(request);
if (payload != null) {
// It's not null on After event. As, on Before event, the Input stream was not read, yet.
msg.append(", payload was:[").append(payload.replace("\n", "")).append("]"); // Remove /n to be compliant with elastic search readers.
}
msg.append(suffix);
return msg.toString();
}
};
logFilter.setBeforeMessagePrefix("Incoming REST call: -->>>[");
logFilter.setBeforeMessageSuffix("]...");
logFilter.setAfterMessagePrefix("REST call processed: -<<<[");
logFilter.setAfterMessageSuffix("]");
logFilter.setIncludePayload(true);
logFilter.setMaxPayloadLength(64000);
return logFilter;
}
Loggging Requeest + Response/status:
see https://www.baeldung.com/spring-http-logging#custom-request-logging
(I can add the exact code example to here if the answer gets the demand/ reaches 50+ upvotes)
You can use Actuator in Spring Boot.
It logs requests and responses and more information about servlet and system operating.
You just add it as a dependency of your project and config, if more details needed to log.
Take a look at this example:
Actuator in Spring Boot example
In order to log requests that result in 400 only:
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.AbstractRequestLoggingFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;
/**
* Implementation is partially copied from {#link AbstractRequestLoggingFilter} and modified to output request information only if request resulted in 400.
* Unfortunately {#link AbstractRequestLoggingFilter} is not smart enough to expose {#link HttpServletResponse} value in afterRequest() method.
*/
#Component
public class RequestLoggingFilter extends OncePerRequestFilter {
public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";
public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
private final boolean includeQueryString = true;
private final boolean includeClientInfo = true;
private final boolean includeHeaders = true;
private final boolean includePayload = true;
private final int maxPayloadLength = (int) (2 * FileUtils.ONE_MB);
private final String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;
private final String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;
/**
* The default value is "false" so that the filter may log a "before" message
* at the start of request processing and an "after" message at the end from
* when the last asynchronously dispatched thread is exiting.
*/
#Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
#Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
throws ServletException, IOException {
final boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
if (includePayload && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request, maxPayloadLength);
}
final boolean shouldLog = shouldLog(requestToUse);
try {
filterChain.doFilter(requestToUse, response);
} finally {
if (shouldLog && !isAsyncStarted(requestToUse)) {
afterRequest(requestToUse, response, getAfterMessage(requestToUse));
}
}
}
private String getAfterMessage(final HttpServletRequest request) {
return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
}
private String createMessage(final HttpServletRequest request, final String prefix, final String suffix) {
final StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append("uri=").append(request.getRequestURI());
if (includeQueryString) {
final String queryString = request.getQueryString();
if (queryString != null) {
msg.append('?').append(queryString);
}
}
if (includeClientInfo) {
final String client = request.getRemoteAddr();
if (StringUtils.hasLength(client)) {
msg.append(";client=").append(client);
}
final HttpSession session = request.getSession(false);
if (session != null) {
msg.append(";session=").append(session.getId());
}
final String user = request.getRemoteUser();
if (user != null) {
msg.append(";user=").append(user);
}
}
if (includeHeaders) {
msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
}
if (includeHeaders) {
final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
final byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
final int length = Math.min(buf.length, maxPayloadLength);
String payload;
try {
payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
} catch (final UnsupportedEncodingException ex) {
payload = "[unknown]";
}
msg.append(";payload=").append(payload);
}
}
}
msg.append(suffix);
return msg.toString();
}
private boolean shouldLog(final HttpServletRequest request) {
return true;
}
private void afterRequest(final HttpServletRequest request, final HttpServletResponse response, final String message) {
if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
logger.warn(message);
}
}
}
you can use Aspect Oriented Programming To Handle All These In One Plcae.

Categories