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

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)


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.

Parameters:

  • actor (Object, nil)

Returns:

  • (Object, nil)


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.

Examples:

RailsAuditLog.acts_as_tenant!

Raises:

  • (RuntimeError)

    if the acts_as_tenant gem is not 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.

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



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.

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



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.

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!



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


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.

Examples:

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

Yields:



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

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



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.

Examples:

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

Yields:

  • executes the block with audit logging disabled

Returns:

  • (Object)

    the return value of the block



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.

Returns:

  • (Boolean)


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.

Parameters:



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

def self.publish_entry(entry)
  streaming_adapter&.publish(entry)
end

.reasonString?

Returns the reason string set on the current thread.

Returns:

  • (String, nil)


252
253
254
# File 'lib/rails_audit_log.rb', line 252

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

.reason=(value) ⇒ String?

Parameters:

  • value (String, nil)

Returns:

  • (String, nil)


258
259
260
# File 'lib/rails_audit_log.rb', line 258

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)


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?

Parameters:

  • value (Hash, nil)

    metadata hash to store on the current thread

Returns:

  • (Hash, nil)


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

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



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.

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



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

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


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

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)


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

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)


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

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)


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

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


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

mattr_accessor :ignored_attributes, default: %w[updated_at]

#page_sizeInteger

Number of entries per page in the web dashboard.

Returns:

  • (Integer)


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

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)


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

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)


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.

Examples:

RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::NotificationsAdapter.new

Returns:

  • (#publish, nil)


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

mattr_accessor :streaming_adapter, default: nil

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


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

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)


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
}