Spring Boot - How to log all requests and responses with exceptions in single place?
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
@Beanpublic 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/testi.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/paramsi.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/123i.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.