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.
- checksums.yaml +4 -4
- data/README.md +284 -55
- data/Rakefile +5 -0
- data/app/concerns/rails_audit_log/auditable.rb +57 -0
- data/app/concerns/rails_audit_log/controller.rb +29 -0
- data/app/controllers/rails_audit_log/application_controller.rb +1 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +1 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +1 -0
- data/app/helpers/rails_audit_log/application_helper.rb +1 -0
- data/app/jobs/rails_audit_log/application_job.rb +1 -0
- data/app/models/rails_audit_log/application_record.rb +1 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +126 -26
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb +21 -0
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb +99 -0
- data/lib/rails_audit_log/engine.rb +1 -0
- data/lib/rails_audit_log/matchers.rb +56 -0
- data/lib/rails_audit_log/minitest_assertions.rb +36 -0
- data/lib/rails_audit_log/paper_trail_compat.rb +76 -0
- data/lib/rails_audit_log/test_helpers.rb +20 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +171 -9
- metadata +9 -5
data/lib/rails_audit_log.rb
CHANGED
|
@@ -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
|
-
#
|
|
6
|
-
#
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
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:
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
149
|
+
summary: Audit logging for Rails with a web dashboard and JSON-first storage
|
|
146
150
|
test_files: []
|