rails_audit_log 0.9.0 → 1.0.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 +284 -55
- data/Rakefile +5 -0
- data/app/concerns/rails_audit_log/auditable.rb +57 -0
- data/app/concerns/rails_audit_log/controller.rb +29 -0
- data/app/controllers/rails_audit_log/application_controller.rb +1 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +1 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +1 -0
- data/app/helpers/rails_audit_log/application_helper.rb +1 -0
- data/app/jobs/rails_audit_log/application_job.rb +1 -0
- data/app/models/rails_audit_log/application_record.rb +1 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +126 -26
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb +21 -0
- data/lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb +99 -0
- data/lib/rails_audit_log/engine.rb +1 -0
- data/lib/rails_audit_log/matchers.rb +56 -0
- data/lib/rails_audit_log/minitest_assertions.rb +36 -0
- data/lib/rails_audit_log/paper_trail_compat.rb +76 -0
- data/lib/rails_audit_log/test_helpers.rb +20 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +171 -9
- metadata +9 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa624cc1cc0ba6984f41de2f19b22891f9a7c376aae0625f94ebe86f2c48ef78
|
|
4
|
+
data.tar.gz: 75bfb038bb1fe170bdf1a893ec87c40b3e7711602ad355a9c0788d20558ef337
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd59c42a1522f4301e8c46c5b25186784847f5e56529fb7baf4d7b3d87d0f5391fb0bbcb33b8abac53bbf47a85a7529eae02d878c0484148a496b28950573541
|
|
7
|
+
data.tar.gz: 7b2be961c2b1f6091c1426d7be966ceb99647a7415faa4a540f3876d63ea6ff2a10fab515b61074b9538a7f43b63d99481c1743e35aeb09e313d2e76c4c3107f
|
data/README.md
CHANGED
|
@@ -6,10 +6,45 @@
|
|
|
6
6
|
[](https://www.ruby-lang.org)
|
|
7
7
|
[](https://codecov.io/gh/eclectic-coding/rails_audit_log)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Audit logging for Rails. Tracks `create`, `update`, and `destroy` events as structured JSON records with a mountable web dashboard, whodunnit actor context, batch writes, time-travel reconstruction, and test helpers — a clean, well-documented replacement for PaperTrail with a built-in migration path.
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Configuration](#configuration)
|
|
15
|
+
- [Web dashboard](#web-dashboard)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Tracking a model](#tracking-a-model)
|
|
18
|
+
- [Recording who made the change](#recording-who-made-the-change)
|
|
19
|
+
- [Actor context outside of controllers](#actor-context-outside-of-controllers)
|
|
20
|
+
- [Querying the audit log](#querying-the-audit-log)
|
|
21
|
+
- [Lightweight queries](#lightweight-queries)
|
|
22
|
+
- [Association tracking](#association-tracking)
|
|
23
|
+
- [Bulk audit writes](#bulk-audit-writes)
|
|
24
|
+
- [Async audit writes](#async-audit-writes)
|
|
25
|
+
- [Capping history per record](#capping-history-per-record)
|
|
26
|
+
- [Selective tracking](#selective-tracking)
|
|
27
|
+
- [Disabling auditing](#disabling-auditing)
|
|
28
|
+
- [Object reconstruction](#object-reconstruction)
|
|
29
|
+
- [Attaching a reason](#attaching-a-reason)
|
|
30
|
+
- [Arbitrary metadata](#arbitrary-metadata)
|
|
31
|
+
- [Request metadata capture](#request-metadata-capture)
|
|
32
|
+
- [Actor display name snapshot](#actor-display-name-snapshot)
|
|
33
|
+
- [Object snapshot storage](#object-snapshot-storage)
|
|
34
|
+
- [Separate audit database](#separate-audit-database)
|
|
35
|
+
- [Test helpers](#test-helpers)
|
|
36
|
+
- [Stability and versioning](#stability-and-versioning)
|
|
37
|
+
- [Migrating from PaperTrail](#migrating-from-papertrail)
|
|
38
|
+
- [Performance](#performance)
|
|
39
|
+
- [Companion gems](#companion-gems)
|
|
40
|
+
- [Requirements](#requirements)
|
|
41
|
+
- [Contributing](#contributing)
|
|
42
|
+
- [License](#license)
|
|
10
43
|
|
|
11
44
|
## Installation
|
|
12
45
|
|
|
46
|
+
[↑ Table of contents](#table-of-contents)
|
|
47
|
+
|
|
13
48
|
Add to your `Gemfile`:
|
|
14
49
|
|
|
15
50
|
```ruby
|
|
@@ -23,16 +58,67 @@ bin/rails generate rails_audit_log:install
|
|
|
23
58
|
bin/rails db:migrate
|
|
24
59
|
```
|
|
25
60
|
|
|
26
|
-
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
[↑ Table of contents](#table-of-contents)
|
|
64
|
+
|
|
65
|
+
Run the initializer generator to create `config/initializers/rails_audit_log.rb` with every option documented as a commented example:
|
|
27
66
|
|
|
28
67
|
```bash
|
|
29
68
|
bin/rails generate rails_audit_log:initializer
|
|
30
69
|
```
|
|
31
70
|
|
|
32
|
-
|
|
71
|
+
The generated file (shown below with all options) uses the block-style `configure` API. Every setting has a sensible default — uncomment only what you need:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# config/initializers/rails_audit_log.rb
|
|
75
|
+
|
|
76
|
+
RailsAuditLog.configure do |config|
|
|
77
|
+
# Global columns excluded from all audited models.
|
|
78
|
+
# Default: ["updated_at"]
|
|
79
|
+
# config.ignored_attributes = %w[updated_at cached_at]
|
|
80
|
+
|
|
81
|
+
# Store a full attribute snapshot alongside object_changes.
|
|
82
|
+
# Default: true
|
|
83
|
+
# Disable to save storage; reify and version_at fall back to diff-only reconstruction.
|
|
84
|
+
# config.store_snapshot = false
|
|
85
|
+
|
|
86
|
+
# Capture remote_ip and user_agent into each entry's metadata column.
|
|
87
|
+
# Default: false — requires RailsAuditLog::Controller in ApplicationController.
|
|
88
|
+
# config.capture_request_metadata = true
|
|
89
|
+
|
|
90
|
+
# Customise how the actor's display name is stored at write time.
|
|
91
|
+
# Default: actor.name if available, otherwise actor.to_s
|
|
92
|
+
# config.whodunnit_display = ->(actor) { actor.email }
|
|
93
|
+
|
|
94
|
+
# Global cap on entries retained per tracked record. nil = no limit.
|
|
95
|
+
# Per-model `audit_log version_limit: N` takes precedence.
|
|
96
|
+
# config.version_limit = 100
|
|
97
|
+
|
|
98
|
+
# Write all audit entries asynchronously via WriteAuditLogJob.
|
|
99
|
+
# Default: false — per-model `audit_log async: true` also works.
|
|
100
|
+
# config.async = true
|
|
101
|
+
|
|
102
|
+
# Route AuditLogEntry to a dedicated database (Rails multi-DB).
|
|
103
|
+
# config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
|
|
104
|
+
|
|
105
|
+
# Entries per page in the web dashboard. Default: 25
|
|
106
|
+
# config.page_size = 50
|
|
107
|
+
|
|
108
|
+
# Gate web dashboard access. Block runs in controller context so controller
|
|
109
|
+
# helpers like current_user are available directly. Falls back to HTTP Basic
|
|
110
|
+
# auth when the block returns falsy. Leave unset for unauthenticated access.
|
|
111
|
+
# config.authenticate { current_user&.admin? }
|
|
112
|
+
# config.authenticate { |c| c.current_user&.admin? }
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Each option is documented in detail in its own Usage section below.
|
|
33
117
|
|
|
34
118
|
## Web dashboard
|
|
35
119
|
|
|
120
|
+
[↑ Table of contents](#table-of-contents)
|
|
121
|
+
|
|
36
122
|
Mount the engine in `config/routes.rb` to enable the built-in audit trail browser:
|
|
37
123
|
|
|
38
124
|
```ruby
|
|
@@ -43,6 +129,8 @@ Then visit `/audit` to browse all audit entries. The dashboard is delivered via
|
|
|
43
129
|
|
|
44
130
|
## Usage
|
|
45
131
|
|
|
132
|
+
[↑ Table of contents](#table-of-contents)
|
|
133
|
+
|
|
46
134
|
### Tracking a model
|
|
47
135
|
|
|
48
136
|
Include `RailsAuditLog::Auditable` in any ActiveRecord model:
|
|
@@ -120,19 +208,6 @@ entry.diff
|
|
|
120
208
|
# => { "title" => { from: "Hello", to: "World" } }
|
|
121
209
|
```
|
|
122
210
|
|
|
123
|
-
### Separate audit database
|
|
124
|
-
|
|
125
|
-
Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
# config/initializers/rails_audit_log.rb
|
|
129
|
-
RailsAuditLog.connects_to = {
|
|
130
|
-
database: { writing: :audit_log, reading: :audit_log }
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
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.
|
|
135
|
-
|
|
136
211
|
### Lightweight queries
|
|
137
212
|
|
|
138
213
|
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):
|
|
@@ -293,22 +368,20 @@ article.skip_audit_log { article.update!(cached_at: Time.current) }
|
|
|
293
368
|
|
|
294
369
|
### Object reconstruction
|
|
295
370
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
`AuditLogEntry#reify` returns an unsaved ActiveRecord instance reflecting the record's state **before** the entry was recorded:
|
|
371
|
+
**Reify a single entry** — `AuditLogEntry#reify` returns an unsaved ActiveRecord instance reflecting the record's state **before** the entry was recorded:
|
|
299
372
|
|
|
300
373
|
```ruby
|
|
301
374
|
article.update!(title: "v2")
|
|
302
375
|
entry = article.audit_log_entries.updated_events.last
|
|
303
376
|
|
|
304
377
|
previous = entry.reify
|
|
305
|
-
previous.title
|
|
378
|
+
previous.title # => "v1" (the pre-update state)
|
|
306
379
|
previous.persisted? # => false
|
|
307
380
|
```
|
|
308
381
|
|
|
309
382
|
Returns `nil` for `create` entries (nothing existed before).
|
|
310
383
|
|
|
311
|
-
|
|
384
|
+
**Reconstruct state at any point in time:**
|
|
312
385
|
|
|
313
386
|
```ruby
|
|
314
387
|
snapshot = RailsAuditLog.version_at(article, 1.week.ago)
|
|
@@ -317,7 +390,7 @@ snapshot.title # => whatever the title was a week ago
|
|
|
317
390
|
|
|
318
391
|
Returns `nil` if the record had no history at that time or was already destroyed.
|
|
319
392
|
|
|
320
|
-
|
|
393
|
+
**Navigate the version chain:**
|
|
321
394
|
|
|
322
395
|
```ruby
|
|
323
396
|
entry = article.audit_log_entries.updated_events.last
|
|
@@ -402,31 +475,58 @@ To save storage at the cost of reduced reification accuracy, switch to diff-only
|
|
|
402
475
|
RailsAuditLog.store_snapshot = false
|
|
403
476
|
```
|
|
404
477
|
|
|
405
|
-
###
|
|
478
|
+
### Separate audit database
|
|
479
|
+
|
|
480
|
+
Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
# config/initializers/rails_audit_log.rb
|
|
484
|
+
RailsAuditLog.connects_to = {
|
|
485
|
+
database: { writing: :audit_log, reading: :audit_log }
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
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.
|
|
490
|
+
|
|
491
|
+
### Test helpers
|
|
406
492
|
|
|
407
|
-
|
|
493
|
+
**`RailsAuditLog::TestHelpers`** — silences audit tracking inside a block; useful in FactoryBot factories and seed data:
|
|
408
494
|
|
|
409
495
|
```ruby
|
|
410
|
-
# spec/rails_helper.rb
|
|
496
|
+
# spec/rails_helper.rb
|
|
411
497
|
require "rails_audit_log/test_helpers"
|
|
412
498
|
|
|
413
499
|
RSpec.configure do |config|
|
|
414
500
|
config.include RailsAuditLog::TestHelpers
|
|
415
501
|
end
|
|
502
|
+
```
|
|
416
503
|
|
|
417
|
-
|
|
418
|
-
FactoryBot
|
|
419
|
-
|
|
420
|
-
after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
|
|
421
|
-
end
|
|
422
|
-
end
|
|
504
|
+
```ruby
|
|
505
|
+
# Or in a FactoryBot factory:
|
|
506
|
+
after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
|
|
423
507
|
```
|
|
424
508
|
|
|
425
509
|
`without_audit_log` is a prefix-free wrapper around `RailsAuditLog.disable` — thread-safe and restores tracking even if the block raises.
|
|
426
510
|
|
|
427
|
-
|
|
511
|
+
**`RailsAuditLog::Matchers`** (RSpec) — add to `spec/rails_helper.rb`:
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
require "rails_audit_log/matchers"
|
|
428
515
|
|
|
429
|
-
|
|
516
|
+
RSpec.configure do |config|
|
|
517
|
+
config.include RailsAuditLog::Matchers
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
expect(post).to have_audit_log_entry
|
|
523
|
+
expect(post).to have_audit_log_entry(:update)
|
|
524
|
+
expect(post).to have_audit_log_entry(:update).touching(:title)
|
|
525
|
+
|
|
526
|
+
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**`RailsAuditLog::MinitestAssertions`** — add to `test/test_helper.rb`:
|
|
430
530
|
|
|
431
531
|
```ruby
|
|
432
532
|
require "rails_audit_log/minitest_assertions"
|
|
@@ -436,51 +536,180 @@ class ActiveSupport::TestCase
|
|
|
436
536
|
end
|
|
437
537
|
```
|
|
438
538
|
|
|
439
|
-
Then use the assertions in any test:
|
|
440
|
-
|
|
441
539
|
```ruby
|
|
442
|
-
assert_audit_log_entry post
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
540
|
+
assert_audit_log_entry post, event: :update, touching: :title
|
|
541
|
+
refute_audit_log_entry post, event: :destroy
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## Stability and versioning
|
|
545
|
+
|
|
546
|
+
[↑ Table of contents](#table-of-contents)
|
|
547
|
+
|
|
548
|
+
`rails_audit_log` follows [Semantic Versioning](https://semver.org/) from 1.0.0. The public API is everything documented in this README. Anything not listed here is internal and may change between minor versions.
|
|
549
|
+
|
|
550
|
+
| Version bump | Meaning |
|
|
551
|
+
|---|---|
|
|
552
|
+
| **Patch** (1.0.x) | Bug fixes only — no API changes |
|
|
553
|
+
| **Minor** (1.x.0) | New features, backward-compatible |
|
|
554
|
+
| **Major** (x.0.0) | Breaking changes with a documented migration path |
|
|
555
|
+
|
|
556
|
+
### Public API surface
|
|
557
|
+
|
|
558
|
+
**Module-level** — `RailsAuditLog`
|
|
559
|
+
|
|
560
|
+
| Method / accessor | Purpose |
|
|
561
|
+
|---|---|
|
|
562
|
+
| `.configure { \|config\| }` | Block-style configuration |
|
|
563
|
+
| `.with_actor(actor) { }` | Set whodunnit context for a block |
|
|
564
|
+
| `.disable { }` | Suppress all audit writes for a block |
|
|
565
|
+
| `.enabled?` | Whether audit writes are active |
|
|
566
|
+
| `.audit_log_reason(value) { }` | Attach a free-text reason for a block |
|
|
567
|
+
| `.batch_audit { }` | Buffer writes and flush with a single `insert_all!` |
|
|
568
|
+
| `.version_at(record, time)` | Reconstruct record state at a point in time |
|
|
569
|
+
| `.authenticate { }` | Gate web dashboard access |
|
|
570
|
+
| `.ignored_attributes=` | Global columns excluded from all models |
|
|
571
|
+
| `.store_snapshot=` | Store full object snapshot alongside diffs |
|
|
572
|
+
| `.capture_request_metadata=` | Capture `remote_ip` and `user_agent` |
|
|
573
|
+
| `.version_limit=` | Global entry cap per record |
|
|
574
|
+
| `.async=` | Write audit entries via `WriteAuditLogJob` |
|
|
575
|
+
| `.connects_to=` | Route `AuditLogEntry` to a separate database |
|
|
576
|
+
| `.page_size=` | Entries per page in the web dashboard |
|
|
577
|
+
| `.whodunnit_display=` | Proc for actor display name snapshot |
|
|
578
|
+
| `.retention_period=` | _(1.1.0)_ Global time-based TTL |
|
|
579
|
+
|
|
580
|
+
**Concerns**
|
|
581
|
+
|
|
582
|
+
| Class | Include in | Key methods |
|
|
583
|
+
|---|---|---|
|
|
584
|
+
| `RailsAuditLog::Auditable` | ActiveRecord models | `audit_log(only:, ignore:, meta:, associations:, version_limit:, async:)`, `skip_audit_log { }` |
|
|
585
|
+
| `RailsAuditLog::Controller` | ActionController | `audit_log_actor { }` |
|
|
586
|
+
|
|
587
|
+
**Model — `RailsAuditLog::AuditLogEntry`**
|
|
588
|
+
|
|
589
|
+
Scopes: `created_events`, `updated_events`, `destroyed_events`, `by_actor`, `for_resource`, `since`, `until`, `touching`, `slim`, `for_period`
|
|
590
|
+
|
|
591
|
+
Instance methods: `reify`, `previous`, `next`, `diff`, `changed_attributes`, `actor`
|
|
592
|
+
|
|
593
|
+
Constants: `EVENTS`, `BLOB_COLUMNS`, `PERIODS`
|
|
594
|
+
|
|
595
|
+
**Testing helpers**
|
|
596
|
+
|
|
597
|
+
| Module | Require | Usage |
|
|
598
|
+
|---|---|---|
|
|
599
|
+
| `RailsAuditLog::Matchers` | `require "rails_audit_log/matchers"` | RSpec: `have_audit_log_entry`, `create_audit_log_entry` |
|
|
600
|
+
| `RailsAuditLog::MinitestAssertions` | `require "rails_audit_log/minitest_assertions"` | Minitest: `assert_audit_log_entry`, `refute_audit_log_entry` |
|
|
601
|
+
| `RailsAuditLog::TestHelpers` | `require "rails_audit_log/test_helpers"` | `without_audit_log { }` |
|
|
602
|
+
|
|
603
|
+
**Jobs** (enqueued internally; configure via ActiveJob)
|
|
604
|
+
|
|
605
|
+
`RailsAuditLog::WriteAuditLogJob` — do not instantiate directly; enqueued when `async: true` is set.
|
|
606
|
+
|
|
607
|
+
**Generators**
|
|
608
|
+
|
|
609
|
+
`rails generate rails_audit_log:install` · `rails generate rails_audit_log:initializer`
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Migrating from PaperTrail
|
|
614
|
+
|
|
615
|
+
[↑ Table of contents](#table-of-contents)
|
|
616
|
+
|
|
617
|
+
Run the migration generator to produce a timestamped data migration:
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
bin/rails generate rails_audit_log:migrate_from_paper_trail
|
|
621
|
+
bin/rails db:migrate
|
|
447
622
|
```
|
|
448
623
|
|
|
449
|
-
|
|
624
|
+
The generated migration reads every row from PaperTrail's `versions` table and inserts it into `audit_log_entries`.
|
|
625
|
+
|
|
626
|
+
### Column mapping
|
|
627
|
+
|
|
628
|
+
| PaperTrail `versions` | `audit_log_entries` | Notes |
|
|
629
|
+
|---|---|---|
|
|
630
|
+
| `item_type` | `item_type` | Direct copy |
|
|
631
|
+
| `item_id` | `item_id` | Direct copy |
|
|
632
|
+
| `event` | `event` | `create` / `update` / `destroy` only; other values are skipped |
|
|
633
|
+
| `object_changes` | `object_changes` | YAML or JSON → JSON (see below) |
|
|
634
|
+
| `object` | `object` | YAML or JSON → JSON (see below) |
|
|
635
|
+
| `whodunnit` | `whodunnit_snapshot` | PaperTrail stores actor as a plain string |
|
|
636
|
+
| `created_at` | `created_at` | Direct copy |
|
|
637
|
+
| — | `actor_type` / `actor_id` | **Not populated** — cannot be inferred from a string `whodunnit` |
|
|
450
638
|
|
|
451
|
-
|
|
639
|
+
### YAML and JSON serialization
|
|
640
|
+
|
|
641
|
+
PaperTrail serializes `object` and `object_changes` as **YAML** by default and as **JSON** when `PaperTrail.serializer = PaperTrail::Serializers::JSON` is configured. The migration handles both transparently — it tries JSON first, then falls back to YAML (with a permissive class list for the Ruby types PaperTrail commonly serializes).
|
|
642
|
+
|
|
643
|
+
### Compatibility shim for gradual migration
|
|
644
|
+
|
|
645
|
+
If you need your codebase to keep using PaperTrail's API while migrating, include `RailsAuditLog::PaperTrailCompat` alongside `Auditable`:
|
|
452
646
|
|
|
453
647
|
```ruby
|
|
454
|
-
require "rails_audit_log/
|
|
648
|
+
require "rails_audit_log/paper_trail_compat"
|
|
455
649
|
|
|
456
|
-
|
|
457
|
-
|
|
650
|
+
class Article < ApplicationRecord
|
|
651
|
+
include RailsAuditLog::Auditable
|
|
652
|
+
include RailsAuditLog::PaperTrailCompat
|
|
458
653
|
end
|
|
459
654
|
```
|
|
460
655
|
|
|
461
|
-
|
|
656
|
+
This adds the familiar PaperTrail surface:
|
|
462
657
|
|
|
463
658
|
```ruby
|
|
464
|
-
#
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
# Assert a block creates a matching audit entry
|
|
470
|
-
expect { post.update!(title: "New") }.to create_audit_log_entry
|
|
471
|
-
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update)
|
|
472
|
-
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update).touching(:title)
|
|
659
|
+
article.versions # audit_log_entries ordered oldest-first
|
|
660
|
+
article.paper_trail.version # most recent AuditLogEntry
|
|
661
|
+
article.paper_trail.previous_version # reconstructed previous state (reify)
|
|
662
|
+
article.paper_trail.originator # whodunnit_snapshot string
|
|
663
|
+
article.paper_trail.version_at(1.week.ago) # reconstructed state at a point in time
|
|
473
664
|
```
|
|
474
665
|
|
|
666
|
+
`AuditLogEntry#reify` already matches PaperTrail's `Version#reify` — no additional alias needed.
|
|
667
|
+
|
|
668
|
+
### What is not migrated
|
|
669
|
+
|
|
670
|
+
- `versions` rows with an `event` value outside `create`, `update`, `destroy` (custom events added by some apps)
|
|
671
|
+
- `actor_type` and `actor_id` — PaperTrail's `whodunnit` is a plain string and does not identify the actor model
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Companion gems
|
|
676
|
+
|
|
677
|
+
[↑ Table of contents](#table-of-contents)
|
|
678
|
+
|
|
679
|
+
> Coming in the 1.x series
|
|
680
|
+
|
|
681
|
+
| Gem | What it adds |
|
|
682
|
+
|---|---|
|
|
683
|
+
| `rails_audit_log-graphql` | Mountable GraphQL endpoint at `/audit/graphql` — queryable audit trail for API-first apps without forcing `graphql-ruby` on users who don't need it |
|
|
684
|
+
|
|
685
|
+
Each companion gem declares `rails_audit_log` as a dependency so you only add what you use.
|
|
686
|
+
|
|
475
687
|
## Requirements
|
|
476
688
|
|
|
689
|
+
[↑ Table of contents](#table-of-contents)
|
|
690
|
+
|
|
477
691
|
- Ruby >= 3.3
|
|
478
692
|
- Rails >= 7.2
|
|
479
693
|
|
|
694
|
+
## Performance
|
|
695
|
+
|
|
696
|
+
[↑ Table of contents](#table-of-contents)
|
|
697
|
+
|
|
698
|
+
See [BENCHMARKS.md](BENCHMARKS.md) for write throughput, `batch_audit` gains, query performance, storage efficiency, and notes on comparing against PaperTrail. To run the suite locally:
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
bundle exec rake dev:setup
|
|
702
|
+
bundle exec rake benchmark
|
|
703
|
+
```
|
|
704
|
+
|
|
480
705
|
## Contributing
|
|
481
706
|
|
|
707
|
+
[↑ Table of contents](#table-of-contents)
|
|
708
|
+
|
|
482
709
|
Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_audit_log).
|
|
483
710
|
|
|
484
711
|
## License
|
|
485
712
|
|
|
713
|
+
[↑ Table of contents](#table-of-contents)
|
|
714
|
+
|
|
486
715
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
|
@@ -16,6 +16,11 @@ end
|
|
|
16
16
|
|
|
17
17
|
task default: ["bundle:audit:update", "bundle:audit:check", :rubocop, :zeitwerk, :spec]
|
|
18
18
|
|
|
19
|
+
desc "Run the performance benchmark suite (requires dev:setup first)"
|
|
20
|
+
task :benchmark do
|
|
21
|
+
sh "bundle exec ruby benchmarks/suite.rb"
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
# Development database tasks (operate on spec/dummy)
|
|
20
25
|
namespace :dev do
|
|
21
26
|
dummy_env = { "RAILS_ENV" => "development" }
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# Include in any ActiveRecord model to automatically track +create+, +update+,
|
|
3
|
+
# and +destroy+ events as {AuditLogEntry} records.
|
|
4
|
+
#
|
|
5
|
+
# == Basic usage
|
|
6
|
+
#
|
|
7
|
+
# class Article < ApplicationRecord
|
|
8
|
+
# include RailsAuditLog::Auditable
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# == Configuring tracking
|
|
12
|
+
#
|
|
13
|
+
# class Article < ApplicationRecord
|
|
14
|
+
# include RailsAuditLog::Auditable
|
|
15
|
+
# audit_log only: %i[title body],
|
|
16
|
+
# meta: { tenant_id: -> { Current.tenant_id } },
|
|
17
|
+
# version_limit: 50,
|
|
18
|
+
# async: true
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Adds a polymorphic +has_many :audit_log_entries+ association to the model.
|
|
2
22
|
module Auditable
|
|
3
23
|
extend ActiveSupport::Concern
|
|
4
24
|
|
|
@@ -12,6 +32,8 @@ module RailsAuditLog
|
|
|
12
32
|
|
|
13
33
|
_warn_if_audit_table_missing
|
|
14
34
|
|
|
35
|
+
# All {AuditLogEntry} records for this object, newest first by default.
|
|
36
|
+
# Destroyed when the object is destroyed.
|
|
15
37
|
has_many :audit_log_entries,
|
|
16
38
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
17
39
|
as: :item,
|
|
@@ -54,6 +76,7 @@ module RailsAuditLog
|
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
class_methods do
|
|
79
|
+
# @api private
|
|
57
80
|
def _warn_if_audit_table_missing
|
|
58
81
|
return if connection.table_exists?("audit_log_entries")
|
|
59
82
|
|
|
@@ -66,6 +89,33 @@ module RailsAuditLog
|
|
|
66
89
|
# DB not reachable during this phase (e.g. before db:create) — skip the check
|
|
67
90
|
end
|
|
68
91
|
|
|
92
|
+
# Configures auditing options for this model. Call once in the class body
|
|
93
|
+
# after +include RailsAuditLog::Auditable+.
|
|
94
|
+
#
|
|
95
|
+
# @param only [Array<Symbol>, nil] whitelist of attributes to track;
|
|
96
|
+
# when set, all other attributes are ignored regardless of +ignore:+
|
|
97
|
+
# @param ignore [Array<Symbol>, nil] additional attributes to exclude on
|
|
98
|
+
# top of {RailsAuditLog.ignored_attributes}; ignored when +only:+ is set
|
|
99
|
+
# @param meta [Hash{Symbol => Proc}, nil] per-entry metadata; each value
|
|
100
|
+
# is a lambda called at write time — zero-argument lambdas receive no
|
|
101
|
+
# arguments, one-argument lambdas receive the record instance
|
|
102
|
+
# @param associations [Boolean, Array<Symbol>, nil] when +true+, tracks
|
|
103
|
+
# all +has_many+ and +has_and_belongs_to_many+ associations; pass an
|
|
104
|
+
# array of association names to track only specific ones
|
|
105
|
+
# @param version_limit [Integer, nil] maximum number of entries to retain
|
|
106
|
+
# per record; oldest entries are pruned after each write; overrides
|
|
107
|
+
# {RailsAuditLog.version_limit} for this model
|
|
108
|
+
# @param async [Boolean, nil] when +true+, writes are dispatched via
|
|
109
|
+
# +WriteAuditLogJob+; overrides {RailsAuditLog.async} for this model
|
|
110
|
+
# @return [void]
|
|
111
|
+
# @example
|
|
112
|
+
# class Article < ApplicationRecord
|
|
113
|
+
# include RailsAuditLog::Auditable
|
|
114
|
+
# audit_log only: %i[title body published_at],
|
|
115
|
+
# meta: { tenant_id: -> { Current.tenant_id } },
|
|
116
|
+
# associations: %i[tags],
|
|
117
|
+
# version_limit: 100
|
|
118
|
+
# end
|
|
69
119
|
def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
|
|
70
120
|
self._audit_log_only = only.map(&:to_s) if only
|
|
71
121
|
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
@@ -76,6 +126,13 @@ module RailsAuditLog
|
|
|
76
126
|
end
|
|
77
127
|
end
|
|
78
128
|
|
|
129
|
+
# Executes the block with audit logging disabled for this record's writes.
|
|
130
|
+
# A convenience wrapper around {RailsAuditLog.disable}.
|
|
131
|
+
#
|
|
132
|
+
# @yield executes the block without recording any audit entries
|
|
133
|
+
# @return [Object] the return value of the block
|
|
134
|
+
# @example Skip auditing during a bulk update
|
|
135
|
+
# post.skip_audit_log { post.update!(cached_at: Time.current) }
|
|
79
136
|
def skip_audit_log
|
|
80
137
|
RailsAuditLog.disable { yield }
|
|
81
138
|
end
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
|
+
# Include in +ApplicationController+ (or any controller) to automatically
|
|
3
|
+
# set and clear the current actor for every request, so that audit entries
|
|
4
|
+
# written during the request are tagged with the signed-in user.
|
|
5
|
+
#
|
|
6
|
+
# == Usage
|
|
7
|
+
#
|
|
8
|
+
# class ApplicationController < ActionController::Base
|
|
9
|
+
# include RailsAuditLog::Controller
|
|
10
|
+
# audit_log_actor { current_user }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# The block passed to {audit_log_actor} is evaluated in controller instance
|
|
14
|
+
# context on every request via a +before_action+. The actor is cleared in an
|
|
15
|
+
# +after_action+ so it never leaks between requests.
|
|
16
|
+
#
|
|
17
|
+
# Request metadata (+remote_ip+, +user_agent+) is captured automatically when
|
|
18
|
+
# {RailsAuditLog.capture_request_metadata} is +true+.
|
|
2
19
|
module Controller
|
|
3
20
|
extend ActiveSupport::Concern
|
|
4
21
|
|
|
@@ -10,10 +27,22 @@ module RailsAuditLog
|
|
|
10
27
|
end
|
|
11
28
|
|
|
12
29
|
class_methods do
|
|
30
|
+
# Registers a block that resolves the current actor for each request.
|
|
31
|
+
# The block is evaluated in controller instance context, so any helper
|
|
32
|
+
# method available to the controller (e.g. +current_user+) can be used.
|
|
33
|
+
#
|
|
34
|
+
# @yield block evaluated in controller context; should return the actor
|
|
35
|
+
# @return [void]
|
|
36
|
+
# @example
|
|
37
|
+
# audit_log_actor { current_user }
|
|
38
|
+
#
|
|
39
|
+
# # With a one-argument lambda style (actor = the controller instance)
|
|
40
|
+
# audit_log_actor { |c| c.current_user }
|
|
13
41
|
def audit_log_actor(&block)
|
|
14
42
|
@audit_log_actor_block = block
|
|
15
43
|
end
|
|
16
44
|
|
|
45
|
+
# @api private
|
|
17
46
|
def audit_log_actor_block
|
|
18
47
|
@audit_log_actor_block
|
|
19
48
|
end
|