Rails audit system with both ActiveResource and ActiveRecord
Asked Answered
P

5

16

I have a huge project with both of ActiveRecord and ActiveResource models. I need to implement logging of user activity with these models and also to log changes of model attributes (save object state or somthing like that). Changes can made by users or cron rake tasks.

I also must have possibility to search any data by date , any field ..etc

Will be nice also to generate readable messages with last activity , for example

  • User Bob change his password to * and email to ** at 2011-08-12 08:12
  • Staff Jeff added new partner: Company name at 2011-08-12 08:13
  • Admin Jack deleted product : Product name at 2011-09-12 11:11
  • Client Sam ordered new service : Service name at 2011-09-12 11:12

Does anybody implement such logging? Ideas? Advices?

should I use gems or can I do all the logic with observers not changing models?


I liked gem https://github.com/airblade/paper_trail can anybody say how can I make it work with activeresource ?

Pronouncement answered 13/5, 2011 at 8:41 Comment(1)
Do you mean changing of data itself or changing or schema? These are two different things. My answer is targeted for the data...Bloomington
W
1

The acts_as_audited gem should work well for you:
https://github.com/collectiveidea/acts_as_audited

And as far as ActiveResource is considered, it will also be a model in some other application. You can use the gem at the server side, and you don't need to audit it at the client side. All the CRUD operations using ActiveResource would finally translate to CRUD operations on the ActiveRecord (on server side).

So probably you need to look at it from a distance, and the same solution would apply in both the cases, but at different places.

Wein answered 22/5, 2011 at 12:24 Comment(5)
if auditing will be both at the server side (Activeresource) and client side (ActiveRecord) how can I search, show last activity.. etc? I need one storage for all activities I thinkPronouncement
Again, it depends on at what side you wish to see the information. I haven't used the gem but I am certain that it would be storing the audit information in a table. So if you need it on the server side you can always create a model and access it using ActiveRecord, and if you need it on the client side you could create another ActiveResource object to access the audit information. You just need to see where the data is residing. If it's in the same place then you should use ActiveRecord and if not then use ActiveResource to access the information remotely.Wein
I need to store auditing information in client database without depending of it is ActiveResource or ActiveRecord, there is no possibility for me to change source code of other application which I communicate via ActiveResourcePronouncement
Each application should itself be responsible for maintaining and auditing it's data. What you are trying to do is, audit the data that you are not responsible for, or that does not exist at your end. This is my personal opinion, and for the record - I am no Rails guru.... ;)Wein
If you want to track certain actions on the client side, you can add a before_filter to the required controllers which stores the audit information such as "user, action, other_data" on client side in the audit model.Wein
B
4

You are looking for

https://github.com/collectiveidea/acts_as_audited

Few open source projects use that plugin I think Red Mine as well as The Foreman.

Edit: Unfortunately it can do only ActiveRecord, not ActiveResource.

