rails_audit_log 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,45 +1,153 @@
1
1
  require "rails_audit_log/version"
2
2
  require "rails_audit_log/engine"
3
3
 
4
+ # RailsAuditLog is a Rails engine that tracks ActiveRecord +create+, +update+,
5
+ # and +destroy+ events as {AuditLogEntry} records with JSON-first storage and
6
+ # thread-local actor context.
7
+ #
8
+ # == Quick start
9
+ #
10
+ # # config/initializers/rails_audit_log.rb
11
+ # RailsAuditLog.configure do |config|
12
+ # config.ignored_attributes = %w[updated_at cached_at]
13
+ # config.store_snapshot = true
14
+ # config.async = false
15
+ # end
16
+ #
17
+ # # app/models/article.rb
18
+ # class Article < ApplicationRecord
19
+ # include RailsAuditLog::Auditable
20
+ # audit_log only: %i[title body]
21
+ # end
22
+ #
23
+ # # app/controllers/application_controller.rb
24
+ # class ApplicationController < ActionController::Base
25
+ # include RailsAuditLog::Controller
26
+ # audit_log_actor { current_user }
27
+ # end
4
28
  module RailsAuditLog
5
- # Global default columns to ignore across all audited models.
6
- # Override in an initializer: RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
29
+ # Columns ignored on every audited model unless overridden with +only:+ or
30
+ # +ignore:+ on {Auditable.audit_log}.
31
+ #
32
+ # @return [Array<String>]
7
33
  mattr_accessor :ignored_attributes, default: %w[updated_at]
8
34
 
9
- def self.configure
10
- yield self
11
- end
35
+ # Whether to store a full snapshot of the record's attributes in the +object+
36
+ # column alongside +object_changes+. Disable to reduce storage at the cost of
37
+ # losing {AuditLogEntry#reify} fidelity for pre-snapshot entries.
38
+ #
39
+ # @return [Boolean]
12
40
  mattr_accessor :store_snapshot, default: true
41
+
42
+ # When +true+, captures +remote_ip+ and +user_agent+ from the current request
43
+ # and merges them into every entry's +metadata+ column.
44
+ # Requires {Controller} to be included in your base controller.
45
+ #
46
+ # @return [Boolean]
13
47
  mattr_accessor :capture_request_metadata, default: false
48
+
49
+ # Global cap on the number of {AuditLogEntry} records kept per tracked object.
50
+ # Oldest entries are pruned after each write once the limit is exceeded.
51
+ # Override per-model with <tt>audit_log version_limit: N</tt>.
52
+ #
53
+ # @return [Integer, nil]
14
54
  mattr_accessor :version_limit, default: nil
55
+
56
+ # When +true+, all audit writes are dispatched via +WriteAuditLogJob+ instead
57
+ # of being written inline. Override per-model with <tt>audit_log async: true</tt>.
58
+ #
59
+ # @return [Boolean]
15
60
  mattr_accessor :async, default: false
61
+
62
+ # Passes +connects_to+ options directly to {AuditLogEntry} so audit entries
63
+ # can be stored on a separate database.
64
+ #
65
+ # @return [Hash, nil]
66
+ # @example
67
+ # RailsAuditLog.connects_to = { database: { writing: :audit_primary } }
16
68
  mattr_accessor :connects_to, default: nil
17
- mattr_accessor :page_size, default: 25
18
69
 
70
+ # Number of entries per page in the web dashboard.
71
+ #
72
+ # @return [Integer]
73
+ mattr_accessor :page_size, default: 25
74
+
75
+ # Controls how an actor object is serialised into the +whodunnit_snapshot+
76
+ # string column. Defaults to +actor.name+ when available, otherwise +to_s+.
77
+ #
78
+ # @return [Proc]
79
+ # @example Store email instead of name
80
+ # RailsAuditLog.whodunnit_display = ->(actor) { actor.email }
81
+ mattr_accessor :whodunnit_display, default: ->(actor) {
82
+ actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
83
+ }
84
+
85
+ # Yields the module so every +mattr_accessor+ setter is reachable as
86
+ # <tt>config.setting = value</tt>.
87
+ #
88
+ # @yield [RailsAuditLog] the module itself
89
+ # @return [void]
90
+ # @example
91
+ # RailsAuditLog.configure do |config|
92
+ # config.ignored_attributes = %w[updated_at]
93
+ # config.async = true
94
+ # end
95
+ def self.configure
96
+ yield self
97
+ end
98
+
99
+ # Sets or returns the authentication block used to gate the web dashboard.
100
+ # The block is evaluated in controller context, so controller helpers
101
+ # (e.g. +current_user+) are available directly.
102
+ # When the block returns falsy, the engine falls back to HTTP Basic auth.
103
+ #
104
+ # @yield block evaluated in controller context; return truthy to allow access
105
+ # @return [Proc, nil] the stored block, or +nil+ when not configured
106
+ # @example Require admin access
107
+ # RailsAuditLog.authenticate { current_user&.admin? }
19
108
  def self.authenticate(&block)
