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,
app/controllers/rails_audit_log/resources_controller.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, TestHelpers Classes: ApplicationController, ApplicationJob, ApplicationRecord, AuditLogEntriesController, AuditLogEntry, Engine, PruneAuditLogJob, ResourcesController, WriteAuditLogJob

Constant Summary collapse

VERSION =
"1.3.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)


177
178
179
# File 'lib/rails_audit_log.rb', line 177

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)


186
187
188
# File 'lib/rails_audit_log.rb', line 186

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.

Examples:

RailsAuditLog.acts_as_tenant!

Raises:

  • (RuntimeError)

    if the acts_as_tenant gem is not loaded



138
139
140
141
142
143
144
# File 'lib/rails_audit_log.rb', line 138

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.

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



249
250
251
252
253
254
255
# File 'lib/rails_audit_log.rb', line 249

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



155
156
157
158
# File 'lib/rails_audit_log.rb', line 155

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!



269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/rails_audit_log.rb', line 269

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)


288
289
290
# File 'lib/rails_audit_log.rb', line 288

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:



113
114
115
# File 'lib/rails_audit_log.rb', line 113

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: -> { ... }.

Examples:

RailsAuditLog.current_tenant { Current.tenant_id }

Yields:

  • block called with no arguments at write time; return the tenant id

Returns:

  • (Proc, nil)

    the stored block, or nil when not configured



125
126
127
128
# File 'lib/rails_audit_log.rb', line 125

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.

Examples:

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

Yields:

  • executes the block with audit logging disabled

Returns:

  • (Object)

    the return value of the block



220
221
222
223
224
225
226
# File 'lib/rails_audit_log.rb', line 220

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)


209
210
211
# File 'lib/rails_audit_log.rb', line 209

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

.reasonString?

Returns the reason string set on the current thread.

Returns:

  • (String, nil)


231
232
233
# File 'lib/rails_audit_log.rb', line 231

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

.reason=(value) ⇒ String?

Parameters:

  • value (String, nil)

Returns:

  • (String, nil)


237
238
239
# File 'lib/rails_audit_log.rb', line 237

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)


164
165
166
# File 'lib/rails_audit_log.rb', line 164

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)


170
171
172
# File 'lib/rails_audit_log.rb', line 170

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



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/rails_audit_log.rb', line 306

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



198
199
200
201
202
203
204
# File 'lib/rails_audit_log.rb', line 198

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)


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

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)


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

mattr_accessor :connects_to, default: nil

#encryptBoolean

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.

Returns:

  • (Boolean)


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

mattr_accessor :encrypt, default: false

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


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

mattr_accessor :page_size, default: 25

#retention_periodActiveSupport::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.

Examples:

RailsAuditLog.retention_period = 90.days

Returns:

  • (ActiveSupport::Duration, nil)


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

mattr_accessor :retention_period, default: nil

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


99
100
101
# File 'lib/rails_audit_log.rb', line 99

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