rails_audit_log 0.6.0 → 0.7.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: 6cc56d9335d26c5f43d52de380aa354a268cbb266696799cf1775f051162c2d8
4
- data.tar.gz: '027792cdf8815ec2822177492b674d05534a3d96163d463dd603a415d471ace2'
3
+ metadata.gz: 3323e92d5f10ecd832a198a65bf6e0fd65c7bf0ff35069230bc8b7791d14e7fa
4
+ data.tar.gz: 4cd882a647d273ee3fe8a6c7b7f89a3b241ee0fa9b22b84aed93b8aea2a30c6d
5
5
  SHA512:
6
- metadata.gz: 107247da00cc1f7eec9750f9d59eb2b41fa47f4ec9c3a0b00f587882bf6080250fe5af5152a2d13491fdb6ebf6aa7d3f82a6418f89777b9dca3819c024692805
7
- data.tar.gz: 3715691ec2538d7fca5538af23e9f087899469876dcb98b660d82f18230ee531f45660950c95e4230f15a6a430b4b85999691472b9c61cf1d4e88af6c90849c2
6
+ metadata.gz: 7c8d577a7b027d8c300b62545930e94798967f5cdc7f57d4d84a06b3f1968d1357b6ea1db9ae8abf1cb3e36bda8e6fd12be6c2da0798cf7d6d6a02e60849621c
7
+ data.tar.gz: 69a905ee623f28403c53d280a56009cd81696406e22ff2f258ef2d0ec56fc4d7a154f72a0a345557ec8a39e0b36b655b3fba7c1a128bee954834e73d537946b6
data/README.md CHANGED
@@ -102,6 +102,32 @@ entry.diff
102
102
  # => { "title" => { from: "Hello", to: "World" } }
