Logging original HTTP requests and responses in Spring Boot using spring filters

Viewed 2656

I am trying to log requests and response in spring boot using filter(lets say LoggingFilter) and trying to get some specific attributes from request body to log in MDC. As getInputStream method from request can only be called once, one needs to use a request and response wrapper. In order to do so spring provides wrapper classes ContentCachingRequestWrapper and ContentCachingResponseWrapper for requests and response respectively. But on using ContentCachingRequestWrapper in filter, request data is only logged after filterChain.doInternalFilter method is called in LoggingFilter.

To log request before filterChain.doInternalFilter, I made a custom class of request wrapper by doing some modification in code of ContentCachingRequestWrapper and now everything is working fine. But I wanted to know if theres any issue in doing so (performance impact, unwanted memory initialisations, Java heap memory exhausting at some point, etc) as its always good to have some expert opinion. :)

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.FastByteArrayOutputStream;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;

/**
 * this file is a copy of Spring's ContentCachingRequestWrapper with few modifications.
 */

@Slf4j
public class MDCContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
    private final ByteArrayOutputStream cachedContent;
    private final Integer contentCacheLimit;
    private ServletInputStream inputStream;
    private BufferedReader reader;
    private FastByteArrayOutputStream fastByteArrayOutputStream;

    public String getPayload() {
//        return payload;
        return new String(fastByteArrayOutputStream.toByteArray());
    }

    private String payload;


    public MDCContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
        int contentLength = request.getContentLength();
        if (isFormPost()) {
            this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
        } else {
            this.cachedContent = new ByteArrayOutputStream();
        }
        this.contentCacheLimit = null;
        try {
            this.getParameterMap();
        } catch (Exception e) {
            log.error("Unable to get parameters map.");
        }

//        StringBuilder stringBuilder = new StringBuilder();
//        try {
//            byte[] buffer = new byte[1024];
//            InputStream inputStream = request.getInputStream();
//            int i = 0;
//            while (-1 != (i = inputStream.read(buffer))) {
//                stringBuilder.append(new String(buffer, 0, i));
//            }
//            inputStream.close();
//        } catch (IOException e) {
//            log.error("can't clone input stream: ", e);
////                throw new RuntimeException(e);
//        }
//        payload = stringBuilder.toString();

        fastByteArrayOutputStream = new FastByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
        try {
            byte[] buffer = new byte[1024];
            InputStream inputStream = request.getInputStream();
            int i = 0;
            while (-1 != (i = inputStream.read(buffer))) {
                fastByteArrayOutputStream.write(buffer, 0, i);
            }
            inputStream.close();
        } catch (IOException e) {
            log.error("can't clone input stream to byteOutputStream: ", e);
//                throw new RuntimeException(e);
      }

    }

    public MDCContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) {
        super(request);
        this.cachedContent = new ByteArrayOutputStream(contentCacheLimit);
        this.contentCacheLimit = contentCacheLimit;
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (this.inputStream == null) {
//            InputStream byteArrayInputStream = IOUtils.toInputStream(this.getPayload(), this.getCharacterEncoding());
            InputStream byteArrayInputStream = fastByteArrayOutputStream.getInputStream();
            this.inputStream = new ServletInputStream() {
                boolean overflow = false;

                public int read() throws IOException {
                    int ch = byteArrayInputStream.read();
                    if (ch != -1 && !overflow) {
                        if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
                            overflow = true;
                            handleContentOverflow(contentCacheLimit);
                        } else {
                            cachedContent.write(ch);
                        }
                    } else {
                        byteArrayInputStream.close();
                    }

                    return ch;
                }

                @Override
                public boolean isFinished() {
                    try {
                        return byteArrayInputStream.available() == 0;
                    } catch (IOException e) {
                        return false;
                    }

                }

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

                @Override
                public void setReadListener(ReadListener readListener) {
                    log.info("setReadListener method called.");
                }
            };
        }
        return this.inputStream;
    }

    public String getCharacterEncoding() {
        String enc = super.getCharacterEncoding();
        return enc != null ? enc : "ISO-8859-1";
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (this.reader == null) {
            this.reader = new BufferedReader(new InputStreamReader(this.getInputStream(), this.getCharacterEncoding()));
        }
        return this.reader;
    }

    public String getParameter(String name) {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameter(name);
    }

    public Map<String, String[]> getParameterMap() {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameterMap();
    }

    public Enumeration<String> getParameterNames() {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameterNames();
    }

    public String[] getParameterValues(String name) {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameterValues(name);
    }

    private boolean isFormPost() {
        String contentType = this.getContentType();
        return contentType != null && contentType.contains("application/x-www-form-urlencoded") && HttpMethod.POST.matches(this.getMethod());
    }

    private void writeRequestParametersToCachedContent() {
        try {
            if (this.cachedContent.size() == 0) {
                String requestEncoding = this.getCharacterEncoding();
                Map<String, String[]> form = super.getParameterMap();
                Map<String, Object> resultMap = new HashMap<>();
                Iterator nameIterator = form.keySet().iterator();

                while (nameIterator.hasNext()) {
                    String name = (String) nameIterator.next();
                    List<String> values = Arrays.asList((String[]) form.get(name));
                    Iterator valueIterator = values.iterator();
                    while (valueIterator.hasNext()) {
                        String value = (String) valueIterator.next();
                        this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes());
                        if (value != null) {
                            this.cachedContent.write(61);
                            this.cachedContent.write(URLEncoder.encode(value, requestEncoding).getBytes());
                            if (valueIterator.hasNext()) {
                                this.cachedContent.write(38);
                            }
                        }
                    }

                    if (nameIterator.hasNext()) {
                        this.cachedContent.write(38);
                    }
                }
            }

        } catch (IOException e) {
            throw new IllegalStateException("Failed to write request parameters to cached content", e);
        }
    }

    public byte[] getContentAsByteArray() {
        return this.cachedContent.toByteArray();
    }

    protected void handleContentOverflow(int contentCacheLimit) {
    }

}

Also I wanted to know which method to be used to copy inputStream data: the string method or using FastByteArrayOutputStream method or should I use something.

0 Answers
Related