54

I've created a filter to in my java webserver (appengine actually) that logs the parameters of an incoming request. I'd also like to log the resulting response that my webserver writes. Although I have access to the response object, I'm not sure how to get the actual string/content response out of it.

Any ideas?

BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
aloo
  • 5,111
  • 7
  • 46
  • 88
  • How are you writing your response? `response.getWriter().write(yourResponseString)`??? Or are you doing something different? Are you wanting to write errors as well? (In other words, do you want to log the response when you're doing `response.sendError(yourError)`??) – Dave Jan 19 '12 at 21:19
  • 1
    perhaps this http://java.sun.com/blueprints/corej2eepatterns/Patterns/InterceptingFilter.html and this http://docstore.mik.ua/orelly/xml/jxslt/ch08_04.htm might give you a hint – Sergey Benner Jan 19 '12 at 21:24
  • @Dave just using response.getWriter().write(yourResponseString) as you mentioned and thats the old output I'd like to capture. – aloo Jan 20 '12 at 00:58
  • Using the TeeOutputStream to write into two outputstreams at time: https://stackoverflow.com/a/28305057/1203628. – pdorgambide Jan 15 '19 at 17:02

5 Answers5

118

You need to create a Filter wherein you wrap the ServletResponse argument with a custom HttpServletResponseWrapper implementation wherein you override the getOutputStream() and getWriter() to return a custom ServletOutputStream implementation wherein you copy the written byte(s) in the base abstract OutputStream#write(int b) method. Then, you pass the wrapped custom HttpServletResponseWrapper to the FilterChain#doFilter() call instead and finally you should be able to get the copied response after the the call.

In other words, the Filter:

@WebFilter("/*")
public class ResponseLogger implements Filter {

    @Override
    public void init(FilterConfig config) throws ServletException {
        // NOOP.
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (response.getCharacterEncoding() == null) {
            response.setCharacterEncoding("UTF-8"); // Or whatever default. UTF-8 is good for World Domination.
        }

        HttpServletResponseCopier responseCopier = new HttpServletResponseCopier((HttpServletResponse) response);

        try {
            chain.doFilter(request, responseCopier);
            responseCopier.flushBuffer();
        } finally {
            byte[] copy = responseCopier.getCopy();
            System.out.println(new String(copy, response.getCharacterEncoding())); // Do your logging job here. This is just a basic example.
        }
    }

    @Override
    public void destroy() {
        // NOOP.
    }

}

The custom HttpServletResponseWrapper:

public class HttpServletResponseCopier extends HttpServletResponseWrapper {

    private ServletOutputStream outputStream;
    private PrintWriter writer;
    private ServletOutputStreamCopier copier;

    public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called on this response.");
        }

        if (outputStream == null) {
            outputStream = getResponse().getOutputStream();
            copier = new ServletOutputStreamCopier(outputStream);
        }

        return copier;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (outputStream != null) {
            throw new IllegalStateException("getOutputStream() has already been called on this response.");
        }

        if (writer == null) {
            copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
            writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
        }

        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (writer != null) {
            writer.flush();
        } else if (outputStream != null) {
            copier.flush();
        }
    }

    public byte[] getCopy() {
        if (copier != null) {
            return copier.getCopy();
        } else {
            return new byte[0];
        }
    }

}

The custom ServletOutputStream:

public class ServletOutputStreamCopier extends ServletOutputStream {

    private OutputStream outputStream;
    private ByteArrayOutputStream copy;

    public ServletOutputStreamCopier(OutputStream outputStream) {
        this.outputStream = outputStream;
        this.copy = new ByteArrayOutputStream(1024);
    }

    @Override
    public void write(int b) throws IOException {
        outputStream.write(b);
        copy.write(b);
    }

    public byte[] getCopy() {
        return copy.toByteArray();
    }

}
Community
  • 1
  • 1
BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
  • The constructor name for `HttpServletResponseCopier` is incorrect, I can't edit it because the edit should be more than 6 characters long and I don't want to change anything else about the answer. – vahidg Jun 25 '12 at 15:16
  • 9
    wondering why is it so complex to get the body of response. It should be something like response.getContent(). Must be some solid reasons behind it :) – antnewbee Feb 06 '13 at 16:01
  • 1
    @ant: It's memory hogging and usually not of interest for the webapp itself. – BalusC Feb 06 '13 at 16:02
  • In my application I have to read the result of login json and if login result is 1 then create a session else don't create any session. I am Using Spring/REST so its not possible for me to create HttpSession in spring. So I have to create session in doFilter. Just sharing a scenario where reading response content is required :) – antnewbee Feb 06 '13 at 18:34
  • 1
    @ant: just set a request attribute. – BalusC Feb 06 '13 at 18:36
  • @BalusC: is it possible. I have posted my question here http://stackoverflow.com/questions/14744442/setting-request-response-attributes-in-spring-rest-environment – antnewbee Feb 07 '13 at 05:46
  • Great!! This solution also worked for me. Can somebody explain the code as well? – quintin Sep 16 '15 at 08:34
  • i read the code but not able to understand what exactly we are doing, please provide brief summary or comments with the functions. – quintin Sep 16 '15 at 08:42
  • Thanks for the comments – quintin Sep 16 '15 at 08:51
  • Finally a decent answer! Thanks – Armen May 14 '16 at 01:35
  • 6
    In case of Spring, starting at version 4.1.3, there's also [ContentCachingResponseWrapper](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/util/ContentCachingResponseWrapper.html). – Benny Bottema Sep 05 '16 at 08:18
  • @BalusC what's not clear is that your filter solution says you can log the response when the response may have not been made available yet. I think this combined with a ContainerResponseFilter will do the trick. – F.O.O Apr 05 '18 at 13:44
  • @BalusC is the reason why getOutputStream() checks if writer != null, and getWriter() checks is outputStream != null because once the response has started to be written with one, you can't attempt to also write with the other? – Kevin Hooke Jun 18 '18 at 23:35
  • @KevinHooke Because the spec says so. See javadoc. – BalusC Jun 19 '18 at 15:47
  • Thankyou very much! – NoisyBoy Feb 19 '19 at 14:14
  • Great solution, worked for me – Dhruv Narayan Singh Dec 17 '20 at 17:30
13

BalusC solution is ok, but little outdated. Spring now has feature for it . All you need to do is use [ContentCachingResponseWrapper], which has method public byte[] getContentAsByteArray() .

I Suggest to make WrapperFactory which will allow to make it configurable, whether to use default ResponseWrapper or ContentCachingResponseWrapper.

omilus
  • 706
  • 8
  • 9
  • How do you "use" it? From playing around a little with it, it looks like you replace the HttpServletResponseCopier with ContentCachingResponseWrapper -- is that correct? – Fund Monica's Lawsuit Oct 02 '18 at 23:52
8

Instead of creating Custom HttpServletResponseWrapper.You can use ContentCachingResponseWrapper as it provide method getContentAsByteArray().

public void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = servletRequest;
        HttpServletResponse response = servletResponse;
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper =new ContentCachingResponseWrapper(response);
        try {
            super.doFilterInternal(requestWrapper, responseWrapper, filterChain);

        } finally {

            byte[] responseArray=responseWrapper.getContentAsByteArray();
            String responseStr=new String(responseArray,responseWrapper.getCharacterEncoding());
            System.out.println("string"+responseStr);       
            /*It is important to copy cached reponse body back to response stream
            to see response */
            responseWrapper.copyBodyToResponse();

        }

    }
Shrinivasan
  • 151
  • 1
  • 8
5

While BalusC's answer will work in most scenarios you have to be careful with the flush call - it commits response and no other writing to it is possible, eg. via following filters. We have found some problems with very simmilar approach in Websphere environment where the delivered response was only partial.

According to this question the flush should not be called at all and you should let it be called internally.

I have solved the flush problem by using TeeWriter (it splits stream into 2 streams) and using non-buffering streams in the "branched stream" for logging purpose. It is unneccessary to call the flush then.

private HttpServletResponse wrapResponseForLogging(HttpServletResponse response, final Writer branchedWriter) {
    return new HttpServletResponseWrapper(response) {
        PrintWriter writer;

        @Override
        public synchronized PrintWriter getWriter() throws IOException {
            if (writer == null) {
                writer = new PrintWriter(new TeeWriter(super.getWriter(), branchedWriter));
            }
            return writer;
        }
    };
}

Then you can use it this way:

protected void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
    //...
    StringBuilderWriter branchedWriter = new org.apache.commons.io.output.StringBuilderWriter();
    try {
        chain.doFilter(request, wrapResponseForLogging(response, branchedWriter));
    } finally {
        log.trace("Response: " + branchedWriter);
    }
}

The code is simplified for brewity.

Community
  • 1
  • 1
Petr Újezdský
  • 952
  • 9
  • 12
3

I am not quite familiar with appengine but you need something Access Log Valve in Tomcat. Its attribute pattern ; a formatting layout identifying the various information fields from the request and response to be logged, or the word common or combined to select a standard format.

It looks appengine has built in functionality for log filtering.

apply a servlet filter

Community
  • 1
  • 1
Muhammad Imran Tariq
  • 20,100
  • 42
  • 115
  • 186