Ruby 同时具有ActiveResource和ActiveRecord的Rails审计系统
我有一个包含ActiveRecord和ActiveResource模型的大型项目。我需要用这些模型实现用户活动的日志记录,还需要记录模型属性的更改(保存对象状态或类似的东西)。用户或cron-rake任务可以进行更改 我还必须有可能搜索任何数据的日期,任何字段…等 例如,使用最后一个活动生成可读的消息也很好Ruby 同时具有ActiveResource和ActiveRecord的Rails审计系统,ruby,ruby-on-rails-3,logging,observer-pattern,auditing,Ruby,Ruby On Rails 3,Logging,Observer Pattern,Auditing,我有一个包含ActiveRecord和ActiveResource模型的大型项目。我需要用这些模型实现用户活动的日志记录,还需要记录模型属性的更改(保存对象状态或类似的东西)。用户或cron-rake任务可以进行更改 我还必须有可能搜索任何数据的日期,任何字段…等 例如,使用最后一个活动生成可读的消息也很好 用户Bob在2011-08-12 08:12将其密码更改为*,并通过电子邮件更改为*** 员工Jeff在2011-08-12 08:13添加了新合作伙伴:公司名称 管理员杰克在2011-0
- 用户Bob在2011-08-12 08:12将其密码更改为*,并通过电子邮件更改为***
- 员工Jeff在2011-08-12 08:13添加了新合作伙伴:公司名称
- 管理员杰克在2011-09-12 11:11删除了产品:产品名称
- 客户Sam订购了新服务:2011-09-12 11:12服务名称
我喜欢gem,有人能说我怎样才能让它与activeresource一起工作吗?您正在寻找的 很少有开源项目使用这个插件,我认为redmine以及Foreman
编辑:不幸的是,它只能执行ActiveRecord,不能执行ActiveResource。acts\u as\u审计的gem应该适合您:
就ActiveResource而言,它也将成为其他应用程序中的一个模型。您可以在服务器端使用gem,而不需要在客户端对其进行审核。使用ActiveResource的所有CRUD操作最终将转换为ActiveRecord(服务器端)上的CRUD操作 因此,您可能需要从远处观察,相同的解决方案将适用于这两种情况,但适用于不同的位置。 及
这两个都是只针对
ActiveRecord
的优秀解决方案,但由于ActiveRecord
的大部分内容已被提取到ActiveModel
,因此扩展这两个功能以支持ActiveResource
,至少对于只读支持是合理的。我浏览了Github网络图并在Google上搜索了一下,似乎没有任何这样的解决方案正在开发中,不过我希望在这两个插件中的一个上实现比从头开始更容易paper_trail
似乎正在进行更积极的开发,并对Rails 3.1做出了一些承诺,因此它可能更符合Rails内部的最新情况,更易于扩展,但这只是直觉——我对其中任何一个的内部都不熟悉。看看这个railscast,也许它可以帮助您:Fivell,我刚刚看到了这个问题,今晚在赏金到期之前没有时间修改,所以我将给你我的审计代码,它与ActiveRecord一起工作,应该与ActiveResource一起工作,也许需要做一些调整(我不经常使用ARes,所以我不会马上知道)。我知道我们使用的回调在那里,但我不确定ARes是否有ActiveRecord的脏属性更改
跟踪
此代码记录所有模型上的每次创建/更新/删除(审计日志模型上的创建和您指定的任何其他异常除外),更改存储为JSON。还存储了一个清理过的回溯,这样您就可以确定是什么代码进行了更改(这将捕获MVC中的任何一点以及rake任务和控制台使用情况)
这段代码适用于控制台使用、rake任务和http请求,尽管通常只有最后一段代码记录当前用户。(如果我没记错的话,被替换的ActiveRecord observer在rake任务或控制台中不起作用。)哦,这段代码来自Rails 2.3应用程序-我有几个Rails 3应用程序,但我还不需要对它们进行这种审核
我没有代码可以很好地显示这些信息(我们只在需要调查问题时才挖掘数据),但由于更改存储为JSON,因此应该相当简单
首先,我们将当前用户存储在user.current中,以便在任何地方都可以访问,因此在app/models/user.rb中:
Class User < ActiveRecord::Base
cattr_accessor :current
...
end
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
# == 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
如果有必要,您可以在rake任务中设置User.current
接下来,我们定义模型来存储审核信息app/models/audit\u log\u 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
最后,这里是我们对此的测试——当然,您需要修改实际的测试操作test/integration/audit\u log\u 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
为了跟踪用户活动(CRUD),我创建了一个从Logger继承的类,现在我计划编写一个用于跟踪用户的litle插件,我可以将其用于任何ROR应用程序的构建。我已经检查过是否有这样的插件,但我没有看到。我想有很多像gem一样的书面记录、审计的行为或日志,但我更喜欢使用插件。有什么建议吗?
以下是一个可能对您有所帮助的链接:
很好的编码您是指更改数据本身还是更改模式?这是两件不同的事情。我的答案是针对数据的……Yeapme to=(还有其他一些想法吗?如果审计同时在服务器端(Activeresource)和客户端(ActiveRecord)进行的话)如何搜索、显示最后一个活动…等等?我想我需要一个存储区来存储所有活动,这取决于您希望在哪一方查看信息。我没有使用gem,但我确信它会将审核信息存储在一个表中。因此,如果您需要在服务器端使用它,您可以始终创建一个模型并使用Activ访问它eRecord,如果您在客户端需要它,您可以创建另一个ActiveResource对象来访问审核信息。您只需要查看数据驻留的位置。如果数据位于同一位置,则应使用ActiveRecord,如果不在,则使用ActiveResource访问信息
# == 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