20
109
  @authenticate = block if block_given?
21
110
  @authenticate
22
111
  end
23
- mattr_accessor :whodunnit_display, default: ->(actor) {
24
- actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
25
- }
26
112
 
113
+ # Returns the request metadata hash attached to the current thread.
114
+ # Populated by {Controller} when {.capture_request_metadata} is +true+.
115
+ #
116
+ # @return [Hash, nil]
27
117
  def self.request_metadata
28
118
  Thread.current[:rails_audit_log_request_metadata]
29
119
  end
30
120
 
121
+ # @param value [Hash, nil] metadata hash to store on the current thread
122
+ # @return [Hash, nil]
31
123
  def self.request_metadata=(value)
32
124
  Thread.current[:rails_audit_log_request_metadata] = value
33
125
  end
34
126
 
127
+ # Returns the actor set on the current thread (e.g. the signed-in user).
128
+ #
129
+ # @return [Object, nil]
35
130
  def self.actor
36
131
  Thread.current[:rails_audit_log_actor]
37
132
  end
38
133
 
134
+ # Sets the actor on the current thread. Prefer {.with_actor} for scoped
135
+ # assignment so the value is always restored.
136
+ #
137
+ # @param actor [Object, nil]
138
+ # @return [Object, nil]
39
139
  def self.actor=(actor)
40
140
  Thread.current[:rails_audit_log_actor] = actor
41
141
  end
42
142
 
143
+ # Sets the actor for the duration of the block, then restores the previous
144
+ # value. Use this in background jobs and rake tasks.
145
+ #
146
+ # @param actor [Object] the actor to set (e.g. a +User+ record)
147
+ # @yield executes the block with +actor+ as the current actor
148
+ # @return [Object] the return value of the block
149
+ # @example
150
+ # RailsAuditLog.with_actor(robot_user) { DataImporter.new.run }
43
151
  def self.with_actor(actor)
44
152
  previous = self.actor
45
153
  self.actor = actor
@@ -48,10 +156,20 @@ module RailsAuditLog
48
156
  self.actor = previous
49
157
  end
50
158
 
159
+ # Returns +true+ when audit logging is active on the current thread.
160
+ #
161
+ # @return [Boolean]
51
162
  def self.enabled?
52
163
  !Thread.current[:rails_audit_log_disabled]
53
164
  end
54
165
 
166
+ # Suspends audit logging for the duration of the block on the current thread.
167
+ # Useful in seeds, factories, and test setup where audit noise is unwanted.
168
+ #
169
+ # @yield executes the block with audit logging disabled
170
+ # @return [Object] the return value of the block
171
+ # @example
172
+ # RailsAuditLog.disable { Post.create!(title: "seed post") }
55
173
  def self.disable
56
174
  previous = Thread.current[:rails_audit_log_disabled]
57
175
  Thread.current[:rails_audit_log_disabled] = true
@@ -60,14 +178,27 @@ module RailsAuditLog
60
178
  Thread.current[:rails_audit_log_disabled] = previous
61
179
  end
62
180
 
181
+ # Returns the reason string set on the current thread.
182
+ #
183
+ # @return [String, nil]
63
184
  def self.reason
64
185
  Thread.current[:rails_audit_log_reason]
65
186
  end
66
187
 
188
+ # @param value [String, nil]
189
+ # @return [String, nil]
67
190
  def self.reason=(value)
68
191
  Thread.current[:rails_audit_log_reason] = value
69
192
  end
70
193
 
194
+ # Sets a human-readable reason for the changes made within the block.
195
+ # The reason is stored in each {AuditLogEntry#reason} and restored afterwards.
196
+ #
197
+ # @param value [String] reason to attach to every entry written in the block
198
+ # @yield executes the block with +value+ as the current reason
199
+ # @return [Object] the return value of the block
200
+ # @example
201
+ # RailsAuditLog.audit_log_reason("bulk import") { records.each(&:save!) }
71
202
  def self.audit_log_reason(value)
