Module: RailsAuditLog
- Defined in:
- lib/rails_audit_log.rb,
lib/rails_audit_log/engine.rb,
lib/rails_audit_log/version.rb,
lib/rails_audit_log/matchers.rb,
lib/rails_audit_log/test_helpers.rb,
app/concerns/rails_audit_log/auditable.rb,
lib/rails_audit_log/paper_trail_compat.rb,
app/concerns/rails_audit_log/controller.rb,
lib/rails_audit_log/minitest_assertions.rb,
app/jobs/rails_audit_log/application_job.rb,
app/models/rails_audit_log/audit_log_entry.rb,
app/jobs/rails_audit_log/prune_audit_log_job.rb,
app/jobs/rails_audit_log/write_audit_log_job.rb,
app/models/rails_audit_log/application_record.rb,
app/helpers/rails_audit_log/application_helper.rb,
lib/rails_audit_log/streaming/active_job_adapter.rb,
lib/rails_audit_log/streaming/notifications_adapter.rb,
app/controllers/rails_audit_log/resources_controller.rb,
app/jobs/rails_audit_log/streaming/publish_entry_job.rb,
app/controllers/rails_audit_log/application_controller.rb,
lib/generators/rails_audit_log/tenant/tenant_generator.rb,
lib/generators/rails_audit_log/install/install_generator.rb,
app/controllers/rails_audit_log/audit_log_entries_controller.rb,
lib/generators/rails_audit_log/encryption/encryption_generator.rb,
lib/generators/rails_audit_log/initializer/initializer_generator.rb,
lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb
Overview
RailsAuditLog is a Rails engine that tracks ActiveRecord create, update, and destroy events as AuditLogEntry records with JSON-first storage and thread-local actor context.
Quick start
# config/initializers/rails_audit_log.rb
RailsAuditLog.configure do |config|
config.ignored_attributes = %w[updated_at cached_at]
config.store_snapshot = true
config.async = false
end
# app/models/article.rb
class Article < ApplicationRecord
include RailsAuditLog::Auditable
audit_log only: %i[title body]
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include RailsAuditLog::Controller
audit_log_actor { current_user }
end
Defined Under Namespace
Modules: ApplicationHelper, Auditable, Controller, Generators, Matchers, MinitestAssertions, PaperTrailCompat, Streaming, TestHelpers Classes: ApplicationController, ApplicationJob, ApplicationRecord, AuditLogEntriesController, AuditLogEntry, Engine, PruneAuditLogJob, ResourcesController, WriteAuditLogJob
Constant Summary collapse
- VERSION =
"1.5.0"
Class Method Summary collapse
-
.actor ⇒ Object?
Returns the actor set on the current thread (e.g. the signed-in user).
-
.actor=(actor) ⇒ Object?
Sets the actor on the current thread.
-
.acts_as_tenant! ⇒ void
Wires RailsAuditLog.current_tenant to ActsAsTenant.current_tenant&.id so audit entries are automatically scoped to the Acts As Tenant context.
-
.audit_log_reason(value) { ... } ⇒ Object
Sets a human-readable reason for the changes made within the block.
-
.authenticate { ... } ⇒ Proc?
Sets or returns the authentication block used to gate the web dashboard.
-
.batch_audit { ... } ⇒ Object
Collects all AuditLogEntry records created within the block and inserts them with a single <tt>INSERT …
-
.batch_audit_buffer ⇒ Array<Hash>?
private
Returns the in-progress batch buffer for the current thread, or
nilwhen no batch is active. -
.configure {|RailsAuditLog| ... } ⇒ void
Yields the module so every
mattr_accessorsetter is reachable asconfig.setting = value. -
.current_tenant { ... } ⇒ Proc?
Sets or returns the global tenant resolver block.
-
.disable { ... } ⇒ Object
Suspends audit logging for the duration of the block on the current thread.
-
.enabled? ⇒ Boolean
Returns
truewhen audit logging is active on the current thread. -
.publish_entry(entry) ⇒ void
private
Passes
entryto the configured #streaming_adapter if one is set. -
.reason ⇒ String?
Returns the reason string set on the current thread.
- .reason=(value) ⇒ String?
-
.request_metadata ⇒ Hash?
Returns the request metadata hash attached to the current thread.
- .request_metadata=(value) ⇒ Hash?
-
.version_at(record, time) ⇒ ActiveRecord::Base?
Reconstructs the state of
recordas it was attimeby replaying audit entries up to that timestamp. -
.with_actor(actor) { ... } ⇒ Object
Sets the actor for the duration of the block, then restores the previous value.
Instance Method Summary collapse
-
#async ⇒ Boolean
When
true, all audit writes are dispatched viaWriteAuditLogJobinstead of being written inline. -
#capture_request_metadata ⇒ Boolean
When
true, capturesremote_ipanduser_agentfrom the current request and merges them into every entry’smetadatacolumn. -
#connects_to ⇒ Hash?
Passes
connects_tooptions directly to AuditLogEntry so audit entries can be stored on a separate database. -
#encrypt ⇒ Boolean
When
true, encryptsobject_changesandobjectfor all audited models usingActiveRecord::Encryption. -
#ignored_attributes ⇒ Array<String>
Columns ignored on every audited model unless overridden with
only:orignore:on Auditable.audit_log. -
#page_size ⇒ Integer
Number of entries per page in the web dashboard.
-
#retention_period ⇒ ActiveSupport::Duration?
Global time-based TTL for audit entries.
-
#store_snapshot ⇒ Boolean
Whether to store a full snapshot of the record’s attributes in the
objectcolumn alongsideobject_changes. -
#streaming_adapter ⇒ #publish?
The active streaming adapter.
-
#version_limit ⇒ Integer?
Global cap on the number of AuditLogEntry records kept per tracked object.
-
#whodunnit_display ⇒ Proc
Controls how an actor object is serialised into the
whodunnit_snapshotstring column.
Class Method Details
.actor ⇒ Object?
Returns the actor set on the current thread (e.g. the signed-in user).
198 199 200 |
# File 'lib/rails_audit_log.rb', line 198 def self.actor Thread.current[:rails_audit_log_actor] end |
.actor=(actor) ⇒ Object?
Sets the actor on the current thread. Prefer with_actor for scoped assignment so the value is always restored.
207 208 209 |
# File 'lib/rails_audit_log.rb', line 207 def self.actor=(actor) Thread.current[:rails_audit_log_actor] = actor end |
.acts_as_tenant! ⇒ void
This method returns an undefined value.
Wires current_tenant to ActsAsTenant.current_tenant&.id so audit entries are automatically scoped to the Acts As Tenant context. Call once in an initializer after the gem is loaded.
149 150 151 152 153 154 155 |
# File 'lib/rails_audit_log.rb', line 149 def self.acts_as_tenant! unless defined?(ActsAsTenant) raise "ActsAsTenant is not loaded. Add the `acts_as_tenant` gem to your Gemfile." end current_tenant { ActsAsTenant.current_tenant&.id } end |
.audit_log_reason(value) { ... } ⇒ Object
Sets a human-readable reason for the changes made within the block. The reason is stored in each AuditLogEntry#reason and restored afterwards.
270 271 272 273 274 275 276 |
# File 'lib/rails_audit_log.rb', line 270 def self.audit_log_reason(value) previous = self.reason self.reason = value yield ensure self.reason = previous end |
.authenticate { ... } ⇒ Proc?
Sets or returns the authentication block used to gate the web dashboard. The block is evaluated in controller context, so controller helpers (e.g. current_user) are available directly. When the block returns falsy, the engine falls back to HTTP Basic auth.
176 177 178 179 |
# File 'lib/rails_audit_log.rb', line 176 def self.authenticate(&block) @authenticate = block if block_given? @authenticate end |
.batch_audit { ... } ⇒ Object
Collects all AuditLogEntry records created within the block and inserts them with a single INSERT ... VALUES (…), (…) via insert_all! instead of one INSERT per record.
Calls are idempotent: if a batch is already in progress on the current thread (i.e. a nested call), the inner block joins the outer batch.
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/rails_audit_log.rb', line 290 def self.batch_audit return yield if Thread.current[:rails_audit_log_batch] Thread.current[:rails_audit_log_batch] = [] begin result = yield batch = Thread.current[:rails_audit_log_batch] if batch.any? AuditLogEntry.insert_all!(batch) batch.each { |attrs| publish_entry(AuditLogEntry.new(attrs)) } if streaming_adapter end result ensure Thread.current[:rails_audit_log_batch] = nil end end |
.batch_audit_buffer ⇒ Array<Hash>?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the in-progress batch buffer for the current thread, or nil when no batch is active.
312 313 314 |
# File 'lib/rails_audit_log.rb', line 312 def self.batch_audit_buffer Thread.current[:rails_audit_log_batch] end |
.configure {|RailsAuditLog| ... } ⇒ void
This method returns an undefined value.
Yields the module so every mattr_accessor setter is reachable as config.setting = value.
124 125 126 |
# File 'lib/rails_audit_log.rb', line 124 def self.configure yield self end |
.current_tenant { ... } ⇒ Proc?
Sets or returns the global tenant resolver block. The block is called at write time and its return value is stored in the tenant_id column of each AuditLogEntry. Override per-model with audit_log tenant: -> { ... }.
136 137 138 139 |
# File 'lib/rails_audit_log.rb', line 136 def self.current_tenant(&block) @current_tenant = block if block_given? @current_tenant end |
.disable { ... } ⇒ Object
Suspends audit logging for the duration of the block on the current thread. Useful in seeds, factories, and test setup where audit noise is unwanted.
241 242 243 244 245 246 247 |
# File 'lib/rails_audit_log.rb', line 241 def self.disable previous = Thread.current[:rails_audit_log_disabled] Thread.current[:rails_audit_log_disabled] = true yield ensure Thread.current[:rails_audit_log_disabled] = previous end |
.enabled? ⇒ Boolean
Returns true when audit logging is active on the current thread.
230 231 232 |
# File 'lib/rails_audit_log.rb', line 230 def self.enabled? !Thread.current[:rails_audit_log_disabled] end |
.publish_entry(entry) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Passes entry to the configured #streaming_adapter if one is set. No-ops when no adapter is configured.
163 164 165 |
# File 'lib/rails_audit_log.rb', line 163 def self.publish_entry(entry) streaming_adapter&.publish(entry) end |
.reason ⇒ String?
Returns the reason string set on the current thread.
252 253 254 |
# File 'lib/rails_audit_log.rb', line 252 def self.reason Thread.current[:rails_audit_log_reason] end |
.reason=(value) ⇒ String?
258 259 260 |
# File 'lib/rails_audit_log.rb', line 258 def self.reason=(value) Thread.current[:rails_audit_log_reason] = value end |
.request_metadata ⇒ Hash?
Returns the request metadata hash attached to the current thread. Populated by Controller when #capture_request_metadata is true.
185 186 187 |
# File 'lib/rails_audit_log.rb', line 185 def self. Thread.current[:rails_audit_log_request_metadata] end |
.request_metadata=(value) ⇒ Hash?
191 192 193 |
# File 'lib/rails_audit_log.rb', line 191 def self.(value) Thread.current[:rails_audit_log_request_metadata] = value end |
.version_at(record, time) ⇒ ActiveRecord::Base?
Reconstructs the state of record as it was at time by replaying audit entries up to that timestamp.
Returns an unsaved, non-persisted instance of record.class whose attributes match the record’s state at time, or nil when no audit entry exists before time or the record was destroyed at or before time.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/rails_audit_log.rb', line 330 def self.version_at(record, time) entry = AuditLogEntry .where(item_type: record.class.name, item_id: record.id) .where(created_at: ..time) .order(created_at: :desc, id: :desc) .first return nil if entry.nil? || entry.event == "destroy" klass = record.class column_names = klass.column_names.map(&:to_s) to_attrs = (entry.object_changes || {}) .select { |k, _| column_names.include?(k) } .transform_values { |v| v[1] } attrs = entry.object.present? ? entry.object.merge(to_attrs) : to_attrs instance = klass.new instance.assign_attributes(attrs.except("id")) instance.id = attrs.fetch("id") { entry.item_id } instance end |
.with_actor(actor) { ... } ⇒ Object
Sets the actor for the duration of the block, then restores the previous value. Use this in background jobs and rake tasks.
219 220 221 222 223 224 225 |
# File 'lib/rails_audit_log.rb', line 219 def self.with_actor(actor) previous = self.actor self.actor = actor yield ensure self.actor = previous end |
Instance Method Details
#async ⇒ Boolean
When true, all audit writes are dispatched via WriteAuditLogJob instead of being written inline. Override per-model with audit_log async: true.
72 |
# File 'lib/rails_audit_log.rb', line 72 mattr_accessor :async, default: false |
#capture_request_metadata ⇒ Boolean
When true, captures remote_ip and user_agent from the current request and merges them into every entry’s metadata column. Requires Controller to be included in your base controller.
49 |
# File 'lib/rails_audit_log.rb', line 49 mattr_accessor :capture_request_metadata, default: false |
#connects_to ⇒ Hash?
Passes connects_to options directly to AuditLogEntry so audit entries can be stored on a separate database.
88 |
# File 'lib/rails_audit_log.rb', line 88 mattr_accessor :connects_to, default: nil |
#encrypt ⇒ Boolean
When true, encrypts object_changes and object for all audited models using ActiveRecord::Encryption. Requires the host app to configure config.active_record.encryption. Override per-model with audit_log encrypt: false to opt a specific model out.
80 |
# File 'lib/rails_audit_log.rb', line 80 mattr_accessor :encrypt, default: false |
#ignored_attributes ⇒ Array<String>
Columns ignored on every audited model unless overridden with only: or ignore: on Auditable.audit_log.
35 |
# File 'lib/rails_audit_log.rb', line 35 mattr_accessor :ignored_attributes, default: %w[updated_at] |
#page_size ⇒ Integer
Number of entries per page in the web dashboard.
93 |
# File 'lib/rails_audit_log.rb', line 93 mattr_accessor :page_size, default: 25 |
#retention_period ⇒ ActiveSupport::Duration?
Global time-based TTL for audit entries. Entries whose created_at is older than this duration are pruned automatically after each write. Composes with #version_limit — an entry is removed when it exceeds either constraint.
66 |
# File 'lib/rails_audit_log.rb', line 66 mattr_accessor :retention_period, default: nil |
#store_snapshot ⇒ Boolean
Whether to store a full snapshot of the record’s attributes in the object column alongside object_changes. Disable to reduce storage at the cost of losing RailsAuditLog::AuditLogEntry#reify fidelity for pre-snapshot entries.
42 |
# File 'lib/rails_audit_log.rb', line 42 mattr_accessor :store_snapshot, default: true |
#streaming_adapter ⇒ #publish?
The active streaming adapter. Any object implementing #publish(entry). Called after every audit entry is persisted, including batch writes. Set to nil (default) to disable streaming.
102 |
# File 'lib/rails_audit_log.rb', line 102 mattr_accessor :streaming_adapter, default: nil |
#version_limit ⇒ Integer?
Global cap on the number of AuditLogEntry records kept per tracked object. Oldest entries are pruned after each write once the limit is exceeded. Override per-model with audit_log version_limit: N.
56 |
# File 'lib/rails_audit_log.rb', line 56 mattr_accessor :version_limit, default: nil |
#whodunnit_display ⇒ Proc
Controls how an actor object is serialised into the whodunnit_snapshot string column. Defaults to actor.name when available, otherwise to_s.
110 111 112 |
# File 'lib/rails_audit_log.rb', line 110 mattr_accessor :whodunnit_display, default: ->(actor) { actor.respond_to?(:name) ? actor.name.to_s : actor.to_s } |