103
103
  ```
104
104
 
105
+ ### Separate audit database
106
+
107
+ Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
108
+
109
+ ```ruby
110
+ # config/initializers/rails_audit_log.rb
111
+ RailsAuditLog.connects_to = {
112
+ database: { writing: :audit_log, reading: :audit_log }
113
+ }
114
+ ```
115
+
116
+ The key (e.g. `:audit_log`) must match a database key in `config/database.yml`. All reads and writes on `AuditLogEntry` — including `batch_audit` inserts and `WriteAuditLogJob` — use that connection automatically.
117
+
118
+ ### Lightweight queries
119
+
120
+ Use `.slim` to exclude the three JSON blob columns (`object_changes`, `object`, `metadata`) from the SQL projection. This is useful for index or listing views where you only need the entry header (who, what event, when):
121
+
122
+ ```ruby
123
+ entries = AuditLogEntry.slim.for_resource(article).since(1.week.ago)
124
+ entries.first.event # => "update"
125
+ entries.first.actor_type # => "User"
126
+ entries.first.object_changes # => raises ActiveModel::MissingAttributeError
127
+ ```
128
+
129
+ > **Note:** Use `.count(:id)` instead of `.count` when chaining `.slim` with other scopes — Rails' `COUNT` with a multi-column `SELECT` is not supported by all databases.
130
+
105
131
  ### Association tracking
106
132
 
107
133
  Track `has_many` add and remove events by passing `associations: true` to `audit_log`. Call `audit_log` **before** the `has_many` declarations so the callbacks are wired at class load time:
@@ -151,6 +177,59 @@ end
151
177
 
152
178
  `belongs_to` foreign-key changes are already tracked as regular column updates and require no extra configuration.
153
179
 
180
+ ### Bulk audit writes
181
+
182
+ Wrap a block with `RailsAuditLog.batch_audit` to buffer all audit entries and flush them in a single `insert_all!` call at the end, eliminating N+1 inserts during imports:
183
+
184
+ ```ruby
185
+ RailsAuditLog.batch_audit do
186
+ records.each { |attrs| Post.create!(attrs) }
187
+ end
188
+ # All audit entries written in one INSERT
189
+ ```
190
+
191
+ Nested calls accumulate into the outermost batch — only one `insert_all!` fires. If the block raises, the buffer is discarded and no entries are written.
192
+
193
+ > **Note:** Version pruning (`version_limit`) is deferred in batch mode — the next write outside the batch will trigger pruning as usual.
194
+
195
+ ### Async audit writes
196
+
197
+ Offload audit writes to a background job by passing `async: true` to `audit_log`. The entry is enqueued via `RailsAuditLog::WriteAuditLogJob` (a subclass of `ActiveJob::Base`) so the request does not block on the database write:
198
+
199
+ ```ruby
200
+ class Post < ApplicationRecord
201
+ include RailsAuditLog::Auditable
202
+ audit_log async: true
203
+ end
204
+ ```
205
+
206
+ Enable async globally in an initializer — per-model `async: true` takes precedence:
207
+
208
+ ```ruby
209
+ # config/initializers/rails_audit_log.rb
210
+ RailsAuditLog.async = true
211
+ ```
212
+
213
+ Configure the queue adapter and queue name through standard ActiveJob settings. Version pruning also runs inside the job when `version_limit` is set.
214
+
215
+ ### Capping history per record
216
+
217
+ Limit how many audit entries are kept per record with `version_limit:`. Oldest entries are pruned automatically after each write once the cap is reached:
218
+
219
+ ```ruby
220
+ class Post < ApplicationRecord
221
+ include RailsAuditLog::Auditable
222
+ audit_log version_limit: 10 # keep only the 10 most recent entries
223
+ end
224
+ ```
225
+
226
+ Set a global default in an initializer — per-model values take precedence:
227
+
228
+ ```ruby
229
+ # config/initializers/rails_audit_log.rb
230
+ RailsAuditLog.version_limit = 50
231
+ ```
232
+
154
233
  ### Selective tracking
155
234
 
156
235
  Track only specific attributes, or exclude noisy ones:
@@ -3,10 +3,12 @@ module RailsAuditLog
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- class_attribute :_audit_log_only, default: nil
7
- class_attribute :_audit_log_ignore, default: nil
8
- class_attribute :_audit_log_meta, default: nil
9
- class_attribute :_audit_log_associations, default: nil
6
+ class_attribute :_audit_log_only, default: nil
7
+ class_attribute :_audit_log_ignore, default: nil
8
+ class_attribute :_audit_log_meta, default: nil
9
+ class_attribute :_audit_log_associations, default: nil
10
+ class_attribute :_audit_log_version_limit, default: nil
11
+ class_attribute :_audit_log_async, default: false
10
12
 
11
13
  has_many :audit_log_entries,
12
14
  class_name: "RailsAuditLog::AuditLogEntry",
@@ -50,11 +52,13 @@ module RailsAuditLog
50
52
  end
51
53
 
52
54
  class_methods do
53
- def audit_log(only: nil, ignore: nil, meta: nil, associations: nil)
54
- self._audit_log_only = only.map(&:to_s) if only
55
- self._audit_log_ignore = ignore.map(&:to_s) if ignore
56
- self._audit_log_meta = meta if meta
57
- self._audit_log_associations = associations unless associations.nil?
55
+ def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
56
+ self._audit_log_only = only.map(&:to_s) if only
57
+ self._audit_log_ignore = ignore.map(&:to_s) if ignore
58
+ self._audit_log_meta = meta if meta
59
+ self._audit_log_associations = associations unless associations.nil?
60
+ self._audit_log_version_limit = version_limit unless version_limit.nil?
61
+ self._audit_log_async = async unless async.nil?
58
62
  end
59
63
  end
60
64
 
@@ -84,11 +88,12 @@ module RailsAuditLog
84
88
 
85
89
  actor = RailsAuditLog.actor
86
90
  meta = build_audit_metadata
87
- RailsAuditLog::AuditLogEntry.create!(
91
+ write_audit_entry(
88
92
  event: "update",
89
93
  item_type: self.class.name,
90
94
  item_id: id,
91
95
  object_changes: { assoc_name => [before, after] },
96
+ object: nil,
92
97
  reason: RailsAuditLog.reason,
93
98
  metadata: meta.presence,
94
99
  whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
@@ -104,8 +109,8 @@ module RailsAuditLog
104
109
  return if filtered.empty? && event == "update"
105
110
 
106
111
  actor = RailsAuditLog.actor
107
- meta = build_audit_metadata
108
- RailsAuditLog::AuditLogEntry.create!(
112
+ meta = build_audit_metadata
113
+ write_audit_entry(
109
114
  event: event,
110
115
  item_type: self.class.name,
111
116
  item_id: id,
@@ -119,6 +124,29 @@ module RailsAuditLog
119
124
  )
120
125
  end
121
126
 
127
+ def write_audit_entry(entry_attrs)
128
+ if (buffer = RailsAuditLog.batch_audit_buffer)
129
+ buffer << entry_attrs.stringify_keys.merge("created_at" => Time.current)
130
+ elsif _audit_log_async || RailsAuditLog.async
131
+ limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
132
+ WriteAuditLogJob.perform_later(entry_attrs.stringify_keys, version_limit: limit)
133
+ else
134
+ RailsAuditLog::AuditLogEntry.create!(entry_attrs)
135
+ prune_audit_entries
136
+ end
137
+ end
138
+
139
+ def prune_audit_entries
140
+ limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
141
+ return unless limit
142
+
143
+ count = audit_log_entries.count
144
+ excess = count - limit
145
+ return unless excess > 0
146
+
147
+ audit_log_entries.order(id: :asc).limit(excess).delete_all
148
+ end
149
+
122
150
  def build_audit_metadata
123
151
  meta = {}
124
152
  if self.class._audit_log_meta
@@ -0,0 +1,20 @@
1
+ module RailsAuditLog
2
+ class WriteAuditLogJob < ApplicationJob
3
+ def perform(entry_attrs, version_limit: nil)
4
+ AuditLogEntry.create!(entry_attrs)
5
+
6
+ return unless version_limit
7
+
8
+ item_type = entry_attrs["item_type"]
9
+ item_id = entry_attrs["item_id"]
10
+ count = AuditLogEntry.where(item_type: item_type, item_id: item_id).count
11
+ excess = count - version_limit
12
+ return unless excess > 0
13
+
14
+ AuditLogEntry.where(item_type: item_type, item_id: item_id)
15
+ .order(id: :asc)
16
+ .limit(excess)
17
+ .delete_all
18
+ end
19
+ end
20
+ end
@@ -3,6 +3,13 @@ module RailsAuditLog
3
3
  self.table_name = "audit_log_entries"
4
4
 
5
5
  EVENTS = %w[create update destroy].freeze
6
+ BLOB_COLUMNS = %w[object_changes object metadata].freeze
7
+
8
+ def self.configure_connection!
9
+ return unless (opts = RailsAuditLog.connects_to)
10
+
11
+ connects_to(**opts)
12
+ end
6
13
 
7
14
  belongs_to :item, polymorphic: true, optional: true
8
15
  belongs_to :actor, polymorphic: true, optional: true
@@ -37,6 +44,9 @@ module RailsAuditLog
37
44
  scope :since, ->(time) { where(created_at: time..) }
38
45
  scope :until, ->(time) { where(created_at: ..time) }
39
46
 
47
+ # Projection scope — omits JSON blob columns for index/listing queries
48
+ scope :slim, -> { select(column_names - BLOB_COLUMNS) }
49
+
40
50
  # Instance methods
41
51
 
42
52
  def reify
@@ -1,5 +1,11 @@
1
1
  module RailsAuditLog
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace RailsAuditLog
4
+
5
+ initializer "rails_audit_log.connect_audit_db" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ RailsAuditLog::AuditLogEntry.configure_connection!
8
+ end
9
+ end
4
10
  end
5
11
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -7,6 +7,9 @@ module RailsAuditLog
7
7
  mattr_accessor :ignored_attributes, default: %w[updated_at]
8
8
  mattr_accessor :store_snapshot, default: true
9
9
  mattr_accessor :capture_request_metadata, default: false
10
+ mattr_accessor :version_limit, default: nil
11
+ mattr_accessor :async, default: false
12
+ mattr_accessor :connects_to, default: nil
10
13
  mattr_accessor :whodunnit_display, default: ->(actor) {
11
14
  actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
12
15
  }
@@ -63,6 +66,24 @@ module RailsAuditLog
63
66
  self.reason = previous
64
67
  end
65
68
 
69
+ def self.batch_audit
70
+ return yield if Thread.current[:rails_audit_log_batch]
71
+
72
+ Thread.current[:rails_audit_log_batch] = []
73
+ begin
74
+ result = yield
75
+ batch = Thread.current[:rails_audit_log_batch]
76
+ AuditLogEntry.insert_all!(batch) if batch.any?
77
+ result
78
+ ensure
79
+ Thread.current[:rails_audit_log_batch] = nil
80
+ end
81
+ end
82
+
83
+ def self.batch_audit_buffer
84
+ Thread.current[:rails_audit_log_batch]
85
+ end
86
+
66
87
  def self.version_at(record, time)
67
88
  entry = AuditLogEntry
68
89
  .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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -41,6 +41,7 @@ files:
41
41
  - app/controllers/rails_audit_log/application_controller.rb
42
42
  - app/helpers/rails_audit_log/application_helper.rb
43
43
  - app/jobs/rails_audit_log/application_job.rb
44
+ - app/jobs/rails_audit_log/write_audit_log_job.rb
44
45
  - app/models/rails_audit_log/application_record.rb
45
46
  - app/models/rails_audit_log/audit_log_entry.rb
46
47
  - app/views/layouts/rails_audit_log/application.html.erb