Introducing Chronicler

Just a few weeks ago the company I work for, Analyte Health, open sourced one of the tools we’ve developed. We had found ourselves routinely needing a way to accurately record the history of particular objects in our systems. Now obviously, there are a bunch of existing tools that could have handled this, but none of them seemed to suit our needs especially well. All of the other tools do a fantastic job of auditing changes to an object, but none of them do an especially good job handling the changes to relationships of that object. Our very specific need was to record the changes on a ManyToMany join table. We have a permission system that we use to assign various rights to all fifty states, so we needed to track when permissions were changed on any particular state. This is where we felt that other tools fell short, and thus created chronicler.

Most of the other tools that are out there are deprecated and no longer maintained. The others we tested didn’t seem to provide the consistent behavior we felt we needed. To be fair, the various other tools out there try to be much more flexible than we were with chronicler. We had the luxury of knowing that the objects we wanted to track only had one or two places where they could be modified. Thanks to that, we opted to go the route of creating a decorator that we could slap on views that enable users to modify our objects. It works like so:

1
2
3
4
5
from chronicler.decorators import audits

@audits(YourModel, ['relation_set', 'another_set'], 'pk', 'incoming_pk', 'POST')
def your_update_view(request):
    # modifications

The first argument is the model class that we want to keep our watchful eyes on. After that, we provide a list of relations that we need to track changes across. We’ll actually end up keeping full dictionary representations of the related objects that we pass along to chronicler. Following that is the field we should use to look up the object before processing the view. The ‘incoming_pk’ argument is the key we want to inspect for a value we’ll use with the previous argument to look up our object. Finally, we finish up by telling the decorator if we should look in the GET or POST of the request object for our ‘incoming_pk’ value. Optionally, you can also pass “force=True”, which will force an AuditItem to be created even if there aren’t any detectable changes. The decorator will then take all of that information, and get to work.

The way it ends up functioning is the decorator grabs a copy of the object before the view is executed. After the view is executed, we fire a custom signal, passing along our now stale object along with request.user so we know who made the changes. From there, chronicler catches the signal and saves a JSON version of the stale object in an AuditItem object. Before we create that JSON representation though, we first verify that there are actually changes. If there aren’t any changes we move on, but as mentioned above you can force AuditItems to be created even if there aren’t any changes.

After a view is processed, and you have your freshly created AuditItem you can access it like so:

1
2
3
4
from chronicler.models import AuditItem
audit_item = AuditItem.objects.filter(content_object=your_object).latest()
print audit_item.audit_data
{u'state_id': 19, u'state_code': u'ME', u'statepermission_set': [{u'created':...

“audit_data” is a @property on AuditItem objects that returns the dictionary representation of the object. Arguably, it would have been worthwhile to use one of the various JSONField options out there, but this @property in conjunction with a TextField suited our needs. Who knows, we still actively use it and hope to improve it over time, maybe we’ll make that change if it makes sense.

We hope that it helps somebody out there. We can’t possibly be the only group that ran into needs such as ours. For further updates, information, and install instructions just visit the GitHub Repo!

Comments