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/write_audit_log_job.rb,
app/models/rails_audit_log/application_record.rb,
app/helpers/rails_audit_log/application_helper.rb,
app/controllers/rails_audit_log/resources_controller.rb,
app/controllers/rails_audit_log/application_controller.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/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, TestHelpers Classes: ApplicationController, ApplicationJob, ApplicationRecord, AuditLogEntriesController, AuditLogEntry, Engine, ResourcesController, WriteAuditLogJob

Constant Summary collapse

VERSION =
"1.0.0"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.actorObject?

Returns the actor set on the current thread (e.g. the signed-in user).

Returns:

  • (Object, nil)


130
131
132
# File 'lib/rails_audit_log.rb', line 130

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.

Parameters:

  • actor (Object, nil)

Returns:

  • (Object, nil)


139
140
141
# File 'lib/rails_audit_log.rb', line 139

def self.actor=(actor)
  Thread.current[:rails_audit_log_actor] = actor
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.

Examples:

RailsAuditLog.audit_log_reason("bulk import") { records.each(&:save!) }

Parameters:

  • value (String)

    reason to attach to every entry written in the block

Yields:

  • executes the block with value as the current reason

Returns:

  • (Object)

    the return value of the block



202
203
204
205
206
207
208
# File 'lib/rails_audit_log.rb', line 202

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.

Examples:

Require admin access

RailsAuditLog.authenticate { current_user&.admin? }

Yields:

  • block evaluated in controller context; return truthy to allow access

Returns:

  • (Proc, nil)

    the stored block, or nil when not configured



108
109
110
111
# File 'lib/rails_audit_log.rb', line 108

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.

Examples:

RailsAuditLog.batch_audit { 500.times { |i| Post.create!(title: "Post #{i}") } }

Yields:

  • executes the block; any audit entries created are buffered

Returns:

  • (Object)

    the return value of the block

Raises:

  • (ActiveRecord::RecordInvalid)

    if any entry fails the insert_all!



222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/rails_audit_log.rb', line 222

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]
    AuditLogEntry.insert_all!(batch) if batch.any?
    result
  ensure
    Thread.current[:rails_audit_log_batch] = nil
  end
end

.batch_audit_bufferArray<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.

Returns:

  • (Array<Hash>, nil)


241
242
243
# File 'lib/rails_audit_log.rb', line 241

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.

Examples:

RailsAuditLog.configure do |config|
  config.ignored_attributes = %w[updated_at]
  config.async = true
end

Yields:



95
96
97
# File 'lib/rails_audit_log.rb', line 95

def self.configure
  yield self
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.

Examples:

RailsAuditLog.disable { Post.create!(title: "seed post") }

Yields:

  • executes the block with audit logging disabled

Returns:

  • (Object)

    the return value of the block



173
174
175
176
177
178
179
# File 'lib/rails_audit_log.rb', line 173

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.

Returns:

  • (Boolean)


162
163
164
# File 'lib/rails_audit_log.rb', line 162

def self.enabled?
  !Thread.current[:rails_audit_log_disabled]
end

.reasonString?

Returns the reason string set on the current thread.

Returns:

  • (String, nil)


184
185
186
# File 'lib/rails_audit_log.rb', line 184

def self.reason
  Thread.current[:rails_audit_log_reason]
end

.reason=(value) ⇒ String?

Parameters:

  • value (String, nil)

Returns:

  • (String, nil)


190
191
192
# File 'lib/rails_audit_log.rb', line 190

def self.reason=(value)
  Thread.current[:rails_audit_log_reason] = value
end

.request_metadataHash?

Returns the request metadata hash attached to the current thread. Populated by Controller when #capture_request_metadata is true.

Returns:

  • (Hash, nil)


117
118
119
# File 'lib/rails_audit_log.rb', line 117

def self.
  Thread.current[:rails_audit_log_request_metadata]
end

.request_metadata=(value) ⇒ Hash?

Parameters:

  • value (Hash, nil)

    metadata hash to store on the current thread

Returns:

  • (Hash, nil)


123
124
125
# File 'lib/rails_audit_log.rb', line 123

def self.request_metadata=(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.

Examples:

post = Post.find(42)
snapshot = RailsAuditLog.version_at(post, 1.week.ago)
snapshot.title  # => title as it was a week ago

Parameters:

  • record (ActiveRecord::Base)

    the record to reconstruct

  • time (Time)

    the point in time to reconstruct at

Returns:

  • (ActiveRecord::Base, nil)

    a new, unpersisted instance; or nil



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/rails_audit_log.rb', line 259

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.

Examples:

RailsAuditLog.with_actor(robot_user) { DataImporter.new.run }

Parameters:

  • actor (Object)

    the actor to set (e.g. a User record)

Yields:

  • executes the block with actor as the current actor

Returns:

  • (Object)

    the return value of the block



151
152
153
154
155
156
157
# File 'lib/rails_audit_log.rb', line 151

def self.with_actor(actor)
  previous = self.actor
  self.actor = actor
  yield
ensure
  self.actor = previous
end

Instance Method Details

#asyncBoolean

When true, all audit writes are dispatched via WriteAuditLogJob instead of being written inline. Override per-model with audit_log async: true.

Returns:

  • (Boolean)


60
# File 'lib/rails_audit_log.rb', line 60

mattr_accessor :async, default: false

#capture_request_metadataBoolean

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.

Returns:

  • (Boolean)


47
# File 'lib/rails_audit_log.rb', line 47

mattr_accessor :capture_request_metadata, default: false

#connects_toHash?

Passes connects_to options directly to AuditLogEntry so audit entries can be stored on a separate database.

Examples:

RailsAuditLog.connects_to = { database: { writing: :audit_primary } }

Returns:

  • (Hash, nil)


68
# File 'lib/rails_audit_log.rb', line 68

mattr_accessor :connects_to, default: nil

#ignored_attributesArray<String>

Columns ignored on every audited model unless overridden with only: or ignore: on Auditable.audit_log.

Returns:

  • (Array<String>)


33
# File 'lib/rails_audit_log.rb', line 33

mattr_accessor :ignored_attributes, default: %w[updated_at]

#page_sizeInteger

Number of entries per page in the web dashboard.

Returns:

  • (Integer)


73
# File 'lib/rails_audit_log.rb', line 73

mattr_accessor :page_size, default: 25

#store_snapshotBoolean

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.

Returns:

  • (Boolean)


40
# File 'lib/rails_audit_log.rb', line 40

mattr_accessor :store_snapshot, default: true

#version_limitInteger?

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.

Returns:

  • (Integer, nil)


54
# File 'lib/rails_audit_log.rb', line 54

mattr_accessor :version_limit, default: nil

#whodunnit_displayProc

Controls how an actor object is serialised into the whodunnit_snapshot string column. Defaults to actor.name when available, otherwise to_s.

Examples:

Store email instead of name

RailsAuditLog.whodunnit_display = ->(actor) { actor.email }

Returns:

  • (Proc)


81
82
83
# File 'lib/rails_audit_log.rb', line 81

mattr_accessor :whodunnit_display, default: ->(actor) {
  actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
}