Bloomington answered 13/5, 2011 at 8:45 Comment(1)
yeap me to =( some other ideas ?Pronouncement
C
4

Fivell, I just saw this question and don't have time to work up alterations this evening before the bounty expires, so I'll give you my auditing code that works with ActiveRecord and should work with ActiveResource, perhaps with a few tweaks (I don't use ARes often enough to know offhand). I know the callbacks we use are there, but I'm not sure if ARes has ActiveRecord's dirty attribute changes tracking.

This code logs each CREATE/UPDATE/DELETE on all models (excepting CREATEs on the audit log model and any other exceptions you specify) with the changes stored as JSON. A cleaned backtrace is also stored so you can determine what code made the change (this captures any point in your MVC as well as rake tasks and console usage).

This code works for console usage, rake tasks, and http requests, although generally only the last one logs the current user. (If I recall correctly, the ActiveRecord observer that this replaced did not work in rake tasks or the console.) Oh, this code comes from a Rails 2.3 app - I have a couple Rails 3 apps, but I haven't needed this kind of auditing for them yet.

I don't have code that builds a nice display of this information (we only dig into the data when we need to look into an issue), but since the changes are stored as JSON it should be fairly straightforward.

First, we store the current user in User.current so it is accessible everywhere, so in app/models/user.rb:

Class User < ActiveRecord::Base
  cattr_accessor :current
  ...
end

The current user is set in the application controller for each request like so (and does not cause concurrency issues):

def current_user
  User.current = session[:user_id] ? User.find_by_id(session[:user_id]) : nil
end

You could set User.current in your rake tasks if it made sense to.

Next, we define the model to store the audit info app/models/audit_log_entry.rb - you'll want to customize IgnoreClassesRegEx to fit any models you don't want audited:

# == Schema Information
#
# Table name: audit_log_entries
#
#  id         :integer         not null, primary key
#  class_name :string(255)
#  entity_id  :integer
#  user_id    :integer
#  action     :string(255)
#  data       :text
#  call_chain :text
#  created_at :datetime
#  updated_at :datetime
#

class AuditLogEntry < ActiveRecord::Base
  IgnoreClassesRegEx = /^ActiveRecord::Acts::Versioned|ActiveRecord.*::Session|Session|Sequence|SchemaMigration|CronRun|CronRunMessage|FontMetric$/
  belongs_to :user

  def entity (reload = false)
    @entity = nil if reload
    begin
      @entity ||= Kernel.const_get(class_name).find_by_id(entity_id)
    rescue
      nil
    end
  end

  def call_chain
    return if call_chain_before_type_cast.blank?
    if call_chain_before_type_cast.instance_of?(Array)
      call_chain_before_type_cast
    else
      JSON.parse(call_chain_before_type_cast)
    end
  end
  def data
    return if data_before_type_cast.blank?
    if data_before_type_cast.instance_of?(Hash)
      data_before_type_cast
    else
      JSON.parse(data_before_type_cast)
    end
  end

  def self.debug_entity(class_name, entity_id)
    require 'fastercsv'
    FasterCSV.generate do |csv|
      csv << %w[class_name entity_id date action first_name last_name data]
      find_all_by_class_name_and_entity_id(class_name, entity_id,
                                           :order => 'created_at').each do |a|
        csv << [a.class_name, a.entity_id, a.created_at, a.action, 
          (a.user && a.user.first_name), (a.user && a.user.last_name), a.data]
      end
    end
  end
end

Next we add some methods to ActiveRecord::Base to make the audits work. You'll want to look at the audit_log_clean_backtrace method and modify for your needs. (FWIW, we put additions to existing classes in lib/extensions/*.rb which are loaded in an initializer.) In lib/extensions/active_record.rb:

class ActiveRecord::Base
  cattr_accessor :audit_log_backtrace_cleaner
  after_create  :audit_log_on_create
  before_update :save_audit_log_update_diff
  after_update  :audit_log_on_update
  after_destroy :audit_log_on_destroy
  def audit_log_on_create
    return if self.class.name =~ /AuditLogEntry/
    return if self.class.name =~ AuditLogEntry::IgnoreClassesRegEx
    audit_log_create 'CREATE', self, caller
  end
  def save_audit_log_update_diff
    @audit_log_update_diff = changes.reject{ |k,v| 'updated_at' == k }
  end
  def audit_log_on_update
    return if self.class.name =~ AuditLogEntry::IgnoreClassesRegEx
    return if @audit_log_update_diff.empty?
    audit_log_create 'UPDATE', @audit_log_update_diff, caller
  end
  def audit_log_on_destroy
    return if self.class.name =~ AuditLogEntry::IgnoreClassesRegEx
    audit_log_create 'DESTROY', self, caller
  end
  def audit_log_create (action, data, call_chain)
    AuditLogEntry.create :user       => User.current,
                         :action     => action,
                         :class_name => self.class.name,
                         :entity_id  => id,
                         :data       => data.to_json,
                         :call_chain => audit_log_clean_backtrace(call_chain).to_json
  end
  def audit_log_clean_backtrace (backtrace)
    if !ActiveRecord::Base.audit_log_backtrace_cleaner
      ActiveRecord::Base.audit_log_backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
      ActiveRecord::Base.audit_log_backtrace_cleaner.add_silencer { |line| line =~ /\/lib\/rake\.rb/ }
      ActiveRecord::Base.audit_log_backtrace_cleaner.add_silencer { |line| line =~ /\/bin\/rake/ }
      ActiveRecord::Base.audit_log_backtrace_cleaner.add_silencer { |line| line =~ /\/lib\/(action_controller|active_(support|record)|hoptoad_notifier|phusion_passenger|rack|ruby|sass)\// }
      ActiveRecord::Base.audit_log_backtrace_cleaner.add_filter   { |line| line.gsub(RAILS_ROOT, '') }
    end
    ActiveRecord::Base.audit_log_backtrace_cleaner.clean backtrace
  end
end

Finally, here are the tests we have on this - you'll need to modify the actual test actions of course. test/integration/audit_log_test.rb

require File.dirname(__FILE__) + '/../test_helper'

class AuditLogTest < ActionController::IntegrationTest
  def setup
  end

  def test_audit_log
    u = users(:manager)
    log_in u
    a = Alert.first :order => 'id DESC'
    visit 'alerts/new'
    fill_in 'alert_note'
    click_button 'Send Alert'
    a = Alert.first :order => 'id DESC', :conditions => ['id > ?', a ? a.id : 0]
    ale = AuditLogEntry.first :conditions => {:class_name => 'Alert', :entity_id => a.id }
    assert_equal 'Alert',  ale.class_name
    assert_equal 'CREATE', ale.action
  end

private

  def log_in (user, password = 'test', initial_url = home_path)
    visit initial_url
    assert_contain 'I forgot my password'
    fill_in 'email',    :with => user.email
    fill_in 'password', :with => password
    click_button 'Log In'
  end

  def log_out
    visit logout_path
    assert_contain 'I forgot my password'
  end
end

And test/unit/audit_log_entry_test.rb:

# == Schema Information
#
# Table name: audit_log_entries
#
#  id         :integer         not null, primary key
#  class_name :string(255)
#  action     :string(255)
#  data       :text
#  user_id    :integer
#  created_at :datetime
#  updated_at :datetime
#  entity_id  :integer
#  call_chain :text
#

require File.dirname(__FILE__) + '/../test_helper'

class AuditLogEntryTest < ActiveSupport::TestCase
  test 'should handle create update and delete' do
    record = Alert.new :note => 'Test Alert'
    assert_difference 'Alert.count' do
      assert_difference 'AuditLogEntry.count' do
        record.save
        ale = AuditLogEntry.first :order => 'created_at DESC'
        assert ale
        assert_equal 'CREATE', ale.action, 'AuditLogEntry.action should be CREATE'
        assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
        assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
      end
    end
    assert_difference 'AuditLogEntry.count' do
      record.update_attribute 'note', 'Test Update'
      ale = AuditLogEntry.first :order => 'created_at DESC'
      expected_data = {'note' => ['Test Alert', 'Test Update']}
      assert ale
      assert_equal 'UPDATE', ale.action, 'AuditLogEntry.action should be UPDATE'
      assert_equal expected_data, ale.data
      assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
      assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
    end
    assert_difference 'AuditLogEntry.count' do
      record.destroy
      ale = AuditLogEntry.first :order => 'created_at DESC'
      assert ale
      assert_equal 'DESTROY', ale.action, 'AuditLogEntry.action should be CREATE'
      assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
      assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
      assert_nil Alert.find_by_id(record.id), 'Alert should be deleted'
    end
  end

  test 'should not log AuditLogEntry create entry and block on update and delete' do
    record = Alert.new :note => 'Test Alert'
    assert_difference 'Alert.count' do
      assert_difference 'AuditLogEntry.count' do
        record.save
      end
    end
    ale = AuditLogEntry.first :order => 'created_at DESC'
    assert_equal 'CREATE', ale.action, 'AuditLogEntry.action should be CREATE'
    assert_equal record.class.name, ale.class_name, 'AuditLogEntry.class_name should match record.class.name'
    assert_equal record.id, ale.entity_id, 'AuditLogEntry.entity_id should match record.id'
    assert_nil AuditLogEntry.first(:conditions => { :class_name => 'AuditLogEntry', :entity_id => ale.id })

    if ale.user_id.nil?
      u = User.first
    else
      u = User.first :conditions => ['id != ?', ale.user_id]
    end
    ale.user_id = u.id
    assert !ale.save

    assert !ale.destroy
  end
end
Caber answered 29/5, 2011 at 3:29 Comment(0)
A
3

https://github.com/collectiveidea/acts_as_audited

and

https://github.com/airblade/paper_trail

are both great solutions for ActiveRecord only, but since much of ActiveRecord has been extracted to ActiveModel, it's likely to be reasonable to extend either to support ActiveResource as well, at least for read-only support. I looked through the Github network graphs and googled around and there doesn't appear to be any ongoing development of such a solution, nevertheless I expect it will be easier to implement on top of one of these two plugins than starting from scratch. paper_trail appears to be under more active development and has some commits for Rails 3.1, so it may be more up to date with Rails internals and easier to extend, but that's just a gut instinct—I'm not familiar with the internals of either.

Americanism answered 24/5, 2011 at 21:46 Comment(0)
W
1

The acts_as_audited gem should work well for you:
https://github.com/collectiveidea/acts_as_audited

And as far as ActiveResource is considered, it will also be a model in some other application. You can use the gem at the server side, and you don't need to audit it at the client side. All the CRUD operations using ActiveResource would finally translate to CRUD operations on the ActiveRecord (on server side).

So probably you need to look at it from a distance, and the same solution would apply in both the cases, but at different places.

Wein answered 22/5, 2011 at 12:24 Comment(5)
if auditing will be both at the server side (Activeresource) and client side (ActiveRecord) how can I search, show last activity.. etc? I need one storage for all activities I thinkPronouncement
Again, it depends on at what side you wish to see the information. I haven't used the gem but I am certain that it would be storing the audit information in a table. So if you need it on the server side you can always create a model and access it using ActiveRecord, and if you need it on the client side you could create another ActiveResource object to access the audit information. You just need to see where the data is residing. If it's in the same place then you should use ActiveRecord and if not then use ActiveResource to access the information remotely.Wein
I need to store auditing information in client database without depending of it is ActiveResource or ActiveRecord, there is no possibility for me to change source code of other application which I communicate via ActiveResourcePronouncement
Each application should itself be responsible for maintaining and auditing it's data. What you are trying to do is, audit the data that you are not responsible for, or that does not exist at your end. This is my personal opinion, and for the record - I am no Rails guru.... ;)Wein
If you want to track certain actions on the client side, you can add a before_filter to the required controllers which stores the audit information such as "user, action, other_data" on client side in the audit model.Wein
W
1

for tracking user activity(CRUD ), i've created a class inherits from Logger, and now I am planing to write a litle plugin for tracking user that i can use for any ROR application built. I have already checked if there is a plugin like that but I didn’t see. I guess there are many gem like paper-trail, acts_as_audited or itslog but i prefer to use a plugin. Any suggestions? Here is a link that might help you : http://robaldred.co.uk/2009/01/custom-log-files-for-your-ruby-on-rails-applications/comment-page-1/#comment-342

nice coding

Welbie answered 29/6, 2011 at 8:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.