Logging Jersey Requests and Responses in Mule

By | July 26, 2015
Reading Time: 14 minutes

The carpentry-season is not quite over yet, but I miss blogging so here is a little something written during a couple of rainy days.

In this article I will show how to add logging of requests and responses to/from a RESTful webservice that uses Jersey and runs in a Mule application.
As a side-effect and since I want to show a complete example program, I will also show how to create Mule projects using the Mule application Maven archetype and the basics of developing a Jersey RESTful web service in Mule.
In this article I will use the Mule 3.7.0 Community Edition, Jersey 2.11 and REST-assured 2.4.1, to mention the more prominent frameworks.

Configure Maven

Before being able to use the Mule application archetype you need to add the Mule repositories to your Maven settings file (settings.xml). One of the ways this is accomplished is described below.

  • Add the following profile containing the Mule repositories inside the <profiles> element:
...
    <profile>
        <id>mule-extra-repos</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <repositories>
            <repository>
                <id>mule-public</id>
                <url> https://repository.mulesoft.org/nexus/content/repositories/public </url>
            </repository>
        </repositories>
        <pluginRepositories>
            <pluginRepository>
                <id>mule-public</id>
                <url> https://repository.mulesoft.org/nexus/content/repositories/public </url>
            </pluginRepository>
        </pluginRepositories>
     </profile>
...
  •  Add the following XML element inside the <pluginGroups> element:
<pluginGroup>org.mule.tools</pluginGroup>

Create a Mule Project

Since I have abandoned Eclipse and AnypointStudio in favour for IntelliJ IDEA, I will use the Mule Maven archetype to create the example project. Eclipse and NetBeans are also able to import Maven projects so this is really the most universal way to create a Mule application project regardless of which IDE you use.

  • Open a terminal window.
  • Go to the directory in which you want the example project to be created.
  • Create the example project using the following command:
mvn archetype:generate -DarchetypeGroupId=org.mule.tools.maven -DarchetypeArtifactId=maven-archetype-mule-app -DarchetypeVersion=1.1 -DgroupId=se.ivankrizsan.mule -DartifactId=jersey-logging-example -Dversion=1.0.0-SNAPSHOT -DmuleVersion=3.7.0 -Dpackage=se.ivankrizsan.mule.jerseylogging -Dtransports=vm -Dmodules=xml,jersey,json,http
  • In order for the proper JDK to be assigned to the project, edit the pom.xml file in the new project and add the following plug-in to the <build> section:
    <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <source>${jdk.version}</source>
            <target>${jdk.version}</target>
        </configuration>
    </plugin>
  •  In the <dependencies> section, add the following test dependency to REST-assured, which will be used in the example project to test the RESTful webservice:
<!-- For testing of the RESTful web services. -->
<dependency>
    <groupId>com.jayway.restassured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>2.4.1</version>
   <scope>test</scope>
</dependency>

The project is now ready to be imported into the IDE of choice.

Log4J Configuration Files

Logging is in the title of this article, so we will need a Log4J2 configuration file for the Mule application. The reason for using Log4J2 is, for those not aware of this, that it is the logging framework of choice for the Mule ESB.

  • In src/main/resources, create a file named log4j2.xml with the following contents:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %-5p %-30C - %m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.mule" level="info"/>
        <Logger name="org.springframework" level="info"/>
        <Logger name="se.ivankrizsan.mule" level="info"/>
        <Logger name="org.mule.module.jersey" level="info"/>
        <!--
            Enable and disable the Jersey logging filter by setting log level
            to debug (enabled) or info (disabled). Log levels higher than
            info also disables logging.
        -->
        <Logger name="se.ivankrizsan.mule.jerseylogging.IvansJerseyLoggingFilter" level="debug"/>

        <Root level="info">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>
  • In src/test/resources, create a file named log4j2-test.xml with the same contents as the log4j2.xml file created above.

Note that:

  • There is a logger with the name attribute set to “se.ivankrizsan.mule.jerseylogging.IvansJerseyLoggingFilter”.
    Setting its level to “debug” will enable logging from the Jersey logger class we will implement later. Setting its level to “info” or higher will disable logging.

