3

I have Spring Boot and I need to log user action in DB, so I wrote HandlerInterceptor:

@Component
public class LogInterceptor implements HandlerInterceptor {
@Autovired
private LogUserActionService logUserActionService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
throws IOException {
    String userName = SecurityContextHolder.getContext().getAuthentication().getName();
    String url = request.getRequestURI();
    String queryString = request.getQueryString() != null ? request.getQueryString() : "";
    String body = "POST".equalsIgnoreCase(request.getMethod()) ? new BufferedReader(new InputStreamReader(request.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator())) : queryString;
    logUserActionService.logUserAction(userName, url, body);
    return true;
}
}

But according to this answer Get RequestBody and ResponseBody at HandlerInterceptor "RequestBody can be read only once", so as I understand I read input stream and then Spring tries to do same, but stream has been read already and I'm getting an error: "Required request body is missing ..."

So I tried different ways to make buffered input stream i.e.:

HttpServletRequest httpServletRequest = new ContentCachingRequestWrapper(request);
new BufferedReader(new InputStreamReader(httpServletRequest.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator()))

Or

InputStream bufferedInputStream = new BufferedInputStream(request.getInputStream());

But nothing helped Also I tried to use

@ControllerAdvice
public class UserActionRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {

But it has only body, no request info like URL or Request parameters Also tried to use Filters, but result same.

So I need a good way to get information from request like user, URL, parameters, body (if present) and write it to DB.

Dharman
  • 30,962
  • 25
  • 85
  • 135
user769552
  • 179
  • 1
  • 3
  • 10
  • Please check this one using filter, it worked for me. https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba – Harshit Mar 06 '20 at 04:03
  • Harshit, there is no body reading in your example, only URL etc. – user769552 Mar 06 '20 at 19:21
  • Yes it has, check ```logRequestBody``` method. – Harshit Mar 09 '20 at 03:26
  • Harshit, are you sure this code works? Cause I sent JSON body and wrote next code according to your example: new ContentCachingRequestWrapper(request).getContentAsByteArray(); and it returned empty byte array, but my controller got my DTO entity as expected. So I've got body, but method getContentAsByteArray returned nothing. – user769552 Mar 09 '20 at 18:44

4 Answers4

7

You can use Filter to log request body.

public class LoggingFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        try {
            chain.doFilter(wrappedRequest, res);
        } finally {
            logRequestBody(wrappedRequest);
        }
    }

    private static void logRequestBody(ContentCachingRequestWrapper request) {

        byte[] buf = request.getContentAsByteArray();
        if (buf.length > 0) {
            try {
                String requestBody = new String(buf, 0, buf.length, request.getCharacterEncoding());
                System.out.println(requestBody);
            } catch (Exception e) {
                System.out.println("error in reading request body");
            }
        }
    }
}

The main thing to note here is that you have to pass object of ContentCachingRequestWrapper in filter chain otherwise you won't get request content in it.

In above example, if you use chain.doFilter(req, res) or chain.doFilter(request, res) then you won't get request body in wrappedRequest object.

Harshit
  • 1,334
  • 1
  • 9
  • 23
  • Work great, I didn't though about it, that's solution witch I was looking for, thank you! – user769552 Mar 13 '20 at 20:32
  • 3
    This does work but only when you log after `chain.doFilter(wrappedRequest, res)` call. The reason is because `ContentCachingRequestWrapper` fills out the cache only when InputStream is read. If you want to log request payload before handling the request, then you'd need to extend `ContentCachingRequestWrapper` with your own wrapper. – dmitryb May 26 '20 at 09:28
6

To log HTTP Request & Response, you can use RequestBodyAdviceAdapter and ResponseBodyAdvice. here, it is using in my way.

CustomRequestBodyAdviceAdapter.java

@ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {

    @Autowired
    HttpServletRequest httpServletRequest;

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {

        // here you can full log httpServletRequest and body.

        return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
    }
}

CustomResponseBodyAdviceAdapter.java

@ControllerAdvice
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {

    @Autowired
    private LoggingService loggingService;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
            Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (serverHttpRequest instanceof ServletServerHttpRequest && serverHttpResponse instanceof ServletServerHttpResponse) {

            // here you can full log httpServletRequest and body.
        }
        return o;
    }
}

Above AdviceAdapter cannot handle the GET request. So, you can use HandlerInterceptor.

CustomWebConfigurerAdapter.java

@Component
public class CustomWebConfigurerAdapter implements WebMvcConfigurer {

   @Autowired
   private CustomLogInterceptor httpServiceInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(httpServiceInterceptor);
   }
}

CustomLogInterceptor.java

@Component
public class CustomLogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (DispatcherType.REQUEST.name().equals(request.getDispatcherType().name()) && request.getMethod().equals(HttpMethod.GET.name())) {

            // here you can full log httpServletRequest and body for GET Request.

        }
        return true;
    }
}

Here you can reference full source code in my git.

springboot-http-request-response-loging-with-json-logger

+Feature => It is already have Integration with ELK (Elasticsearch, Logstash, Kibana)

Zaw Than oo
  • 9,651
  • 13
  • 83
  • 131
  • Have you read my question? I said that I tried it 'but it has only body, no request info like URL or Request parameters'. Is there any way to get URL and query parameters using RequestBodyAdviceAdapter? May be I son't see something, but there is no request parameter in RequestBodyAdviceAdapter methods. If you show me how to get URL and request parameters in RequestBodyAdviceAdapter it would be great! – user769552 Mar 06 '20 at 17:40
  • And about Interseptor, I said if I read body in interceptor I'll get an Error, and you suggest me to use CustomLogInterceptor, why?? Did you read my question? How to ready body there? – user769552 Mar 06 '20 at 17:47
1

You can get the Request Body data using RequestBodyAdviceAdapter for POST/PUT requests. You can use HandlerInterceptorAdapter for GET calls. Here's a working example - https://frandorado.github.io/spring/2018/11/15/log-request-response-with-body-spring.html

@ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter
{

@Autowired
HttpServletRequest httpServletRequest;

private static final Log LOGGER = LogFactory.getLog(CustomRequestBodyAdviceAdapter.class);

private static final Charset DEFAULT_CHARSET = ISO_8859_1;


@Override
public boolean supports(MethodParameter methodParameter, Type type, 
                        Class<? extends HttpMessageConverter<?>> aClass)
{
    return true;
}

@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
                            MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) 
{
    Instant startTime = Instant.now();
    StringBuilder stringBuilder = new StringBuilder();

    
    stringBuilder.append("REQUEST call Starts :: Start Time : %s ").append(startTime);

    try
    {
        logRequest(httpServletRequest, body);

    } 
    catch (IOException e) 
    {
        LOGGER.info("Exception getting the Request Body into the Log: {}" + e.getMessage());
    }
    

public void logRequest(HttpServletRequest httpServletRequest, Object body) throws IOException
{
    StringBuilder stringBuilder = new StringBuilder();
    Map<String, String> parameters = buildParametersMap(httpServletRequest);
    
    
    stringBuilder.append("REQUEST ");
    stringBuilder.append("method=[").append(httpServletRequest.getMethod()).append("] ");
    stringBuilder.append("path=[").append(httpServletRequest.getRequestURI()).append("] ");
    stringBuilder.append("headers=[").append(buildHeadersMap(httpServletRequest)).append("] ");
    
    if (!parameters.isEmpty())
    {
        stringBuilder.append("parameters=[").append(parameters).append("] ");
    }
    
    if (body != null)
    {
        stringBuilder.append("body=[" + body + "]");
    }

    ObjectMapper objectMapper = new ObjectMapper();
    
    String jsonInString = null;
    try 
    {
        jsonInString = objectMapper.writer().writeValueAsString(body);
    } 
    catch (JsonProcessingException e) 
    {
        throw new RestApiException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
    }
    
    stringBuilder.append("REQUEST Body = [").append(jsonInString).append("] ");
    
    LOGGER.info("BODY DATA >>>> " + jsonInString);
    LOGGER.info("Body - : {}" + stringBuilder);
}



private Map<String, String> buildParametersMap(HttpServletRequest httpServletRequest)
{
    Map<String, String> resultMap = new HashMap<>();
    Enumeration<String> parameterNames = httpServletRequest.getParameterNames();
    
    while (parameterNames.hasMoreElements())
    {
        String key = parameterNames.nextElement();
        String value = httpServletRequest.getParameter(key);
        resultMap.put(key, value);
    }
    
    return resultMap;
}

private Map<String, String> buildHeadersMap(HttpServletRequest request)
{
    Map<String, String> map = new HashMap<>();
    
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) 
    {
        String key = headerNames.nextElement();
        String value = request.getHeader(key);
        map.put(key, value);
    }
    
    return map;
}
}

I have used ObjectMapper here because I need the body response as raw JSON object, but the afterBodyRead() is invoked after the body is transformed to Java Object.

srishti
  • 23
  • 1
  • 1
  • 9
-2

I found this solved my problem for copying the request buffer for application/json content types. It also shows how to extend the wrapper as the comments to Harshit solution mentions.

https://levelup.gitconnected.com/how-to-log-the-request-body-in-a-spring-boot-application-10083b70c66

The important pieces are that you need a filter to pass along the new request to the server.

@Component
public class LoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        if (Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
            CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
            requestWrapper.setAttribute("input", requestWrapper.getBodyInStringFormat());
            filterChain.doFilter(requestWrapper, servletResponse);
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

The logger requires a custom wrapper, and the one spring boot provides seems to be insufficient for application/json type messages.

public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {

    public String getBodyInStringFormat() {
        return bodyInStringFormat;
    }

    private final String bodyInStringFormat;

    public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
    }

    private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
        return getString(stream, charset);
    }

    static String getString(InputStream stream, Charset charset) throws IOException {
        final int MAX_BODY_SIZE = 1024;
        final StringBuilder bodyStringBuilder = new StringBuilder();
        if (!stream.markSupported()) {
            stream = new BufferedInputStream(stream);
        }

        stream.mark(MAX_BODY_SIZE + 1);
        final byte[] entity = new byte[MAX_BODY_SIZE + 1];
        final int bytesRead = stream.read(entity);

        if (bytesRead != -1) {
            bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
            if (bytesRead > MAX_BODY_SIZE) {
                bodyStringBuilder.append("...");
            }
        }
        stream.reset();

        return bodyStringBuilder.toString();
    }

    @Override
    public BufferedReader getReader()  {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream ()  {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());

        return new ServletInputStream() {
            private boolean finished = false;

            @Override
            public boolean isFinished() {
                return finished;
            }

            @Override
            public int available()  {
                return byteArrayInputStream.available();
            }

            @Override
            public void close() throws IOException {
                super.close();
                byteArrayInputStream.close();
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }

            public int read ()  {
                int data = byteArrayInputStream.read();
                if (data == -1) {
                    finished = true;
                }
                return data;
            }
        };
    }
}