Skip to content

Observing and logging with paper_trail

denispeplin edited this page Dec 29, 2012 · 3 revisions

While observing models, it sometimes can be handy to have some information about a person, who made an action, or any other additional information available on the controller level.

PaperTrail usually has that information, and it can be obtained by calling PaperTrail.whodunnit or PaperTrail.controller_info.

Example:

# app/models/version_observer.rb
class VersionObserver < ActiveRecord::Observer
  observe :client, :contract # observing multiple models

  # observe only #create actions here
  def after_create(record)
    # do something useful here instead
    Rails.logger.error "#{PaperTrail.whodunnit.name} created #{record}"
  end
end
# app/config/application.rb
config.active_record.observers = :version_observer

Note that observers are unavailable by default in Rails4, but can be added using rails-observers gem.

Observing with paper_trail can be used to store logs in a separate table. The versions table can grow large, so you have to squeeze it from time to time, but you can store just event names and only for a few objects in a small separate table.

In this example is assumed that PaperTrail stores and returns user_id as whodunnit.

# app/models/version_observer.rb
class VersionObserver < ActiveRecord::Observer
  observe :client, :contract

  def after_create(record)
    create_event_log(record, PaperTrail.whodunnit, 'create')
  end

  # no metaprogramming here, just copy'n'paste, so code readability kept safe
  def after_update(record)
    create_event_log(record, PaperTrail.whodunnit, 'update')
  end

  # copy'n'paste again
  def after_destroy(record)
    create_event_log(record, PaperTrail.whodunnit, 'destroy')
  end

  private

  def create_event_log(event_source, user, event)
    # user sometimes can be empty
    # PaperTrail can be disabled in test
    if PaperTrail.enabled? && user
      # most straightforward way, paper_trail implementation-independent
      version = event_source.versions.last
      EventLog.create(user: user, event_source_type: event_source.class.to_s,
        event_source_id: event_source.id, event: event, version_id: version.id)
    end
  end
end

The EventLog is polymorphic model:

# app/models/event_log.rb
class EventLog < ActiveRecord::Base
  attr_accessible :generated, :message, :user, :event_source_type,
    :event_source_id, :event, :version_id
  belongs_to :event_source, polymorphic: true
  belongs_to :user
end

The migration:

# db/migrate/<some_digits_here>_create_event_logs.rb
class CreateEventLogs < ActiveRecord::Migration
  def change
    create_table :event_logs do |t|
      t.integer :event_source_id
      t.string  :event_source_type
      t.integer :user_id
      t.integer :version_id

      t.timestamps
    end
    add_index :event_logs, [:event_source_id, :event_source_type]
    add_index :event_logs, :user_id
    add_index :event_logs, :version_id
  end
end

You can link related models to the EventLog.

# app/models/client.rb
class Client < ActiveRecord::Base
  has_many :event_logs, as: :event_source
end

# app/models/contract.rb
class Contract < ActiveRecord::Base
  has_many :event_logs, as: :event_source
end

# app/models/user.rb
class User < ActiveRecord::Base
  # not polymorphic
  has_many :event_logs
end

You can even export past events from the versions table to the event_logs table. To keep migrations safe, here it is done in SQL.

# db/migrate/<some_digits_here>_export_versions_to_event_logs.rb
class ExportVersionsToEventLogs < ActiveRecord::Migration
  def up
    execute "INSERT INTO event_logs (version_id, event_source_type, event_source_id,
      user_id, created_at, updated_at) SELECT v.id, v.item_type AS event_source_type,
      v.item_id AS event_source_id, cast(v.whodunnit as integer) AS user_id,
      v.created_at AS created_at, v.created_at AS updated_at FROM versions v WHERE
      v.item_type IN ('Client', 'Contract') AND v.event IN ('create', 'update', 'destroy');"
  end

  def down
    # probably unsafe. some additional attribute can be used to fix this
    # i.e. event_logs.generated, use it in INSERT above, and then execute
    # execute "DELETE FROM event_logs WHERE generated"
  end
end