rails_audit_log 1.0.0 → 1.1.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: aa624cc1cc0ba6984f41de2f19b22891f9a7c376aae0625f94ebe86f2c48ef78
4
- data.tar.gz: 75bfb038bb1fe170bdf1a893ec87c40b3e7711602ad355a9c0788d20558ef337
3
+ metadata.gz: 26fdf8335990094a7278cbcf9921e123fbf44f6ec464b974b1c73e1f41047691
4
+ data.tar.gz: 9700fef384bb8f23eb929b0c62cb82880ba8217a44f64660f97823c4ffe6f1c0
5
5
  SHA512:
6
- metadata.gz: fd59c42a1522f4301e8c46c5b25186784847f5e56529fb7baf4d7b3d87d0f5391fb0bbcb33b8abac53bbf47a85a7529eae02d878c0484148a496b28950573541
7
- data.tar.gz: 7b2be961c2b1f6091c1426d7be966ceb99647a7415faa4a540f3876d63ea6ff2a10fab515b61074b9538a7f43b63d99481c1743e35aeb09e313d2e76c4c3107f
6
+ metadata.gz: 8c0450b8f9228ec6ea98e7e026e0554d73004accf1a284433fa6260b5acd941d8314fd33b9bf873156e214f65f8bcd1d687613077f60c310dacd2a2b4e1f98e8
7
+ data.tar.gz: 733c560b234f96900a760cc76b74a18bd39303179e36ae8272c0f150560139272eb3fb0de9ee435883fbd2c128bd408b373601f407818ff9e5c72cb9c465e503
data/README.md CHANGED
@@ -23,6 +23,8 @@ Audit logging for Rails. Tracks `create`, `update`, and `destroy` events as stru
23
23
  - [Bulk audit writes](#bulk-audit-writes)
24
24
  - [Async audit writes](#async-audit-writes)
25
25
  - [Capping history per record](#capping-history-per-record)
26
+ - [Time-based retention](#time-based-retention)
27
+ - [Scheduled and manual pruning](#scheduled-and-manual-pruning)
26
28
  - [Selective tracking](#selective-tracking)
27
29
  - [Disabling auditing](#disabling-auditing)
28
30
  - [Object reconstruction](#object-reconstruction)
@@ -95,6 +97,11 @@ RailsAuditLog.configure do |config|
95
97
  # Per-model `audit_log version_limit: N` takes precedence.
96
98
  # config.version_limit = 100
97
99
 
100
+ # Global time-based TTL — entries older than this duration are pruned after
101
+ # each write. Composes with version_limit: an entry is removed when it
102
+ # exceeds either constraint. Default: nil (no TTL)
103
+ # config.retention_period = 90.days
104
+
98
105
  # Write all audit entries asynchronously via WriteAuditLogJob.
99
106
  # Default: false — per-model `audit_log async: true` also works.
100
107
  # config.async = true
@@ -323,6 +330,45 @@ Set a global default in an initializer — per-model values take precedence:
323
330
  RailsAuditLog.version_limit = 50
324
331
  ```
325
332
 
333
+ ### Time-based retention
334
+
335
+ Automatically prune entries older than a configured duration by setting `retention_period` in an initializer:
336
+
337
+ ```ruby
338
+ # config/initializers/rails_audit_log.rb
339
+ RailsAuditLog.retention_period = 90.days
340
+ ```
341
+
342
+ Entries whose `created_at` is older than the period are deleted after each write. `retention_period` and `version_limit` compose — an entry is pruned when it exceeds **either** constraint.
343
+
344
+ Override the global default per model with `retain_for:`:
345
+
346
+ ```ruby
347
+ class Post < ApplicationRecord
348
+ include RailsAuditLog::Auditable
349
+ audit_log retain_for: 30.days # takes precedence over RailsAuditLog.retention_period
350
+ end
351
+ ```
352
+
353
+ ### Scheduled and manual pruning
354
+
355
+ `RailsAuditLog::PruneAuditLogJob` prunes all audited models in one pass. It iterates over every `item_type` present in `audit_log_entries`, resolves the effective `retain_for` / `retention_period` and `version_limit` per model, and deletes entries that exceed either constraint.
356
+
357
+ Enqueue it on a recurring schedule via your job backend:
358
+
359
+ ```ruby
360
+ # config/recurring.yml (Solid Queue)
361
+ prune_audit_log:
362
+ class: RailsAuditLog::PruneAuditLogJob
363
+ schedule: every day at midnight
364
+ ```
365
+
366
+ Or run it once manually via the rake task:
367
+
368
+ ```bash
369
+ bin/rails rails_audit_log:prune
370
+ ```
371
+
326
372
  ### Selective tracking
327
373
 
328
374
  Track only specific attributes, or exclude noisy ones:
@@ -575,13 +621,13 @@ refute_audit_log_entry post, event: :destroy
575
621
  | `.connects_to=` | Route `AuditLogEntry` to a separate database |
576
622
  | `.page_size=` | Entries per page in the web dashboard |
577
623
  | `.whodunnit_display=` | Proc for actor display name snapshot |
578
- | `.retention_period=` | _(1.1.0)_ Global time-based TTL |
624
+ | `.retention_period=` | Global time-based TTL for audit entries |
579
625
 
580
626
  **Concerns**
581
627
 
582
628
  | Class | Include in | Key methods |
583
629
  |---|---|---|
584
- | `RailsAuditLog::Auditable` | ActiveRecord models | `audit_log(only:, ignore:, meta:, associations:, version_limit:, async:)`, `skip_audit_log { }` |
630
+ | `RailsAuditLog::Auditable` | ActiveRecord models | `audit_log(only:, ignore:, meta:, associations:, version_limit:, retain_for:, async:)`, `skip_audit_log { }` |
585
631
  | `RailsAuditLog::Controller` | ActionController | `audit_log_actor { }` |
586
632
 
587
633
  **Model — `RailsAuditLog::AuditLogEntry`**
@@ -600,10 +646,12 @@ Constants: `EVENTS`, `BLOB_COLUMNS`, `PERIODS`
600
646
  | `RailsAuditLog::MinitestAssertions` | `require "rails_audit_log/minitest_assertions"` | Minitest: `assert_audit_log_entry`, `refute_audit_log_entry` |
601
647
  | `RailsAuditLog::TestHelpers` | `require "rails_audit_log/test_helpers"` | `without_audit_log { }` |
602
648
 
603
- **Jobs** (enqueued internally; configure via ActiveJob)
649
+ **Jobs** (configure via ActiveJob)
604
650
 
605
651
  `RailsAuditLog::WriteAuditLogJob` — do not instantiate directly; enqueued when `async: true` is set.
606
652
 
653
+ `RailsAuditLog::PruneAuditLogJob` — enqueue on a schedule to prune all audited models; also invoked by `bin/rails rails_audit_log:prune`.
654
+
607
655
  **Generators**
608
656
 
609
657
  `rails generate rails_audit_log:install` · `rails generate rails_audit_log:initializer`
@@ -28,6 +28,7 @@ module RailsAuditLog
28
28
  class_attribute :_audit_log_meta, default: nil
29
29
  class_attribute :_audit_log_associations, default: nil
30
30
  class_attribute :_audit_log_version_limit, default: nil
31
+ class_attribute :_audit_log_retain_for, default: nil
31
32
  class_attribute :_audit_log_async, default: false
32
33
 
33
34
  _warn_if_audit_table_missing
@@ -105,6 +106,9 @@ module RailsAuditLog
105
106
  # @param version_limit [Integer, nil] maximum number of entries to retain
106
107
  # per record; oldest entries are pruned after each write; overrides
107
108
  # {RailsAuditLog.version_limit} for this model
109
+ # @param retain_for [ActiveSupport::Duration, nil] per-model time-based TTL;
110
+ # entries older than this duration are pruned after each write; overrides
111
+ # {RailsAuditLog.retention_period} for this model
108
112
  # @param async [Boolean, nil] when +true+, writes are dispatched via
109
113
  # +WriteAuditLogJob+; overrides {RailsAuditLog.async} for this model
110
114
  # @return [void]
@@ -114,14 +118,16 @@ module RailsAuditLog
114
118
  # audit_log only: %i[title body published_at],
115
119
  # meta: { tenant_id: -> { Current.tenant_id } },
116
120
  # associations: %i[tags],
117
- # version_limit: 100
121
+ # version_limit: 100,
122
+ # retain_for: 30.days
118
123
  # end
119
- def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
124
+ def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, retain_for: nil, async: nil)
120
125
  self._audit_log_only = only.map(&:to_s) if only
121
126
  self._audit_log_ignore = ignore.map(&:to_s) if ignore
122
127
  self._audit_log_meta = meta if meta
123
128
  self._audit_log_associations = associations unless associations.nil?
124
129
  self._audit_log_version_limit = version_limit unless version_limit.nil?
130
+ self._audit_log_retain_for = retain_for unless retain_for.nil?
125
131
  self._audit_log_async = async unless async.nil?
126
132
  end
127
133
  end
@@ -199,8 +205,9 @@ module RailsAuditLog
199
205
  if (buffer = RailsAuditLog.batch_audit_buffer)
200
206
  buffer << entry_attrs.stringify_keys.merge("created_at" => Time.current)
201
207
  elsif _audit_log_async || RailsAuditLog.async
202
- limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
203
- WriteAuditLogJob.perform_later(entry_attrs.stringify_keys, version_limit: limit)
208
+ limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
209
+ period = self.class._audit_log_retain_for || RailsAuditLog.retention_period
210
+ WriteAuditLogJob.perform_later(entry_attrs.stringify_keys, version_limit: limit, retention_period: period)
204
211
  else
205
212
  RailsAuditLog::AuditLogEntry.create!(entry_attrs)
206
213
  prune_audit_entries
@@ -208,14 +215,19 @@ module RailsAuditLog
208
215
  end
209
216
 
210
217
  def prune_audit_entries
211
- limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
212
- return unless limit
218
+ limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
219
+ period = self.class._audit_log_retain_for || RailsAuditLog.retention_period
220
+ return unless limit || period
213
221
 
214
- count = audit_log_entries.count
215
- excess = count - limit
216
- return unless excess > 0
222
+ if period
223
+ audit_log_entries.where(created_at: ..period.ago).delete_all
224
+ end
217
225
 
218
- audit_log_entries.order(id: :asc).limit(excess).delete_all
226
+ if limit
227
+ count = audit_log_entries.count
228
+ excess = count - limit
229
+ audit_log_entries.order(id: :asc).limit(excess).delete_all if excess > 0
230
+ end
219
231
  end
220
232
 
221
233
  def build_audit_metadata
@@ -0,0 +1,58 @@
1
+ module RailsAuditLog
2
+ # Background job that prunes {AuditLogEntry} records for every audited model
3
+ # that has a configured retention policy.
4
+ #
5
+ # Enqueue on a recurring schedule via your job backend (Solid Queue,
6
+ # Sidekiq, GoodJob, etc.):
7
+ #
8
+ # RailsAuditLog::PruneAuditLogJob.perform_later
9
+ #
10
+ # The job iterates over every +item_type+ present in +audit_log_entries+,
11
+ # resolves the effective +retention_period+ / +version_limit+ for that model,
12
+ # and deletes entries that exceed either constraint. Pruning is always scoped
13
+ # to one +item_type+ at a time so models do not interfere with each other.
14
+ class PruneAuditLogJob < ApplicationJob
15
+ def perform
16
+ AuditLogEntry.distinct.pluck(:item_type).each do |item_type|
17
+ prune_item_type(item_type)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def prune_item_type(item_type)
24
+ klass = item_type.safe_constantize
25
+ period = resolve_period(klass)
26
+ limit = resolve_limit(klass)
27
+ return unless period || limit
28
+
29
+ scope = AuditLogEntry.where(item_type: item_type)
30
+
31
+ scope.where(created_at: ..period.ago).delete_all if period
32
+
33
+ return unless limit
34
+
35
+ scope.select(:item_id).distinct.pluck(:item_id).each do |item_id|
36
+ record_scope = scope.where(item_id: item_id)
37
+ excess = record_scope.count - limit
38
+ record_scope.order(id: :asc).limit(excess).delete_all if excess > 0
39
+ end
40
+ end
41
+
42
+ def resolve_period(klass)
43
+ if klass.respond_to?(:_audit_log_retain_for)
44
+ klass._audit_log_retain_for || RailsAuditLog.retention_period
45
+ else
46
+ RailsAuditLog.retention_period
47
+ end
48
+ end
49
+
50
+ def resolve_limit(klass)
51
+ if klass.respond_to?(:_audit_log_version_limit)
52
+ klass._audit_log_version_limit || RailsAuditLog.version_limit
53
+ else
54
+ RailsAuditLog.version_limit
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,20 +1,21 @@
1
1
  module RailsAuditLog
2
2
  class WriteAuditLogJob < ApplicationJob
3
- def perform(entry_attrs, version_limit: nil)
3
+ def perform(entry_attrs, version_limit: nil, retention_period: nil)
4
4
  AuditLogEntry.create!(entry_attrs)
5
5
 
6
- return unless version_limit
7
-
8
6
  item_type = entry_attrs["item_type"]
9
7
  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
8
+ scope = AuditLogEntry.where(item_type: item_type, item_id: item_id)
9
+
10
+ if retention_period
11
+ scope.where(created_at: ..retention_period.ago).delete_all
12
+ end
13
13
 
14
- AuditLogEntry.where(item_type: item_type, item_id: item_id)
15
- .order(id: :asc)
16
- .limit(excess)
17
- .delete_all
14
+ if version_limit
15
+ count = scope.count
16
+ excess = count - version_limit
17
+ scope.order(id: :asc).limit(excess).delete_all if excess > 0
18
+ end
18
19
  end
19
20
  end
20
21
  end
@@ -22,6 +22,11 @@ RailsAuditLog.configure do |config|
22
22
  # Per-model `audit_log version_limit: N` takes precedence.
23
23
  # config.version_limit = 100
24
24
 
25
+ # Global time-based TTL — entries older than this duration are pruned after
26
+ # each write. Composes with version_limit: an entry is removed when it
27
+ # exceeds either constraint. Default: nil (no TTL)
28
+ # config.retention_period = 90.days
29
+
25
30
  # Write all audit entries asynchronously via WriteAuditLogJob. Default: false
26
31
  # Per-model `audit_log async: true` also works.
27
32
  # config.async = true
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -53,6 +53,16 @@ module RailsAuditLog
53
53
  # @return [Integer, nil]
54
54
  mattr_accessor :version_limit, default: nil
55
55
 
56
+ # Global time-based TTL for audit entries. Entries whose +created_at+ is
57
+ # older than this duration are pruned automatically after each write.
58
+ # Composes with {.version_limit} — an entry is removed when it exceeds
59
+ # either constraint.
60
+ #
61
+ # @return [ActiveSupport::Duration, nil]
62
+ # @example
63
+ # RailsAuditLog.retention_period = 90.days
64
+ mattr_accessor :retention_period, default: nil
65
+
56
66
  # When +true+, all audit writes are dispatched via +WriteAuditLogJob+ instead
57
67
  # of being written inline. Override per-model with <tt>audit_log async: true</tt>.
58
68
  #
@@ -1,4 +1,7 @@
1
- # desc "Explaining what the task does"
2
- # task :rails_audit_log do
3
- # # Task goes here
4
- # end
1
+ namespace :rails_audit_log do
2
+ desc "Prune audit log entries that exceed the configured retention_period or version_limit. " \
3
+ "Delegates to RailsAuditLog::PruneAuditLogJob."
4
+ task prune: :environment do
5
+ RailsAuditLog::PruneAuditLogJob.perform_now
6
+ end
7
+ end
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: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -98,6 +98,7 @@ files:
98
98
  - app/javascript/rails_audit_log/diff_controller.js
99
99
  - app/javascript/rails_audit_log/search_controller.js
100
100
  - app/jobs/rails_audit_log/application_job.rb
101
+ - app/jobs/rails_audit_log/prune_audit_log_job.rb
101
102
  - app/jobs/rails_audit_log/write_audit_log_job.rb
102
103
  - app/models/rails_audit_log/application_record.rb
103
104
  - app/models/rails_audit_log/audit_log_entry.rb