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 +4 -4
- data/README.md +51 -3
- data/app/concerns/rails_audit_log/auditable.rb +22 -10
- data/app/jobs/rails_audit_log/prune_audit_log_job.rb +58 -0
- data/app/jobs/rails_audit_log/write_audit_log_job.rb +11 -10
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +5 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +10 -0
- data/lib/tasks/rails_audit_log_tasks.rake +7 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26fdf8335990094a7278cbcf9921e123fbf44f6ec464b974b1c73e1f41047691
|
|
4
|
+
data.tar.gz: 9700fef384bb8f23eb929b0c62cb82880ba8217a44f64660f97823c4ffe6f1c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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=` |
|
|
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** (
|
|
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
|
|
203
|
-
|
|
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
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
if period
|
|
223
|
+
audit_log_entries.where(created_at: ..period.ago).delete_all
|
|
224
|
+
end
|
|
217
225
|
|
|
218
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
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
|