For those who don’t know, Dropwizard is Java framework that allows you to build light-weight RESTful webservices. It comes with a few functionalities already configured and ready to use like an embedded Jetty container and the Jersey framework for building webservices. It also allows you to add more functionality as you need by importing or creating bundles. This framework is currently very popular among the Java community and it’s being used as an alternative of the much heavier enterprise options. The Thoughtworks Tech Radar shows it on the Adopt ring, meaning we promote and recommend its use. In our project we are using Dropwizard to build a set of microservices that are consumed by a single page app.
Although it has a really active community and it’s improving pretty fast, the framework is not mature yet and therefore there are some functionalities or requirements that are not available out of the box. One of them is the audit trail, that is, keeping a record of the transactions done by the users in the system that is not intrusive for them and respect their sensitive information. When we talk about webservices, we can try a few approaches:
- Log at the persistence layer. When there is a change in any table or entity you can trigger a procedure that will record the change. Hibernate has something called Envers that at first seems like an easy way to do this but we could not make it work as we were not using J2EE and the library is only available as part of the enterprise package . Another reason not to go down this path is that there doesn’t seem to be a way of separating the audit tables from the normal tables so the database can grow really quickly and there may be some performance issues related to that.
- Log using DB hooks or interceptors that get called when the DB is about to get modified.
- Log at the resource level. When a request came to the server, log the content of it as well as the additional information you need. Jersey allows you to easily register MethodDispatcher that will decorate your resources with new functionality. I did some research in the DW mailing list and found this gist, which gave the confirmation and inspiration we needed to follow this path.
Creating the audit bundle in DW
Creating a dispatcher is easy enough, our looks like this:
1 package me.mariagomez.dropwizard.audit;
2
3 import com.fasterxml.jackson.core.JsonProcessingException;
4 import com.fasterxml.jackson.databind.ObjectMapper;
5 import com.sun.jersey.api.core.HttpContext;
6 import com.sun.jersey.spi.dispatch.RequestDispatcher;
7 import io.dropwizard.jackson.Jackson;
8 import me.mariagomez.dropwizard.audit.providers.PrincipalProvider;
9 import org.joda.time.DateTime;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12
13 import static me.mariagomez.dropwizard.audit.Constants.X_REMOTE_ADDR;
14
15 public class AuditRequestDispatcher implements RequestDispatcher {
16
17 private static final ObjectMapper MAPPER = Jackson.newObjectMapper();
18 private static final Logger LOGGER = LoggerFactory.getLogger(AuditRequestDispatcher.class);
19
20 private RequestDispatcher dispatcher;
21 private AuditWriter auditWriter;
22 private PrincipalProvider principalProvider;
23
24 public AuditRequestDispatcher(RequestDispatcher dispatcher, AuditWriter auditWriter,
25 PrincipalProvider principalProvider) {
26 this.dispatcher = dispatcher;
27 this.auditWriter = auditWriter;
28 this.principalProvider = principalProvider;
29 }
30
31 @Override
32 public void dispatch(Object resource, HttpContext context) {
33 dispatcher.dispatch(resource, context);
34
35 int responseCode = context.getResponse().getStatus();
36 if (responseCode < 200 || responseCode > 299) {
37 return;
38 }
39 String method = context.getRequest().getMethod();
40 String path = context.getRequest().getPath();
41 String remoteAddress = context.getRequest().getRequestHeaders().getFirst(X_REMOTE_ADDR);
42 String username = principalProvider.getUsername();
43 DateTime date = DateTime.now();
44
45 String entity = null;
46 try {
47 entity = MAPPER.writeValueAsString(context.getResponse().getEntity());
48 } catch (JsonProcessingException e) {
49 LOGGER.error("Error while parsing the entity. \n Message: " + e.getMessage());
50 }
51
52 AuditInfo auditInfo = new AuditInfo(method, responseCode, date, entity, path, remoteAddress, username);
53 auditWriter.write(auditInfo);
54 }
55 }
As you can see, we let the dispatcher’s flow continue and we insert our logic after the request has been processed. This is because we only want to audit the successful requests. We also have a PrincipalProvider injected as a dependency, this gives us the info about the user making the request.
Next we attach this dispatcher to the resource by using a Provider:
1 package me.mariagomez.dropwizard.audit;
2
3 import com.sun.jersey.api.model.AbstractResourceMethod;
4 import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
5 import com.sun.jersey.spi.dispatch.RequestDispatcher;
6 import me.mariagomez.dropwizard.audit.providers.PrincipalProvider;
7
8 import java.util.Arrays;
9 import java.util.List;
10
11 public class AuditMethodDispatchProvider implements ResourceMethodDispatchProvider {
12
13 private static final List<String> AUDITABLE_METHODS = Arrays.asList("POST", "PUT", "DELETE");
14 private ResourceMethodDispatchProvider provider;
15 private AuditWriter auditWriter;
16 private PrincipalProvider principalProvider;
17
18 public AuditMethodDispatchProvider(ResourceMethodDispatchProvider provider, AuditWriter auditWriter,
19 PrincipalProvider principalProvider) {
20 this.provider = provider;
21 this.auditWriter = auditWriter;
22 this.principalProvider = principalProvider;
23 }
24
25 @Override
26 public RequestDispatcher create(AbstractResourceMethod abstractResourceMethod) {
27 RequestDispatcher requestDispatcher = provider.create(abstractResourceMethod);
28 if (isMethodAuditable(abstractResourceMethod.getHttpMethod())){
29 return new AuditRequestDispatcher(requestDispatcher, auditWriter, principalProvider);
30 }
31 return requestDispatcher;
32 }
33
34 private boolean isMethodAuditable(String method) {
35 return AUDITABLE_METHODS.contains(method);
36 }
37
38 }
In our case, we wanted the dispatcher to be attached to all resources with POST, PUT and DELETE verbs, but the gist I mentioned before shows how to do this by using an annotation instead.
Once we have all this in place, we just need to create the bundle so we can use this functionality within our application. Bundles in Dropwizard can have access to the configuration files so we took advantage of that to allow different writers (initially we only used the normal logs but you can link to a writer that send the data somewhere else).
1 package me.mariagomez.dropwizard.audit;
2
3 import io.dropwizard.Configuration;
4 import io.dropwizard.ConfiguredBundle;
5 import io.dropwizard.jersey.setup.JerseyEnvironment;
6 import io.dropwizard.setup.Bootstrap;
7 import io.dropwizard.setup.Environment;
8 import me.mariagomez.dropwizard.audit.filters.RemoteAddressFilter;
9 import me.mariagomez.dropwizard.audit.providers.PrincipalProvider;
10
11 public class AuditBundle<T extends Configuration> implements ConfiguredBundle<T> {
12
13 private AuditWriter auditWriter;
14 private PrincipalProvider principalProvider;
15
16 public AuditBundle(AuditWriter auditWriter, PrincipalProvider principalProvider) {
17 this.auditWriter = auditWriter;
18 this.principalProvider = principalProvider;
19 }
20
21 @Override
22 public void run(T configuration, Environment environment) {
23 JerseyEnvironment jersey = environment.jersey();
24 jersey.register(new AuditMethodDispatchAdapter(auditWriter, principalProvider));
25 jersey.getResourceConfig().getContainerRequestFilters().add(new RemoteAddressFilter());
26 }
27
28 @Override
29 public void initialize(Bootstrap<?> bootstrap) {
30 // Do nothing
31 }
32 }
Finally, you need to register the bundle in your application:
1 package me.mariagomez.dropwizard.audit.example;
2
3 import io.dropwizard.Application;
4 import io.dropwizard.setup.Bootstrap;
5 import io.dropwizard.setup.Environment;
6 import me.mariagomez.dropwizard.audit.AuditBundle;
7
8 public class ExampleApplication extends Application<ExampleConfiguration> {
9
10 @Override
11 public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
12 AuditBundle<ExampleConfiguration> bundle = new AuditBundle<>(new PrincipalProviderImpl());
13 bootstrap.addBundle(bundle);
14 }
15
16 @Override
17 public void run(ExampleConfiguration configuration, Environment environment) throws Exception {
18 environment.jersey().register(ExampleResource.class);
19 }
20 }
And you should be good to go!
PS: All the code plus an example is in this project on github. Contribution are more than welcome.