-
Notifications
You must be signed in to change notification settings - Fork 902
Observing and logging with paper_trail
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