Quick & Dirty Auditing for Rails
Friday, Aug 21, 2015
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.
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.
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:
- The logic that does the auditing
- The audit class itself
- The auditable objects
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!