Quick & Dirty Auditing for Rails

By ozzyaaron

Friday, Aug 21, 2015

Why?

Auditing is one of those things that most businesses really should have built into every one of their IT systems but it’s often seen as a nice-to-have that you will-never-have. Yet in every business I’ve worked at that has a web app you inevitably hit a problem where everyone wished there had been a trail of what had been done. As soon as you have any other person than the owner updating a record, you really should invest a little in auditing your data.

Let’s talk about doing a little auditing.

audit-phonebox

Limiting Scope

It’s important to note that this solution is not for everything in your system though there’s no reason you couldn’t easily extend this to do so. There are other solutions out there that are built to do auditing for every ActiveRecord class but I’m hesitant to jump into a turn-key solution by someone else for something as simple as Auditing.

Also important is that this is not a ‘scalable’ solution. It will suffice for our use case but I’d never want to count on this being our forever-solution.

I feel like this solution is in the ethos of Rails by keeping control inside your application rather than offloading it to your database. For that reason it should be easy to find and reason about by anyone on your development team.

SQL & Message Buses Can Do It!

If you’re reading this you might’ve read articles that talk about using database triggers to create an audit log. I think the database solution falls somewhere between a real, scalable solution and what I’m presenting here. The reason I don’t like database magic in Rails apps is because it’s often out of sight and out of mind for developers - especially in a small team. Personally I’d always skip auditing by database triggers and move straight to a message bus type of solution. The other benefits to using a shared bus to transfer messages (eg writing workers to email, real time updates, etc) and the small leap in complexity to use a bus makes the trigger based solution seem less appealing to me. Having written a couple of auditing solutions using RabbitMQ, Ruby and Postgres I can say I’d always tend toward message buses if you have the technical team capable of managing it.

Solution Time

Given this blog post definitely took longer to write than the solution lets just get it out there!

There are 3 pieces to the puzzle:

  1. The logic that does the auditing
  2. The audit class itself
  3. The auditable objects

The Logic

This is a very simple module you mixin

module Auditable
  extend ActiveSupport::Concern

  class SystemUser < User
    def id
      0
    end
  end

  included do
    attr_accessor :updated_by

    has_many :audits, as: :auditable

    # One concern here is the ordering of callbacks ...
    after_save :create_audit
  end

  private

  def normalized_user
    updated_by || SystemUser.new
  end

  def create_audit
    Audit.create!(auditable: self, user: normalized_user)
  end
end

The Audit Class

class Audit < ActiveRecord::Base
  belongs_to :auditable, polymorphic: true
  belongs_to :user

  validates :auditable, presence: true
  validates :user, presence: true
end

The Auditable Classes

class TravelDirection < ActiveRecord::Base
  include Auditable
end

Seriously Folks, That’s All

You’ll notice the caveat in the Auditable module about callback order. If you’re a Rails aficionado then you know that callbacks just happen in the order they’ve been specified which might be a concern given we’re adding it at module inclusion time. Now if you’re writing nice callbacks then this shouldn’t be a problem; why mutate a saved object and save again?! We have a pretty reasonable app in this regard as we’re not doing anything much out of band, certainly someone writing such a callback would be slapped on the wrist. Auditing after the object you’re auditing is saved should be safe. If not then it’s trivial to modify the logic to give the developer more control over it.

In the end I think this took about 15 minutes to write and manually test. It was a spike but unit tests would be trivial to write and this solution will work for us for the foreseeable future.

Hopefully this helps someone, even if you’re just writing a little audit trail for your tiny new business… which as I explained you most certainly should!