72
203
  previous = self.reason
73
204
  self.reason = value
@@ -76,6 +207,18 @@ module RailsAuditLog
76
207
  self.reason = previous
77
208
  end
78
209
 
210
+ # Collects all {AuditLogEntry} records created within the block and inserts
211
+ # them with a single <tt>INSERT ... VALUES (…), (…)</tt> via +insert_all!+
212
+ # instead of one INSERT per record.
213
+ #
214
+ # Calls are idempotent: if a batch is already in progress on the current
215
+ # thread (i.e. a nested call), the inner block joins the outer batch.
216
+ #
217
+ # @yield executes the block; any audit entries created are buffered
218
+ # @return [Object] the return value of the block
219
+ # @raise [ActiveRecord::RecordInvalid] if any entry fails the +insert_all!+
220
+ # @example
221
+ # RailsAuditLog.batch_audit { 500.times { |i| Post.create!(title: "Post #{i}") } }
79
222
  def self.batch_audit
80
223
  return yield if Thread.current[:rails_audit_log_batch]
81
224
 
@@ -90,10 +233,29 @@ module RailsAuditLog
90
233
  end
91
234
  end
92
235
 
236
+ # Returns the in-progress batch buffer for the current thread, or +nil+ when
237
+ # no batch is active.
238
+ #
239
+ # @api private
240
+ # @return [Array<Hash>, nil]
93
241
  def self.batch_audit_buffer
94
242
  Thread.current[:rails_audit_log_batch]
95
243
  end
96
244
 
245
+ # Reconstructs the state of +record+ as it was at +time+ by replaying audit
246
+ # entries up to that timestamp.
247
+ #
248
+ # Returns an unsaved, non-persisted instance of +record.class+ whose
249
+ # attributes match the record's state at +time+, or +nil+ when no audit
250
+ # entry exists before +time+ or the record was destroyed at or before +time+.
251
+ #
252
+ # @param record [ActiveRecord::Base] the record to reconstruct
253
+ # @param time [Time] the point in time to reconstruct at
254
+ # @return [ActiveRecord::Base, nil] a new, unpersisted instance; or +nil+
255
+ # @example
256
+ # post = Post.find(42)
257
+ # snapshot = RailsAuditLog.version_at(post, 1.week.ago)
258
+ # snapshot.title # => title as it was a week ago
97
259
  def self.version_at(record, time)
98
260
  entry = AuditLogEntry
99
261
  .where(item_type: record.class.name, item_id: record.id)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_audit_log
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -65,9 +65,10 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '43.0'
68
- description: A modern Rails engine that tracks ActiveRecord create, update, and destroy
69
- events with JSON-first storage, whodunnit actor context, and a clean query API.
70
- Drop-in replacement for PaperTrail with no legacy baggage.
68
+ description: Rails engine that tracks ActiveRecord create, update, and destroy events
69
+ as structured JSON records. Ships a mountable web dashboard, whodunnit actor context,
70
+ batch inserts via insert_all!, async writes via ActiveJob, time-travel reconstruction,
71
+ RSpec matchers, Minitest assertions, and a migration path from PaperTrail.
71
72
  email:
72
73
  - chuck@eclecticcoding.com
73
74
  executables: []
@@ -111,10 +112,13 @@ files:
111
112
  - lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
112
113
  - lib/generators/rails_audit_log/install/install_generator.rb
113
114
  - lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
115
+ - lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb
116
+ - lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb
114
117
  - lib/rails_audit_log.rb
115
118
  - lib/rails_audit_log/engine.rb
116
119
  - lib/rails_audit_log/matchers.rb
117
120
  - lib/rails_audit_log/minitest_assertions.rb
121
+ - lib/rails_audit_log/paper_trail_compat.rb
118
122
  - lib/rails_audit_log/test_helpers.rb
119
123
  - lib/rails_audit_log/version.rb
120
124
  - lib/tasks/rails_audit_log_tasks.rake
@@ -142,5 +146,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
146
  requirements: []
143
147
  rubygems_version: 3.6.9
144
148
  specification_version: 4
145
- summary: Zeitwerk-native audit logging for ActiveRecord
149
+ summary: Audit logging for Rails with a web dashboard and JSON-first storage
146
150
  test_files: []