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/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, PruneAuditLogJob, ResourcesController, WriteAuditLogJob

Constant Summary collapse

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


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

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)


149
150
151
# File 'lib/rails_audit_log.rb', line 149

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



212
213
214
215
216
217
218
# File 'lib/rails_audit_log.rb', line 212

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



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

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!



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/rails_audit_log.rb', line 232

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)


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

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:



105
106
107
# File 'lib/rails_audit_log.rb', line 105

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



183
184
185
186
187
188
189
# File 'lib/rails_audit_log.rb', line 183

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)


172
173
174
# File 'lib/rails_audit_log.rb', line 172

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

.reasonString?

Returns the reason string set on the current thread.

Returns:

  • (String, nil)


194
195
196
# File 'lib/rails_audit_log.rb', line 194

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

.reason=(value) ⇒ String?

Parameters:

  • value (String, nil)

Returns:

  • (String, nil)


200
201
202
# File 'lib/rails_audit_log.rb', line 200

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)


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

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)


133
134
135
# File 'lib/rails_audit_log.rb', line 133

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



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/rails_audit_log.rb', line 269

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



161
162
163
164
165
166
167
# File 'lib/rails_audit_log.rb', line 161

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)


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

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)


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

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)


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

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