У меня огромный проект с моделями ActiveRecord и ActiveResource. Мне нужно реализовать регистрацию активности пользователя с этими моделями, а также регистрировать изменения атрибутов модели (сохранить состояние объекта или что-то подобное). Изменения могут выполняться пользователями или задачами cron rake.
У меня также должна быть возможность искать любые данные по дате, любое поле..etc
Будет приятно также генерировать читаемые сообщения с последней активностью, например
Я должен использовать драгоценные камни или могу ли я сделать всю логику с наблюдателями, не меняющими модели?
Ответ 3
Fivell, я только что увидел этот вопрос и не успел обработать изменения сегодня вечером до того, как истекает срок, и я дам вам свой код аудита, который работает с ActiveRecord, и должен работать с ActiveResource, возможно, с несколькими (я не использую ARes достаточно часто, чтобы знать, что небрежно). Я знаю, что обратные вызовы, которые мы используем, есть, но я не уверен, что ARes имеет отслеживание атрибутов changes
ActiveRecord.
Этот код регистрирует каждый CREATE/UPDATE/DELETE для всех моделей (кроме CREATEs в модели журнала аудита и любых других исключений, которые вы указываете) с изменениями, хранящимися как JSON. Очищенная обратная трассировка также сохраняется, поэтому вы можете определить, какой код внес изменения (это фиксирует любую точку вашего MVC, а также задачи рейка и использование консоли).
Этот код работает для использования консоли, рейк-задач и http-запросов, хотя обычно только последний регистрирует текущего пользователя. (Если я правильно помню, наблюдатель ActiveRecord, который это заменил, не работал в рейк-задачах или консоли.) О, этот код исходит из приложения Rails 2.3 - у меня есть несколько приложений Rails 3, но мне не нужен этот вид аудита для них еще.
У меня нет кода, который бы красиво отображал эту информацию (мы только копаем данные, когда нам нужно искать проблему), но поскольку изменения хранятся как JSON, это должно быть довольно просто.
Сначала мы сохраняем текущего пользователя в User.current, поэтому он доступен везде, поэтому в app/models/user.rb
:
Class User < ActiveRecord::Base
cattr_accessor :current
...
end
Текущий пользователь устанавливается в контроллере приложения для каждого такого запроса (и не вызывает проблем с concurrency):
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