Servlet Filer to Log Request and Response Payload

filter

Objective

To have one self containing class that can be plugged into any servlet container. Class will log everything about the request and response, including the payload data coming in and going out.

The Problem

I need to dump out the payload for posts, gets and responses to troubleshoot some issues where data got mismatched between the request and response. Ideal solution would have all this data on one single line. Quick look around gave no off the shelf results, so I put together a quick Filter implementation to capture request and response data.


The biggest challenge is to get the copies of request and response payload for logging and still eave it in tact for servlet processing. I had the write wrappers for input and output streams, as you will see in the inner classes. Here is the code:


package com.your.package.name;

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.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
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.apache.log4j.Logger;

public class ReqRespDumpFilter implements Filter {

	
	private static final Logger logger = Logger.getLogger(ReqRespDumpFilter.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 bufferedReqest = 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.getPathInfo())                                        
	                   .append("] [REQUEST PARAMETERS:")
	                   .append(requestMap)
	                   .append("] [REQUEST BODY:")
                           .append(bufferedReqest.getRequestBody())                                        
	                   .append("] [REMOTE ADDRESS:")
	                   .append(httpServletRequest.getRemoteAddr())
	                   .append("]");
	
	        chain.doFilter (bufferedReqest, bufferedResponse);        
	        logMessage.append(" [RESPONSE:").append( bufferedResponse.getContent() ).append("]");
	        logger.debug(logMessage);
	} catch( Throwable a ) {
		logger.error(a);
	}
	}

	
	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 = 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 letti;
            while ((letti = is.read(buf)) > 0) {
                this.baos.write(buf, 0, letti);
            }
            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);
        }
 

    }
	
    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();
		}		
    }
    
    
    
    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 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);
		}

    }
	
}

web.xml configuration

You need to add filter declaration to your web.xml and configure filter mapping for specific url(s).

        
        <filter>
		<filter-name>RRDumpFilter</filter-name>
		<filter-class>com.your.package.name.ReqRespDumpFilter</filter-class>
	</filter>
	 
	<filter-mapping>
		<filter-name>RRDumpFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

Conclusion

Now you can have your pie and eat it too. I mean see your request and response payload without adding logging of it in every servlet/controller/resource/endpoint/etc. Tested with java 1.6 in Tomcat 6. Dependency on commons-io to provide TeeOutputStream implementation and log4j, but you can substitute.

12 thoughts on “Servlet Filer to Log Request and Response Payload

  1. hey, it’s not working for me the method getOutputStream it’s never been called 🙁 so when i excecute the code i get a null pointer in bos.toString();

  2. Fixed, the issue by dealing with stream and also writer …. see code fragment:

    public class BufferedResponseWrapper implements HttpServletResponse {

    HttpServletResponse original;

    TeeServletOutputStream teeStream;

    PrintWriter teeWriter;

    ByteArrayOutputStream bos;

    public BufferedResponseWrapper(HttpServletResponse response) {
    original = response;
    }

    public String getContent() throws IOException {
    return bos.toString();
    }

    @Override
    public PrintWriter getWriter() throws IOException {

    if (this.teeWriter == null) {
    this.teeWriter = new PrintWriter(new OutputStreamWriter(getOutputStream()));
    }
    return this.teeWriter;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {

    if (teeStream == null) {
    bos = new ByteArrayOutputStream();
    teeStream = new TeeServletOutputStream(original.getOutputStream(), bos);
    }
    return teeStream;
    }

    @Override
    public void flushBuffer() throws IOException {
    if (teeStream != null) {
    teeStream.flush();
    System.err.println(“teeStream flush”);
    }
    if (this.teeWriter != null) {
    this.teeWriter.flush();
    System.err.println(“teeWriter flush”);
    }
    }

  3. Actually, BufferedResponseWrapper could be a lot shorter if you extended javax.servlet.http.HttpServletResponseWrapper.

  4. Hi Tomas,
    Thank you for making this example available.
    We have a need to develop a Servlet Filter and would like to make use of this example as a basis for what we need to do along with some modifications.
    Can I ask if you have any conditions of use as the copyright owner?
    Please feel free to email me direct, as I assume you will have access to my email address from this post.
    Thanks and hope to hear from you soon.
    Les.

  5. Provide a simple TeeOutputStream implementation if someone don’t have common io libs.

    private static class TeeOutputStream extends OutputStream
    {
    private OutputStream mChainStream;
    private OutputStream mTeeStream;

    public TeeOutputStream(OutputStream chainStream, OutputStream teeStream)
    {
    mChainStream = chainStream;
    mTeeStream = teeStream;
    }

    @Override
    public void write(int b) throws IOException
    {
    mChainStream.write(b);
    mTeeStream.write(b);
    mTeeStream.flush();
    }

    @Override
    public void close() throws IOException
    {
    flush();
    mChainStream.close();
    mTeeStream.close();
    }

    @Override
    public void flush() throws IOException
    {
    mChainStream.close();
    }
    }

  6. Hi,
    I don’t see in BufferedResponseWrapper method where we can close streams something like
    public void finish() throws IOException {
    if (this.teeWriter != null) {
    this.teeWriter.close();
    }
    if (this.teeStream != null) {
    this.teeStream.close();
    }
    }

    and call this method in ReqRespDumpFilter doFilter


    chain.doFilter (bufferedReqest, bufferedResponse);
    bufferedResponseWrapper.finish();
    ….

    Something similar is in logback TeeFilter
    http://logback.qos.ch/xref/ch/qos/logback/access/servlet/TeeFilter.html

  7. thansk for the example. however in my case i am getting a special character in payload ‘&’ for a perticular and when it comes to chain.doFilter the server returns the error code 400
    Any workaround ?

  8. Thanks for providing the code. To get it working for me I had to do following.

    1. I need to make sure I was using the javax.servlet-api 3.0.1.

    javax.servlet
    javax.servlet-api
    3.0.1

    2. I had to implement and override “getHeader(String name)”,Collection getHeaderNames(), Collection getHeaders(String arg0) , int getStatus() get method in “BufferedResponseWrapper”.

    3. I had to also make sure that no previous version of servlet-api was being include via transitive dependencies.

    4. I did take part of BufferedResponseWrapper implementation from the comments above.

Leave a Reply

Your email address will not be published. Required fields are marked *