Система аудита Rails с ActiveResource и ActiveRecord - PullRequest
17 голосов
/ 13 мая 2011

У меня огромный проект с моделями ActiveRecord и ActiveResource. Мне нужно реализовать ведение журнала активности пользователя с этими моделями, а также для регистрации изменений атрибутов модели (сохранить состояние объекта или что-то подобное). Изменения могут быть сделаны пользователями или задачами cron rake.

У меня также должна быть возможность поиска любых данных по дате, любому полю .. и т.д.

Будет также хорошо генерировать читаемые сообщения с последней активностью, например

  • Пользователь Боб изменит свой пароль на * и отправит электронное письмо на ** в 2011-08-12 08:12
  • Персонал Джефф добавил нового партнера: название компании на 2011-08-12 08: 13
  • Администратор Джек удалил продукт: Название продукта на 2011-09-12 11: 11
  • Клиент Сэм заказал новую услугу: Наименование услуги на 2011-09-12 11: 12

Кто-нибудь осуществляет такую ​​регистрацию? Идеи? Советы?

я должен использовать драгоценные камни или я могу делать всю логику с наблюдателями, не меняющими модели?


Мне понравился гем https://github.com/airblade/paper_trail Кто-нибудь может сказать, как я могу заставить его работать с activeresource?

Ответы [ 6 ]

4 голосов
/ 29 мая 2011

Fivell, я только что увидел этот вопрос, и у меня нет времени на внесение изменений сегодня вечером до истечения срока действия награды, поэтому я дам вам свой код аудита, который работает с ActiveRecord и должен работать с ActiveResource, возможно, с несколькими твики (я не использую ARes достаточно часто, чтобы знать не по порядку). Я знаю, что обратные вызовы, которые мы используем, существуют, но я не уверен, что у ARes есть грязный атрибут ActiveRecord changes tracking.

Этот код регистрирует каждое CREATE / UPDATE / DELETE на всех моделях (за исключением CREATE в модели журнала аудита и любых других указанных вами исключений) с изменениями, сохраненными в формате JSON. Очищенная обратная трассировка также сохраняется, чтобы вы могли определить, какой код внес изменения (при этом фиксируется любая точка в вашем MVC, а также задачи rake и использование консоли).

Этот код работает для использования консоли, задач rake и http-запросов, хотя, как правило, только последний регистрирует текущего пользователя. (Если я правильно помню, наблюдатель ActiveRecord, заменивший эту замену, не работал в задачах rake или на консоли.) О, этот код взят из приложения Rails 2.3 - у меня есть пара приложений Rails 3, но мне этот вид не нужен одитинга для них пока нет.

У меня нет кода, который бы хорошо отображал эту информацию (мы копаем данные только тогда, когда нам нужно разобраться в проблеме), но поскольку изменения хранятся в формате JSON, это должно быть довольно просто.

Во-первых, мы сохраняем текущего пользователя в User.current, чтобы он был доступен везде, поэтому в app/models/user.rb:

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

Текущий пользователь устанавливается в контроллере приложения для каждого запроса следующим образом (и не вызывает проблем с параллелизмом):

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

Вы можете установить User.current в своих заданиях на рейк, если это имеет смысл.

Далее мы определяем модель для хранения информации аудита app/models/audit_log_entry.rb - вам нужно настроить IgnoreClassesRegEx, чтобы она подходила для любых моделей, которые вы не хотите проверять:

# == 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

Затем мы добавим несколько методов к ActiveRecord::Base, чтобы аудиты работали. Вы захотите взглянуть на метод audit_log_clean_backtrace и изменить его для своих нужд. (FWIW, мы помещаем дополнения к существующим классам в lib/extensions/*.rb, которые загружаются в инициализатор.) В 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

Наконец, вот тесты, которые у нас есть по этому вопросу - вам, конечно, нужно будет изменить действительные действия теста. 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

И 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
4 голосов
/ 13 мая 2011

Вы ищете

https://github.com/collectiveidea/acts_as_audited

Немногие проекты с открытым исходным кодом используют этот плагин, я думаю Red Mine , а также The Foreman .

Редактировать : К сожалению, он может выполнять только ActiveRecord, но не ActiveResource.

3 голосов
/ 25 мая 2011

https://github.com/collectiveidea/acts_as_audited

и

https://github.com/airblade/paper_trail

являются отличными решениями только для ActiveRecord, но поскольку большая часть ActiveRecord была извлечена для ActiveModel, вероятно, целесообразно расширить либо на поддержку ActiveResource, по крайней мере для поддержки только для чтения.Я просматривал сетевые графики Github и гуглил вокруг, и похоже, что нет никакого дальнейшего развития такого решения, тем не менее, я ожидаю, что будет проще реализовать поверх одного из этих двух плагинов, чем начинать с нуля.paper_trail, кажется, находится в стадии более активной разработки и имеет некоторые коммиты для Rails 3.1, поэтому он может быть более современным с внутренними компонентами Rails и его легче расширять, но это всего лишь инстинкт инстинкта - я не знаком с внутренними компонентамилибо.

1 голос
/ 29 июня 2011

для отслеживания активности пользователей (CRUD), я создал класс, унаследованный от Logger, и теперь я планирую написать небольшой плагин для отслеживания пользователей, который я могу использовать для любого созданного приложения ROR. Я уже проверил, есть ли такой плагин, но я не видел. Я думаю, что есть много драгоценных камней, таких как paper-trail, acts_as_audited или itslog, но я предпочитаю использовать плагин. Какие-либо предложения? Вот ссылка, которая может вам помочь: http://robaldred.co.uk/2009/01/custom-log-files-for-your-ruby-on-rails-applications/comment-page-1/#comment-342

хорошее кодирование

1 голос
/ 22 мая 2011

Камень act_as_audited должен хорошо работать для вас:
https://github.com/collectiveidea/acts_as_audited

А что касается ActiveResource, он также будет моделью в каком-то другом приложении. Вы можете использовать гем на стороне сервера, и вам не нужно проверять его на стороне клиента. Все операции CRUD, использующие ActiveResource, в конечном итоге преобразуются в операции CRUD на ActiveRecord (на стороне сервера).

Так что, вероятно, вам нужно смотреть на это на расстоянии, и в обоих случаях применимо одно и то же решение, но в разных местах.

0 голосов
/ 26 мая 2011

Взгляните на этот Railscast, может быть, он может вам помочь: Уведомления

...