18

I'd like to be able to print JAX-RS 2 JSON payload from request, regardless of actual implementation on my application server.

I've tried suggested solutions on SO but all include binaries from actual implementation (like Jersey and similar), and I'm allowed only to use javaee-api v 7.0 in my application.

I've tried implementing ClientRequestFilter and ClientResponseFilter on my Client but they don't contain serialized entities.

Here's an example of client:

WebTarget target = ClientBuilder.newClient().register(MyLoggingFilter.class).target("http://localhost:8080/loggingtest/resources/accounts");
Account acc = target.request().accept(MediaType.APPLICATION_JSON).get(account.Account.class);

And here's the implementation of MyLoggingFilter:

@Provider
public class MyLoggingFilter implements ClientRequestFilter, ClientResponseFilter {

    private static final Logger LOGGER = Logger.getLogger(MyLoggingFilter.class.getName());

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {

        LOGGER.log(Level.SEVERE, "Request method: {0}", requestContext.getMethod());

    }

    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
        LOGGER.log(Level.SEVERE, "Response status: {0}", responseContext.getStatus());
    }        
}
D00de
  • 880
  • 3
  • 7
  • 26

1 Answers1

47

So there are a couple things to consider when trying to implement this

  1. For the request entity, you will want the serialization to be handle by the framework, meaning you don't want to do something like

    @Override
    public void filter(ClientRequestContext requestContext) {
        Object entity = requestContext.getEntity();
        String serialized = serializeEntity(entity);
        log(serialized);
    

    Here you are serializing it yourself, maybe using Jackson ObjectMapper or something. You could do it this way, but it's kind of limited in the types it can handle. If you let the object be serialized the way it is already handled by the framework, the framework will be able to support many more types than just JSON.

    To let the framework handle the serialization, and still be able to get serialized data, we need to use a WriterInterceptor. What we can do is set the entity output stream to a ByteArrayOutputStream, and then let the framework serialize the request object to our ByteArrayOutputStream, then afterwords log those bytes. This is how the Jersey LoggingFilter handles this.

  2. For the response, in our filter, we need to extract the data from the response stream, but also we need to make sure the stream still has data, as it has not been deserialized yet for the client. To do this mark() and reset() the stream, assuming marking is supported. If not, wrap it in a BufferedOutputStream. Again, this is how the Jersey LoggingFilter handles this.

Below is a complete simple implementation. Most of it is taken straight from the Jersey LoggingFilter, though it is stripped down just for your use case. The Jersey LoggingFilter logs a lot of other information, aside from the just the entity. One thing I left out is the checking for the charset. I just used a hard coded UTF-8, as the MessageUtil class used by Jersey, is Jersey specific. If you want to make the filter more universal for other charsets, you may want to look into fixing that.

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
import javax.annotation.Priority;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;

@Priority(Integer.MIN_VALUE)
public class EntityLoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor {

    private static final Logger logger = Logger.getLogger(EntityLoggingFilter.class.getName());
    private static final String ENTITY_STREAM_PROPERTY = "EntityLoggingFilter.entityStream";
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private final int maxEntitySize = 1024 * 8;

    private void log(StringBuilder sb) {
        logger.info(sb.toString());
    }

    private InputStream logInboundEntity(final StringBuilder b, InputStream stream, final Charset charset) throws IOException {
        if (!stream.markSupported()) {
            stream = new BufferedInputStream(stream);
        }
        stream.mark(maxEntitySize + 1);
        final byte[] entity = new byte[maxEntitySize + 1];
        final int entitySize = stream.read(entity);
        b.append(new String(entity, 0, Math.min(entitySize, maxEntitySize), charset));
        if (entitySize > maxEntitySize) {
            b.append("...more...");
        }
        b.append('\n');
        stream.reset();
        return stream;
    }

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        if (requestContext.hasEntity()) {
            final OutputStream stream = new LoggingStream(requestContext.getEntityStream());
            requestContext.setEntityStream(stream);
            requestContext.setProperty(ENTITY_STREAM_PROPERTY, stream);
        }
    }

    @Override
    public void filter(ClientRequestContext requestContext,
            ClientResponseContext responseContext) throws IOException {
        final StringBuilder sb = new StringBuilder();
        if (responseContext.hasEntity()) {
            responseContext.setEntityStream(logInboundEntity(sb, responseContext.getEntityStream(),
                    DEFAULT_CHARSET));
            log(sb);
        }

    }

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
            throws IOException, WebApplicationException {
        final LoggingStream stream = (LoggingStream) context.getProperty(ENTITY_STREAM_PROPERTY);
        context.proceed();
        if (stream != null) {
            log(stream.getStringBuilder(DEFAULT_CHARSET));
        }
    }

    private class LoggingStream extends FilterOutputStream {

        private final StringBuilder sb = new StringBuilder();
        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        LoggingStream(OutputStream out) {
            super(out);
        }

        StringBuilder getStringBuilder(Charset charset) {
            // write entity to the builder
            final byte[] entity = baos.toByteArray();

            sb.append(new String(entity, 0, entity.length, charset));
            if (entity.length > maxEntitySize) {
                sb.append("...more...");
            }
            sb.append('\n');

            return sb;
        }

        @Override
        public void write(final int i) throws IOException {
            if (baos.size() <= maxEntitySize) {
                baos.write(i);
            }
            out.write(i);
        }
    }
}

See Also:

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Works great. I just needed to annotate filter as @Provider and register it on my Client and it logs JSON from POSTed Entity. BTW, I'm using JAX-B to serialize Entities. Thanks for your time fine sire, have my upvote and acceptance. – D00de Apr 17 '16 at 19:36
  • Great for debugging! – Ivan Aranibar Mar 09 '17 at 19:01
  • I tried to make a subclass of this to override the maxEntitySize because of a client that returns a ton of data. However, when I register the subclass instead of the `EntityLoggingFilter` I get a NullPointerException on `Response response = target.request(MediaType.APPLICATION_JSON).post(Entity.json(path));`. Is there any obvious reason why passing a subclass wouldn't work? It is not even calling my subclass constructor, and the NPE is happening at `at org.jboss.resteasy.core.ConstructorInjectorImpl.(ConstructorInjectorImpl.java:45) [resteasy-jaxrs-3.0.10.Final.jar:]` – Paul Jul 05 '19 at 04:59
  • Update: this seems to be an issue using an inner class, and then accidentally making the constructor not public when I made it a regular class. – Paul Jul 05 '19 at 05:29
  • 1
    Instead of `stream.read(entity)` in `logInboundEntity` you should use `byte[] entity = stream.readNBytes(maxEntitySize + 1);` introduced in Java 11. In theory `InputStream.read(byte[])` could read to little bytes. There is no guarantee that it will read until the array is full or the stream is empty. `InputStream.readNBytes(int)` does offer that guarantee. – Adrodoc May 02 '22 at 19:25