Creating an Audit Log in Rails

February 02, 2009

Someone just asked how to track changes via an audit log in Rails. They wanted more than just model observers however—they wanted to track the logged in user and some other session related data. I recently did something just like this.

Create an AuditLog model with the following schema:

create_table "audit_logs", :force => true do |t|
    t.string   "author"
    t.string   "summary"
    t.text     "changes"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

Setup the AuditLog model as below. This simply adds some code to not log a record if nothing was actually changed (convenient if your show view is also your edit view and your users hit ‘update’ to go back).

class AuditLog < ActiveRecord::Base
  attr_accessor :force
  serialize :changes

  before_create :skip_unchanged

  def skip_unchanged
    return false if read_attribute(:changes).blank? && force != true
    return true
  end
  private :skip_unchanged
end

Create the TrackChanges module and put it into RAILS_ROOT/lib/track_changes.rb:

module TrackChanges

  def self.included(base)
    base.after_save :track_changes
  end

  #
  #
  #
  def track_changes
    @tracked_changes = self.changes.reject{|k,v| ['created_at', 'updated_at'].include?(k.to_s)}
  end
  private :track_changes

  #
  #
  #
  def tracked_changes
    @tracked_changes
  end

end

In all of the models that you want to track, put the following line after any existing before filters:

include TrackChanges # must follow any before filters

Now, anywhere you want (in a controller’s create() or update() methods, in a method called via script/runner) you could do something like this (pretend we’re in a update method):

#...
@logged_in_user = User.find(....)
@something = Something.find(params[:id])
if @something.update_attributes(params[:something])
  AuditLog.create(:author => @logged_in_user.login, :summary => "created some thing '#{@something.name}'", :changes => @something.tracked_changes)
  #...
end
#...

One nice thing about not tying the audit log directly to a user (although you certainly could) is that system scripts can set an author of “system” or some other moniker that clearly indicates it’s a system process.

All that’s left is to build out a UI to review the records and purge old ones…