Entity Class

The REST service implemented in this example will allow for managing of customers. Our service thus need a Customer entity class.

  • Create the Customer class in src/main/java, in the package declared below:
package se.ivankrizsan.mule.jerseylogging.entities;

import javax.xml.bind.annotation.XmlRootElement;

/**
 * Customer entity class.
 *
 * @author Ivan krizsan
 */
@XmlRootElement
public class Customer {
    protected Long id;
    protected String firstName;
    protected String lastName;
    protected String streetName;
    protected String city;

    public Long getId() {
        return id;
    }

    public void setId(final Long inId) {
        id = inId;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(final String inFirstName) {
        firstName = inFirstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(final String inLastName) {
        lastName = inLastName;
    }

    public String getStreetName() {
        return streetName;
    }

    public void setStreetName(final String inStreetName) {
        streetName = inStreetName;
    }

    public String getCity() {
        return city;
    }

    public void setCity(final String inCity) {
        city = inCity;
    }
}

Note that:

  • The class is annotated with @XmlRootElement.
    This is in order for JAXB to be able to create an XML representation of the entity.
  • There are no annotations related to creating a JSON representation of the entity.
    Jackson, which is the library responsible to create JSON representations, does not need any annotations to create JSON representations. You can use Jackson annotations to customize creation of JSON representations, but I leave that for a later article.

REST Resource Test Class

In order to be able to conveniently assess our progress we implement a test of the REST resource class that will be implemented later. The test class only contains one single test, but again this article is about logging requests and responses to/from REST web services implemented using Jersey and one test which sends data to and receives data from the service is enough in order for us to be able to view the results of the logging.

  • In src/test/java, in the appropriate package, implement the class CustomerResourceTest as below:
package se.ivankrizsan.mule.jerseylogging.resources;

import com.jayway.restassured.RestAssured;
import com.jayway.restassured.http.ContentType;
import org.junit.Before;
import org.junit.Test;
import org.mule.tck.junit4.FunctionalTestCase;

/**
 * Tests the REST {@code CustomerResource}.
 *
 * @author Ivan Krizsan
 */
public class CustomerResourceTest extends FunctionalTestCase
{
    /* Constant(s): */
    protected static final int RESOURCE_ENDPOINT_PORT = 8083;
    protected static final String RESOURCES_BASE_PATH = "/resources/v100";
    protected final static String NEW_CUSTOMER_JSON =
        "{\"firstName\":\"Banana\",\"lastName\":\"Chippy\"," +
        "\"streetName\":\"Jolly Street 42\",\"city\":\"Monkey City\"}";

    @Override
    protected String getConfigFile()
    {
        return "src/main/app/mule-config.xml";
    }

    /**
     * Sets up before each test method.
     */
    @Before
    public void setUp() {
        RestAssured.reset();
        RestAssured.port = RESOURCE_ENDPOINT_PORT;
        RestAssured.basePath = RESOURCES_BASE_PATH;
    }

    /**
     * Tests creation of a new customer.
     * Expected result: HTTP status 200 and a response containing JSON data
     * should be returned.
     *
     * @throws Exception If error occurs. Indicates test failure.
     */
    @Test
    public void testCreateNewCustomer() throws Exception
    {
        RestAssured.
            given().
            contentType("application/json").
            accept("application/json").
            body(NEW_CUSTOMER_JSON).
            when().
            post(CustomersResource.CUSTOMERS_RESOURCE_PATH).
            then().
            statusCode(200).
            contentType(ContentType.JSON);
    }
}

Note that:

  • The test is a standard Mule test-case, inheriting from the FunctionalTestCase class.
  • In the setUp method, REST-assured is initialized.
    REST-assured is a very nice library that makes testing REST web services easy.
  • In the testCreateNewCustomer method the JSON representation of a new customer is sent to the service and the result is verified to be HTTP status 200 and containing JSON data.
  • The call to the post method in the testCreateNewCustomer method uses a constant in the CustomerResource class.
    Since we haven’t created the CustomerResource class yet, there will be a compilation error.

REST Resource Class

The customer entity has an accompanying resource class that exposes the RESTful service.

  • Implement the CustomerResource class in src/main/java like this:
package se.ivankrizsan.mule.jerseylogging.resources;

import se.ivankrizsan.mule.jerseylogging.entities.Customer;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.*;

/**
 * REST customers resource implemented using Jersey.
 *
 * @author Ivan Krizsan
 */
@Path(CustomersResource.CUSTOMERS_RESOURCE_PATH)
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public class CustomersResource {
    /* Constant(s): */
    public static final String CUSTOMERS_RESOURCE_PATH = "/customers";

    /* Instance variable(s): */
    /** Fake customer repository. */
    protected Map<Long, Customer> mCustomersRepository = new Hashtable<Long, Customer>();
    /** Customer id generator. */
    protected Random mCustomerIdGenerator = new Random();

    /**
     * Default constructor.
     * Creates one customer in the repository, as to have some data.
     */
    public CustomersResource() {
        final Customer theCustomer = new Customer();
        theCustomer.setId(1L);
        theCustomer.setFirstName("Dave");
        theCustomer.setLastName("Donkey");
        theCustomer.setCity("Paradise City");
        theCustomer.setStreetName("11 Downing Street");
        mCustomersRepository.put(1L, theCustomer);
    }

    /**
     * Retrieves all customers.
     *
     * @return Response containing all customers.
     */
    @GET
    public Response getAllCustomers() {
        final List<Customer> theCustomers = new ArrayList<Customer>();
        theCustomers.addAll(mCustomersRepository.values());
        /*
         * Need to return an array of entities in the response in order for
         * JAXB to be able to create an XML representation of the list.
         */
        final Customer[] theCustomersArray = theCustomers.toArray(
            new Customer[theCustomers.size()]);
        final Response theResponse =
            Response.ok().entity(theCustomersArray).build();
        return theResponse;
    }

    /**
     * Retrieves the customer with supplied id.
     *
     * @param id Id of customer to retrieve.
     * @return Response containing requested customer, or 404 if no such customer.
     */
    @GET
    @Path("{id}")
    public Response getCustomerById(@PathParam("id") final Long id) {
        final Response theResponse;
        final Customer theCustomer = mCustomersRepository.get(id);
        if (theCustomer == null) {
            theResponse = Response.status(Response.Status.NOT_FOUND).build();
        } else {
            theResponse = Response.ok().entity(theCustomer).build();
        }

        return theResponse;
    }

    /**
     * Deletes the customer with supplied id.
     *
     * @param id Id of customer to delete.
     * @return Response OK if customer deleted, or 404 if no such customer.
     */
    @DELETE
    @Path("{id}")
    public Response deleteCustomerById(@PathParam("id") final Long id) {
        final Response theResponse;
        final Customer theCustomer = mCustomersRepository.get(id);
        if (mCustomersRepository.containsKey(id)) {
            mCustomersRepository.remove(id);
            theResponse = Response.ok().entity(theCustomer).build();
        } else {
            theResponse = Response.status(Response.Status.NOT_FOUND).build();
        }

        return theResponse;
    }

    /**
     * Creates a new customer using the supplied customer data.
     * New customers must not have an id assigned.
     *
     * @param inCustomer Customer to create.
     * @return Response containing new customer, or bad request
     * if supplied customer had an id.
     */
    @POST
    public Response createCustomer(final Customer inCustomer) {
        final Response theResponse;

        /* New customers must not have an id. */
        if (inCustomer.getId() != null) {
            theResponse = Response.status(Response.Status.BAD_REQUEST).entity(
                    "New customers must not have an id").build();
        } else {
            Long theNewCustomerId;
            do {
                theNewCustomerId = Math.abs(mCustomerIdGenerator.nextLong());
            } while (mCustomersRepository.containsKey(theNewCustomerId));
            inCustomer.setId(theNewCustomerId);

            mCustomersRepository.put(theNewCustomerId, inCustomer);

            theResponse = Response.ok().entity(inCustomer).build();
        }

        return theResponse;
    }

    /**
     * Updates supplied customer setting its id to supplied id, overwriting any
     * existing customers with same id.
     *
     * @param inCustomer Customer data to write.
     * @param id Id of customer to update.
     * @return Response containing updated customer, or bad request if no id supplied.
     */
    @PUT
    @Path("{id}")
    public Response updateCustomer(final Customer inCustomer, @PathParam("id") Long id) {
        final Response theResponse;
        inCustomer.setId(id);

        mCustomersRepository.put(inCustomer.getId(), inCustomer);
        theResponse = Response.ok().entity(inCustomer).build();

        return theResponse;
    }
}

Note that:

  • The @Path annotation specify the relative path at which the resource is to be made available.
  • The @Produces annotation specify which representation types the resource should be able to produce.
    In this case I want to be able to retrieve both XML and JSON representations of customers.
  • The customers repository is a hashtable.
    A hashtable has been used to simplify the example but is of course not advisable for real REST resources.
  • There is a random number generator stored in the instance variable mCustomerIdGenerator.
    The random number generator is used to generate ids when creating new customers in the customer repository – something which the database will take care of when using a real repository.
  • In the constructor a customer is created and inserted into the repository.
    Again, this is not something you would find in a normal REST resource implementation class but something I put there to have some data in the repository for the sake of the example.
  • In the getAllCustomers method, an array of customers are placed in the response object.
    In its standard configuration, JAXB doesn’t quite like Java lists and is not able to serialize these into XML.
  • The remaining methods implement the basic CRUD functionality.
    I have chosen to use POST to create customers and PUT to update customers.

Mule Configuration File

A Mule configuration file named mule-config.xml is already present in the project at src/main/app.

  • Open the mule-config.xml file and replace its contents with the following XML data:
<?xml version="1.0" encoding="UTF-8"?>
<mule xmlns="http://www.mulesoft.org/schema/mule/core"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:spring="http://www.springframework.org/schema/beans"
      xmlns:http="http://www.mulesoft.org/schema/mule/http"
      xmlns:jersey="http://www.mulesoft.org/schema/mule/jersey"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

		http://www.mulesoft.org/schema/mule/core
		http://www.mulesoft.org/schema/mule/core/current/mule.xsd

		http://www.mulesoft.org/schema/mule/http
		http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd

		http://www.mulesoft.org/schema/mule/jersey
		http://www.mulesoft.org/schema/mule/jersey/current/mule-jersey.xsd">

    <!--
        HTTP listener for the new Mule HTTP transport that specifies which
        address and port to listen for requests to the REST service.
    -->
    <http:listener-config name="myHttpListener" host="0.0.0.0" port="8083"/>

    <spring:beans>
        <!-- Customer resource Spring bean. -->
        <spring:bean name="customerResource"
             class="se.ivankrizsan.mule.jerseylogging.resources.CustomersResource"/>
    </spring:beans>

    <flow name="restServiceFlow">
        <!--
            Specifies the base path of the resources exposed by this REST
            service. If the DNS name of the computer at which the service
            runs is ivankrizsan.se, then all resource URLs will start
            with https://ivankrizsan.se:8083/resources/v100.
        -->
        <http:listener config-ref="myHttpListener" path="resources/v100/*"/>
        <jersey:resources>
            <!--
                Each resource that is to be exposed is to have one <component>
                entry. A component entry can specify the resource
                implementation using either the class attribute on the
                <component> element or, like in the example below, a Spring bean.
            -->
            <component>
                <spring-object bean="customerResource"/>
            </component>
            <!--
                Tell Jersey where to look for classes implementing one or
                more extension interfaces and annotated with @Provider.
            -->
            <jersey:package packageName="se.ivankrizsan.mule.jerseylogging"/>
        </jersey:resources>
    </flow>
</mule>

Note that:

  • There is a <http:listener-config> element in which a host and a port is specified.
    This is similar to a Mule transport’s connector, but for the new Mule HTTP transport. The host is declared as 0.0.0.0, in order to be available to clients outside of the computer on which the service is run.
  • There is a Spring bean named customerResource declared.
    While it is not necessary to declare Spring beans for REST resources, I find it convenient if there are properties that need to be injected into the resource instances.
    If you do not want to create Spring beans for the REST resources, use the class attribute on the <component> element like in this example: <component class=”se.ivankrizsan.mule.jerseylogging.resources.CustomersResource”/>
  • The first element in the <flow> is a <http:listener> element.
    This element exposes an endpoint using the http listener configuration declared earlier at the path specified by the path attribute. Using the value “resources/v100/*” for the path results in the customer resource developed earlier (having the path “/customers”) will be exposed at http://0.0.0.0:8083/resources/v100/customers, where 0.0.0.0 will need to be replaced with the external IP address of the computer running the service if the client is not within the same computer.
  • For each REST resource that is to be exposed, one <component> element needs to be created inside the <jersey:resources> element.
  • There is a <jersey:package> element which specify the package “se.ivankrizsan.mule.jerseylogging”.
    This is how we tell Jersey where to look for classes that modify and/or extend Jersey when the Jersey webservices are deployed in the Mule ESB.
    Later in this article we will see an example of how to implement such a class.

We should now be able to run the CustomerResourceTest implemented earlier and the test should pass.

Logging with the Jersey LoggingFilter

In Jersey there are filters and interceptors that can be inserted into the path of requests and responses. Filters allow for access to entire requests and responses, while interceptors are more focused on the body of the message; be it an object or a stream. More information about Jersey filters and interceptors can be found here. Since we want to log all properties of requests and responses, this article will focus solely on Jersey filters.
The developers of Jersey does indeed provide a filter for logging to be found in the LoggingFilter class. Before we develop our own logging filter, lets try the Jersey logging filter..

  • In src/main/java, implement the OriginalJerseyLoggingFilter class:
package se.ivankrizsan.mule.jerseylogging;

import org.glassfish.jersey.filter.LoggingFilter;

import javax.ws.rs.ext.Provider;

/**
 * Class that makes it possible to use the Jersey {@code LoggingFilter} by placing this class in
 * in the application package specified by <jersey:package> in the Mule configuration file.
 *
 * @author Ivan Krizsan
 */
@Provider
public class OriginalJerseyLoggingFilter extends LoggingFilter {
}

As you can see this class contains nothing, it just extends the Jersey LoggingFilter class. However, note the @Provider annotation on the class.
The reason for creating this class and annotating it with @Provider is in order for the filter to be discovered by Jersey when we start Mule. Remember the configuration line in the mule-config.xml file that tells Jersey to automatically discover extension classes:

<jersey:package packageName="se.ivankrizsan.mule.jerseylogging"/>

If we now run the CustomerResourceTest, there should be output similar to this in the console:

Jul 26, 2015 4:34:42 PM org.glassfish.jersey.filter.LoggingFilter log
INFO: 1 * Server has received a request on thread myHttpListener.worker.01
1 > POST http://localhost:8083/resources/v100/customers
1 > accept: application/json
1 > accept-encoding: gzip,deflate
1 > connection: Keep-Alive
1 > content-length: 94
1 > content-type: application/json; charset=ISO-8859-1
1 > host: localhost:8083
1 > http.listener.path: /resources/v100/*
1 > http.method: POST
1 > http.query.params: ParameterMap{[]}
1 > http.query.string: 
1 > http.relative.path: /resources/v100/customers
1 > http.remote.address: /127.0.0.1:52202
1 > http.request.path: /resources/v100/customers
1 > http.request.uri: /resources/v100/customers
1 > http.scheme: http
1 > http.uri.params: ParameterMap{[]}
1 > http.version: HTTP/1.1
1 > user-agent: Apache-HttpClient/4.3.6 (java 1.5)

Jul 26, 2015 4:34:42 PM org.glassfish.jersey.filter.LoggingFilter log
INFO: 1 * Server responded with a response on thread myHttpListener.worker.01
1 < 200
1 < Content-Type: application/json

2015-07-26 16:34:42,913 INFO  org.mule.construct.FlowConstructLifecycleManager - Stopping flow: restServiceFlow

All but the last line, the line that has the message “Stopping flow: restServiceFlow”, are output from the Jersey LoggingFilter. If we examine the LoggingFilter class, we can see that it uses the Java default logging. In addition we can also note that all the methods that are not found in any of the interfaces the LoggingFilter class implements are private. This makes it difficult, if not impossible, to extend this class.

Creating a Logging Jersey Filter from Scratch

In order to have all the logging from my Mule application that uses Jersey write to the same log file, I set out to implement my own Jersey logging filter. In addition, I wanted to create something with a slightly higher degree of extensibility. As of writing this article, this filter is only for the server-side, since it implements the ContainerRequestFilter and ContainerResponseFilter interfaces.
If you want a logging filter for the client-side as well, implement the ClientRequestFilter and ClientResponseFilter interfaces too.

  • In src/main/java, implement the class IvansJerseyLoggingFilter as follows:
package se.ivankrizsan.mule.jerseylogging;

import org.apache.commons.lang.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Jersey logging filter that logs requests and responses to/from Jersey REST services.
 * Request and response payloads are also logged up to a configurable maximum size.
 * Logging is enabled and disabled by setting the log level for this class:
 * Log level DEBUG and below enable logging while a higher log level than DEBUG disable logging.
 *
 * This class implements the Jersey extension interfaces {@code ContainerRequestFilter} and
 * {@code ContainerResponseFilter} that allows us to gain access to requests before they arrive
 * at resources and responses before they are delivered to the clients.
 *
 * @author Ivan Krizsan
 */
@Provider
public class IvansJerseyLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter {
    /* Constant(s): */
    private static final Logger LOGGER = LoggerFactory.getLogger(IvansJerseyLoggingFilter.class);
    /** Maximum size of request/response body in bytes that will be logged. */
    protected static final int DEFAULT_MAX_BODY_SIZE = 20 * 1024;

    /* Instance variable(s): */
    protected int mMaxBodySize = DEFAULT_MAX_BODY_SIZE;

    @Override
    public void filter(final ContainerRequestContext inRequestContext) throws IOException {
        /* Do nothing if logging not enabled on debug level. */
        if (!isLoggingEnabled()) {
            return;
        }

        final StringBuilder theLogMessageBuilder = new StringBuilder();
        appendRequestInfoToMessageBuilder(inRequestContext, theLogMessageBuilder);

        /* Log message body only if there is one. */
        if (!isRequestBodyless(inRequestContext) && inRequestContext.hasEntity()) {
            theLogMessageBuilder.append("\nMessage body: \n");
            inRequestContext.setEntityStream(logMessageBodyInputStream(theLogMessageBuilder,
                inRequestContext.getEntityStream()));
        }

        logMessage(theLogMessageBuilder.toString());
    }

    @Override
    public void filter(final ContainerRequestContext inRequestContext, final ContainerResponseContext inResponseContext)
        throws IOException {
        /* Do nothing if logging not enabled on debug level. */
        if (!isLoggingEnabled()) {
            return;
        }

        final StringBuilder theLogMessageBuilder = new StringBuilder();
        appendResponseInfoToMessageBuilder(inResponseContext, inRequestContext, theLogMessageBuilder);

        logMessage(theLogMessageBuilder.toString());
    }

    /**
     * Determines if the request having the supplied request context has a body or not.
     *
     * @param inRequestContext Request context for request.
     * @return True if request has a body, false otherwise.
     */
    protected boolean isRequestBodyless(ContainerRequestContext inRequestContext) {
        final String theRequestHttpMethod = inRequestContext.getMethod();
        final boolean theBodylessFlag = ("GET".equals(theRequestHttpMethod))
                                        || ("DELETE".equals(theRequestHttpMethod))
                                        || ("HEAD".equals(theRequestHttpMethod));

        return theBodylessFlag;
    }

    /**
     * Appends information taken from the supplied request and response contexts to the supplied string builder, as
     * to create a log message.
     *
     * @param inResponseContext Response context to take information from.
     * @param inRequestContext Request context to take information from.
     * @param inLogMessageBuilder String builder used to create the log message.
     */
    protected void appendResponseInfoToMessageBuilder(final ContainerResponseContext inResponseContext,
        final ContainerRequestContext inRequestContext, final StringBuilder inLogMessageBuilder) {
        final String theMediaType = ObjectUtils.toString(inResponseContext.getMediaType(), "n/a");
        final String theHeaders = ObjectUtils.toString(inResponseContext.getHeaders(), "n/a");
        final String theStatus = ObjectUtils.toString(inResponseContext.getStatusInfo(), "n/a")
            + " (" + inResponseContext.getStatus() + ")";
        final String theRequestUri = requestUriFromRequestContext(inRequestContext);

        inLogMessageBuilder.append("Response from location: ");
        inLogMessageBuilder.append(theRequestUri);
        inLogMessageBuilder.append(", media type: ");
        inLogMessageBuilder.append(theMediaType);
        inLogMessageBuilder.append(", status: ");
        inLogMessageBuilder.append(theStatus);
        inLogMessageBuilder.append("\nHeaders: ");
        inLogMessageBuilder.append(theHeaders);
    }

    /**
     * Appends information taken from the supplied request context to the supplied string builder, as
     * to create a log message.
     *
     * @param inRequestContext Request context to take information from.
     * @param inLogMessageBuilder String builder used to create the log message.
     */
    protected void appendRequestInfoToMessageBuilder(final ContainerRequestContext inRequestContext,
                                                     final StringBuilder inLogMessageBuilder) {
        final String theRequestMethod = ObjectUtils.toString(inRequestContext.getMethod(), "n/a");
        final String theMediaType = ObjectUtils.toString(inRequestContext.getMediaType(), "n/a");
        final String theHeaders = ObjectUtils.toString(inRequestContext.getHeaders(), "n/a");
        final String theRequestUri = requestUriFromRequestContext(inRequestContext);

        inLogMessageBuilder.append("Received ");
        inLogMessageBuilder.append(theRequestMethod);
        inLogMessageBuilder.append(" request to URL ");
        inLogMessageBuilder.append(theRequestUri);
        inLogMessageBuilder.append(" with contents of type ");
        inLogMessageBuilder.append(theMediaType);
        inLogMessageBuilder.append(". \nHTTP headers: ");
        inLogMessageBuilder.append(theHeaders);
    }

    /**
     * Retrieves the absolute path request URI from the supplied request context.
     *
     * @param inRequestContext Request context from which to retrieve URI.
     * @return Request URI string or "n/a" if it is not available.
     */
    protected String requestUriFromRequestContext(final ContainerRequestContext inRequestContext) {
        final UriInfo theRequestUriInfo = inRequestContext.getUriInfo();
        String theRequestUrl = "n/a";
        if (theRequestUriInfo != null) {
            theRequestUrl = ObjectUtils.toString(theRequestUriInfo.getAbsolutePath(), "n/a");
        }
        return theRequestUrl;
    }

    /**
     * Logs the contents of the supplied input stream to the supplied string builder, as to produce
     * a log message.
     * If the length of the data in the input stream exceeds the maximum body size configured on the
     * logging filter then the exceeding data is not logged.
     *
     * @param inLogMessageStringBuilder String builder used to create log message.
     * @param inBodyInputStream Input stream containing message body.
     * @return An input stream from which the message body can be read from.
     * Will be the original input stream if it supports mark and reset.
     * @throws IOException If error occurs reading from input stream.
     */
    protected InputStream logMessageBodyInputStream(final StringBuilder inLogMessageStringBuilder,
        final InputStream inBodyInputStream) throws IOException {
        InputStream theResultEntityStream = inBodyInputStream;

        if (!inBodyInputStream.markSupported()) {
            theResultEntityStream = new BufferedInputStream(inBodyInputStream);
        }
        theResultEntityStream.mark(mMaxBodySize + 1);
        final byte[] theEntityBytes = new byte[mMaxBodySize + 1];
        final int theEntitySize = theResultEntityStream.read(theEntityBytes);
        inLogMessageStringBuilder.append(new String(theEntityBytes, 0, Math.min(theEntitySize, mMaxBodySize)));
        if (theEntitySize > mMaxBodySize) {
            inLogMessageStringBuilder.append(" [additional data truncated]");
        }
        theResultEntityStream.reset();
        return theResultEntityStream;
    }

    /**
     * Determines whether logging is enabled or not.
     * This method should be overridden by subclasses that wish to use some other way of logging than Log4J.
     *
     * @return True if this logging filter is to log requests and responses, false otherwise.
     */
    protected boolean isLoggingEnabled() {
        return LOGGER.isDebugEnabled();
    }

    /**
     * Logs the supplied message.
     * This method should be overridden by subclasses that wish to use some other way of logging than Log4J.
     *
     * @param inMessageToLog Message to log.
     */
    protected void logMessage(final String inMessageToLog) {
        LOGGER.debug(inMessageToLog);
    }
}

Note that:

  • If logging is not enabled, both the filter methods will immediately return without processing or changing anything.
  • The filter method that is invoked upon a request being received is the filter(ContainerRequestContext) method.
    This method will log some information about requests, such as which HTTP method was used, which content-type the message has and the HTTP headers. In addition, the message body, up to a certain size determined by the mMaxBodySize instance variable, will also be logged.
  • The other filter method that takes two context object parameters is the one that will be invoked to log responses.
    This method will log the HTTP status, the content-type as well as the HTTP headers of responses.
  • The methods isLoggingEnabled and logMessage are protected.
    This enables subclasses to replace the Log4J logging with some other logging framework or mechanism.

If we once again run the CustomerResourceTest, the new logging filter will produce output similar to this:

2015-07-26 17:30:35,431 DEBUG se.ivankrizsan.mule.jerseylogging.IvansJerseyLoggingFilter - Received POST request to URL http://localhost:8083/resources/v100/customers with contents of type application/json; charset=ISO-8859-1. 
HTTP headers: {http.version=[HTTP/1.1], accept-encoding=[gzip,deflate], connection=[Keep-Alive], http.remote.address=[/127.0.0.1:52797], http.uri.params=[ParameterMap{[]}], content-type=[application/json; charset=ISO-8859-1], http.relative.path=[/resources/v100/customers], http.listener.path=[/resources/v100/*], http.request.uri=[/resources/v100/customers], content-length=[94], http.method=[POST], http.query.params=[ParameterMap{[]}], host=[localhost:8083], user-agent=[Apache-HttpClient/4.3.6 (java 1.5)], accept=[application/json], http.scheme=[http], http.request.path=[/resources/v100/customers], http.query.string=[]}
Message body: 
{"firstName":"Banana","lastName":"Chippy","streetName":"Jolly Street 42","city":"Monkey City"}
2015-07-26 17:30:35,488 DEBUG se.ivankrizsan.mule.jerseylogging.IvansJerseyLoggingFilter - Response from location: http://localhost:8083/resources/v100/customers, media type: application/json, status: OK (200)
Headers: {Content-Type=[application/json]}
2015-07-26 17:30:35,632 INFO  org.mule.construct.FlowConstructLifecycleManager - Stopping flow: restServiceFlow

Again, the last line “Stopping flow: restServiceFlow” is not output from the logging filter.
This concludes this article and it is back to carpentry for me. The sourcecode for the example project is available on GitHub.

One thought on “Logging Jersey Requests and Responses in Mule

  1. Tito

    Hello Ivan,
    6.5 years after being written, this post proved quite useful to me today.
    Thanks a lot! Cheers from Uruguay

    Reply

Leave a Reply

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