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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb82307e35624fe226336c4fb33559b5432a6290c4978076aadec80e6939afa0
4
- data.tar.gz: 47e700f38e3d1fbe2a0953ba687285389721b31e8a6531dcbc5e336c6d9af438
3
+ metadata.gz: cf93211ea95d97c01b541195ade4517d221d39035896e06a6432c512e5f284c0
4
+ data.tar.gz: f1bce85319393eaca9db2e078a0087e1f0818c1025fdd23e472fb8381a322b91
5
5
  SHA512:
6
- metadata.gz: 5dc66b780e8d1875260ef21c143b3ec823832ffa7e90c58f8392e002515d4cbe30c797936f4766bd15791a9b1b37e2f8918973f33fec58e396ca117a3623a563
7
- data.tar.gz: d7fbf0c15e39d95a57270203b296ce5bac2835cc15f69d44cd6a7b000a884bc8d639cee91de5c1dd84aeaec64795fa6325397a1926a67f9a8317cc1128c18808
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: event,
56
- item_type: self.class.name,
57
- item_id: id,
58
- object_changes: filtered,
59
- object: snapshot,
60
- actor_type: actor&.class&.name,
61
- actor_id: actor.respond_to?(:id) ? actor.id : nil
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" }
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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)
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith