rails_audit_log 0.4.0 → 0.5.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 +62 -0
- data/app/concerns/rails_audit_log/auditable.rb +25 -8
- data/app/concerns/rails_audit_log/controller.rb +17 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +9 -0
- data/lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb +3 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +28 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf93211ea95d97c01b541195ade4517d221d39035896e06a6432c512e5f284c0
|
|
4
|
+
data.tar.gz: f1bce85319393eaca9db2e078a0087e1f0818c1025fdd23e472fb8381a322b91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0f9def2049dc5ca95e13fad9746bc84a79c7e8081d8e84dd0f4fbfd3915b57b9dd35aaa600687970475f49b395e15c6725d3c558b1272f7afda6ba3b18054c0
|
|
7
|
+
data.tar.gz: 1699b3f01dbfd55377144207ff36c987a83419adf318671a16676e5513fd31fc98f908e5b2f83cef3a9f7b597bdf7ac8fe2cf4da2c275f569bdcd4837a3a1727
|
data/README.md
CHANGED
|
@@ -179,6 +179,68 @@ entry.previous # => the entry before this one
|
|
|
179
179
|
entry.next # => the entry after this one (nil if last)
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
### Attaching a reason
|
|
183
|
+
|
|
184
|
+
Record a free-text rationale alongside any write using a thread-local block — safe to nest, cleared automatically:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
RailsAuditLog.audit_log_reason("Approved by legal") do
|
|
188
|
+
contract.update!(status: "approved")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
entry.reason # => "Approved by legal"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Arbitrary metadata
|
|
195
|
+
|
|
196
|
+
The `metadata` JSON column accepts any key/value hash. Populate it automatically with `audit_log meta:`:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
class Article < ApplicationRecord
|
|
200
|
+
include RailsAuditLog::Auditable
|
|
201
|
+
|
|
202
|
+
# Zero-arg lambda — evaluated at write time (good for thread-locals)
|
|
203
|
+
# One-arg lambda — receives the record instance
|
|
204
|
+
audit_log meta: {
|
|
205
|
+
tenant_id: -> { Current.tenant.id },
|
|
206
|
+
title_length: ->(record) { record.title.length }
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
entry.metadata # => { "tenant_id" => "acme", "title_length" => 12 }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Request metadata capture
|
|
214
|
+
|
|
215
|
+
Enable opt-in capture of `remote_ip` and `user_agent` from the current request in an initializer:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# config/initializers/rails_audit_log.rb
|
|
219
|
+
RailsAuditLog.capture_request_metadata = true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
When enabled, the `Controller` concern automatically stores these into each entry's `metadata` column:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
entry.metadata # => { "remote_ip" => "203.0.113.1", "user_agent" => "Mozilla/5.0 ..." }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Request metadata and `audit_log meta:` values are merged together automatically.
|
|
229
|
+
|
|
230
|
+
### Actor display name snapshot
|
|
231
|
+
|
|
232
|
+
`whodunnit_snapshot` stores the actor's display name at write time so entries remain meaningful even after the actor record is deleted:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
entry.whodunnit_snapshot # => "Alice" (even if the User record is later deleted)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The default display proc uses `actor.name` if available, otherwise `actor.to_s`. Override globally in an initializer:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
RailsAuditLog.whodunnit_display = ->(actor) { actor.email }
|
|
242
|
+
```
|
|
243
|
+
|
|
182
244
|
### Object snapshot storage
|
|
183
245
|
|
|
184
246
|
By default every entry stores a full `object` snapshot of the pre-change state alongside `object_changes`. This makes `reify` and `version_at` reliable without any database lookups:
|
|
@@ -5,6 +5,7 @@ module RailsAuditLog
|
|
|
5
5
|
included do
|
|
6
6
|
class_attribute :_audit_log_only, default: nil
|
|
7
7
|
class_attribute :_audit_log_ignore, default: nil
|
|
8
|
+
class_attribute :_audit_log_meta, default: nil
|
|
8
9
|
|
|
9
10
|
has_many :audit_log_entries,
|
|
10
11
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
@@ -17,9 +18,10 @@ module RailsAuditLog
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
class_methods do
|
|
20
|
-
def audit_log(only: nil, ignore: nil)
|
|
21
|
+
def audit_log(only: nil, ignore: nil, meta: nil)
|
|
21
22
|
self._audit_log_only = only.map(&:to_s) if only
|
|
22
23
|
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
24
|
+
self._audit_log_meta = meta if meta
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -51,17 +53,32 @@ module RailsAuditLog
|
|
|
51
53
|
return if filtered.empty? && event == "update"
|
|
52
54
|
|
|
53
55
|
actor = RailsAuditLog.actor
|
|
56
|
+
meta = build_audit_metadata
|
|
54
57
|
RailsAuditLog::AuditLogEntry.create!(
|
|
55
|
-
event:
|
|
56
|
-
item_type:
|
|
57
|
-
item_id:
|
|
58
|
-
object_changes:
|
|
59
|
-
object:
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
event: event,
|
|
59
|
+
item_type: self.class.name,
|
|
60
|
+
item_id: id,
|
|
61
|
+
object_changes: filtered,
|
|
62
|
+
object: snapshot,
|
|
63
|
+
reason: RailsAuditLog.reason,
|
|
64
|
+
metadata: meta.presence,
|
|
65
|
+
whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
|
|
66
|
+
actor_type: actor&.class&.name,
|
|
67
|
+
actor_id: actor.respond_to?(:id) ? actor.id : nil
|
|
62
68
|
)
|
|
63
69
|
end
|
|
64
70
|
|
|
71
|
+
def build_audit_metadata
|
|
72
|
+
meta = {}
|
|
73
|
+
if self.class._audit_log_meta
|
|
74
|
+
self.class._audit_log_meta.each do |key, callable|
|
|
75
|
+
meta[key.to_s] = callable.arity == 0 ? callable.call : callable.call(self)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
meta.merge!(RailsAuditLog.request_metadata || {})
|
|
79
|
+
meta
|
|
80
|
+
end
|
|
81
|
+
|
|
65
82
|
def filter_changes(changes)
|
|
66
83
|
result = changes.dup
|
|
67
84
|
|
|
@@ -5,6 +5,8 @@ module RailsAuditLog
|
|
|
5
5
|
included do
|
|
6
6
|
before_action :set_audit_log_actor
|
|
7
7
|
after_action :clear_audit_log_actor
|
|
8
|
+
before_action :capture_audit_log_request_metadata
|
|
9
|
+
after_action :clear_audit_log_request_metadata
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
class_methods do
|
|
@@ -27,5 +29,20 @@ module RailsAuditLog
|
|
|
27
29
|
def clear_audit_log_actor
|
|
28
30
|
RailsAuditLog.actor = nil
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
def capture_audit_log_request_metadata
|
|
34
|
+
return unless RailsAuditLog.capture_request_metadata
|
|
35
|
+
|
|
36
|
+
RailsAuditLog.request_metadata = {
|
|
37
|
+
"remote_ip" => request.remote_ip,
|
|
38
|
+
"user_agent" => request.user_agent
|
|
39
|
+
}.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear_audit_log_request_metadata
|
|
43
|
+
return unless RailsAuditLog.capture_request_metadata
|
|
44
|
+
|
|
45
|
+
RailsAuditLog.request_metadata = nil
|
|
46
|
+
end
|
|
30
47
|
end
|
|
31
48
|
end
|
|
@@ -10,6 +10,7 @@ module RailsAuditLog
|
|
|
10
10
|
validates :event, presence: true, inclusion: { in: EVENTS }
|
|
11
11
|
validates :item_type, presence: true
|
|
12
12
|
validates :item_id, presence: true
|
|
13
|
+
validate :metadata_must_be_a_hash
|
|
13
14
|
|
|
14
15
|
# Event scopes
|
|
15
16
|
scope :created_events, -> { where(event: "create") }
|
|
@@ -82,6 +83,14 @@ module RailsAuditLog
|
|
|
82
83
|
object_changes.transform_values { |from_to| { from: from_to[0], to: from_to[1] } }
|
|
83
84
|
end
|
|
84
85
|
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def metadata_must_be_a_hash
|
|
89
|
+
errors.add(:metadata, "must be a Hash") if metadata.present? && !metadata.is_a?(Hash)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
public
|
|
93
|
+
|
|
85
94
|
# Attribute scope — uses json_extract (SQLite/MySQL) or ->> (PostgreSQL)
|
|
86
95
|
scope :touching, ->(attribute) {
|
|
87
96
|
if connection.adapter_name =~ /PostgreSQL/i
|
|
@@ -6,6 +6,9 @@ class CreateAuditLogEntries < ActiveRecord::Migration[<%= ActiveRecord::Migratio
|
|
|
6
6
|
t.bigint :item_id, null: false
|
|
7
7
|
t.json :object_changes
|
|
8
8
|
t.json :object
|
|
9
|
+
t.json :metadata
|
|
10
|
+
t.string :reason
|
|
11
|
+
t.string :whodunnit_snapshot
|
|
9
12
|
t.string :actor_type
|
|
10
13
|
t.bigint :actor_id
|
|
11
14
|
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -6,6 +6,18 @@ module RailsAuditLog
|
|
|
6
6
|
# Override in an initializer: RailsAuditLog.ignored_attributes = %w[updated_at cached_at]
|
|
7
7
|
mattr_accessor :ignored_attributes, default: %w[updated_at]
|
|
8
8
|
mattr_accessor :store_snapshot, default: true
|
|
9
|
+
mattr_accessor :capture_request_metadata, default: false
|
|
10
|
+
mattr_accessor :whodunnit_display, default: ->(actor) {
|
|
11
|
+
actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def self.request_metadata
|
|
15
|
+
Thread.current[:rails_audit_log_request_metadata]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.request_metadata=(value)
|
|
19
|
+
Thread.current[:rails_audit_log_request_metadata] = value
|
|
20
|
+
end
|
|
9
21
|
|
|
10
22
|
def self.actor
|
|
11
23
|
Thread.current[:rails_audit_log_actor]
|
|
@@ -35,6 +47,22 @@ module RailsAuditLog
|
|
|
35
47
|
Thread.current[:rails_audit_log_disabled] = previous
|
|
36
48
|
end
|
|
37
49
|
|
|
50
|
+
def self.reason
|
|
51
|
+
Thread.current[:rails_audit_log_reason]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.reason=(value)
|
|
55
|
+
Thread.current[:rails_audit_log_reason] = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.audit_log_reason(value)
|
|
59
|
+
previous = self.reason
|
|
60
|
+
self.reason = value
|
|
61
|
+
yield
|
|
62
|
+
ensure
|
|
63
|
+
self.reason = previous
|
|
64
|
+
end
|
|
65
|
+
|
|
38
66
|
def self.version_at(record, time)
|
|
39
67
|
entry = AuditLogEntry
|
|
40
68
|
.where(item_type: record.class.name, item_id: record.id)
|