rails_audit_log 0.8.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 +294 -55
- data/Rakefile +5 -0
- data/app/assets/stylesheets/rails_audit_log/_01_base.css +32 -0
- data/app/assets/stylesheets/rails_audit_log/_02_layout.css +34 -0
- data/app/assets/stylesheets/rails_audit_log/_03_table.css +39 -0
- data/app/assets/stylesheets/rails_audit_log/_04_badges.css +13 -0
- data/app/assets/stylesheets/rails_audit_log/_05_diff.css +94 -0
- data/app/assets/stylesheets/rails_audit_log/_06_timeline.css +21 -0
- data/app/assets/stylesheets/rails_audit_log/_07_detail.css +15 -0
- data/app/assets/stylesheets/rails_audit_log/_08_pagination.css +56 -0
- data/app/assets/stylesheets/rails_audit_log/_09_filters.css +54 -0
- data/app/assets/stylesheets/rails_audit_log/application.css +1 -15
- 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 +13 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +32 -0
- data/app/controllers/rails_audit_log/resources_controller.rb +14 -0
- data/app/helpers/rails_audit_log/application_helper.rb +14 -0
- data/app/javascript/rails_audit_log/application.js +8 -0
- data/app/javascript/rails_audit_log/diff_controller.js +20 -0
- data/app/javascript/rails_audit_log/search_controller.js +12 -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 +127 -25
- data/app/views/layouts/rails_audit_log/application.html.erb +16 -10
- data/app/views/rails_audit_log/audit_log_entries/_diff.html.erb +55 -0
- data/app/views/rails_audit_log/audit_log_entries/index.html.erb +76 -0
- data/app/views/rails_audit_log/audit_log_entries/show.html.erb +50 -0
- data/app/views/rails_audit_log/resources/show.html.erb +35 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb +10 -0
- 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 +29 -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 +173 -5
- metadata +70 -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,79 @@ 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.
|
|
117
|
+
|
|
118
|
+
## Web dashboard
|
|
119
|
+
|
|
120
|
+
[↑ Table of contents](#table-of-contents)
|
|
121
|
+
|
|
122
|
+
Mount the engine in `config/routes.rb` to enable the built-in audit trail browser:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
mount RailsAuditLog::Engine, at: "/audit"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Then visit `/audit` to browse all audit entries. The dashboard is delivered via propshaft, importmaps, and CDN-pinned Turbo and Stimulus — no asset pipeline configuration required in the host app.
|
|
33
129
|
|
|
34
130
|
## Usage
|
|
35
131
|
|
|
132
|
+
[↑ Table of contents](#table-of-contents)
|
|
133
|
+
|
|
36
134
|
### Tracking a model
|
|
37
135
|
|
|
38
136
|
Include `RailsAuditLog::Auditable` in any ActiveRecord model:
|
|
@@ -110,19 +208,6 @@ entry.diff
|
|
|
110
208
|
# => { "title" => { from: "Hello", to: "World" } }
|
|
111
209
|
```
|
|
112
210
|
|
|
113
|
-
### Separate audit database
|
|
114
|
-
|
|
115
|
-
Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
# config/initializers/rails_audit_log.rb
|
|
119
|
-
RailsAuditLog.connects_to = {
|
|
120
|
-
database: { writing: :audit_log, reading: :audit_log }
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
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.
|
|
125
|
-
|
|
126
211
|
### Lightweight queries
|
|
127
212
|
|
|
128
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):
|
|
@@ -283,22 +368,20 @@ article.skip_audit_log { article.update!(cached_at: Time.current) }
|
|
|
283
368
|
|
|
284
369
|
### Object reconstruction
|
|
285
370
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
`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:
|
|
289
372
|
|
|
290
373
|
```ruby
|
|
291
374
|
article.update!(title: "v2")
|
|
292
375
|
entry = article.audit_log_entries.updated_events.last
|
|
293
376
|
|
|
294
377
|
previous = entry.reify
|
|
295
|
-
previous.title
|
|
378
|
+
previous.title # => "v1" (the pre-update state)
|
|
296
379
|
previous.persisted? # => false
|
|
297
380
|
```
|
|
298
381
|
|
|
299
382
|
Returns `nil` for `create` entries (nothing existed before).
|
|
300
383
|
|
|
301
|
-
|
|
384
|
+
**Reconstruct state at any point in time:**
|
|
302
385
|
|
|
303
386
|
```ruby
|
|
304
387
|
snapshot = RailsAuditLog.version_at(article, 1.week.ago)
|
|
@@ -307,7 +390,7 @@ snapshot.title # => whatever the title was a week ago
|
|
|
307
390
|
|
|
308
391
|
Returns `nil` if the record had no history at that time or was already destroyed.
|
|
309
392
|
|
|
310
|
-
|
|
393
|
+
**Navigate the version chain:**
|
|
311
394
|
|
|
312
395
|
```ruby
|
|
313
396
|
entry = article.audit_log_entries.updated_events.last
|
|
@@ -392,31 +475,58 @@ To save storage at the cost of reduced reification accuracy, switch to diff-only
|
|
|
392
475
|
RailsAuditLog.store_snapshot = false
|
|
393
476
|
```
|
|
394
477
|
|
|
395
|
-
###
|
|
478
|
+
### Separate audit database
|
|
396
479
|
|
|
397
|
-
|
|
480
|
+
Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
|
|
398
481
|
|
|
399
482
|
```ruby
|
|
400
|
-
#
|
|
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
|
|
492
|
+
|
|
493
|
+
**`RailsAuditLog::TestHelpers`** — silences audit tracking inside a block; useful in FactoryBot factories and seed data:
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
# spec/rails_helper.rb
|
|
401
497
|
require "rails_audit_log/test_helpers"
|
|
402
498
|
|
|
403
499
|
RSpec.configure do |config|
|
|
404
500
|
config.include RailsAuditLog::TestHelpers
|
|
405
501
|
end
|
|
502
|
+
```
|
|
406
503
|
|
|
407
|
-
|
|
408
|
-
FactoryBot
|
|
409
|
-
|
|
410
|
-
after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
|
|
411
|
-
end
|
|
412
|
-
end
|
|
504
|
+
```ruby
|
|
505
|
+
# Or in a FactoryBot factory:
|
|
506
|
+
after(:create) { |p| without_audit_log { p.update!(cached_at: Time.current) } }
|
|
413
507
|
```
|
|
414
508
|
|
|
415
509
|
`without_audit_log` is a prefix-free wrapper around `RailsAuditLog.disable` — thread-safe and restores tracking even if the block raises.
|
|
416
510
|
|
|
417
|
-
|
|
511
|
+
**`RailsAuditLog::Matchers`** (RSpec) — add to `spec/rails_helper.rb`:
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
require "rails_audit_log/matchers"
|
|
418
515
|
|
|
419
|
-
|
|
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`:
|
|
420
530
|
|
|
421
531
|
```ruby
|
|
422
532
|
require "rails_audit_log/minitest_assertions"
|
|
@@ -426,51 +536,180 @@ class ActiveSupport::TestCase
|
|
|
426
536
|
end
|
|
427
537
|
```
|
|
428
538
|
|
|
429
|
-
Then use the assertions in any test:
|
|
430
|
-
|
|
431
539
|
```ruby
|
|
432
|
-
assert_audit_log_entry post
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
437
622
|
```
|
|
438
623
|
|
|
439
|
-
|
|
624
|
+
The generated migration reads every row from PaperTrail's `versions` table and inserts it into `audit_log_entries`.
|
|
440
625
|
|
|
441
|
-
|
|
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` |
|
|
638
|
+
|
|
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`:
|
|
442
646
|
|
|
443
647
|
```ruby
|
|
444
|
-
require "rails_audit_log/
|
|
648
|
+
require "rails_audit_log/paper_trail_compat"
|
|
445
649
|
|
|
446
|
-
|
|
447
|
-
|
|
650
|
+
class Article < ApplicationRecord
|
|
651
|
+
include RailsAuditLog::Auditable
|
|
652
|
+
include RailsAuditLog::PaperTrailCompat
|
|
448
653
|
end
|
|
449
654
|
```
|
|
450
655
|
|
|
451
|
-
|
|
656
|
+
This adds the familiar PaperTrail surface:
|
|
452
657
|
|
|
453
658
|
```ruby
|
|
454
|
-
#
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
# Assert a block creates a matching audit entry
|
|
460
|
-
expect { post.update!(title: "New") }.to create_audit_log_entry
|
|
461
|
-
expect { post.update!(title: "New") }.to create_audit_log_entry(event: :update)
|
|
462
|
-
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
|
|
463
664
|
```
|
|
464
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
|
+
|
|
465
687
|
## Requirements
|
|
466
688
|
|
|
689
|
+
[↑ Table of contents](#table-of-contents)
|
|
690
|
+
|
|
467
691
|
- Ruby >= 3.3
|
|
468
692
|
- Rails >= 7.2
|
|
469
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
|
+
|
|
470
705
|
## Contributing
|
|
471
706
|
|
|
707
|
+
[↑ Table of contents](#table-of-contents)
|
|
708
|
+
|
|
472
709
|
Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_audit_log).
|
|
473
710
|
|
|
474
711
|
## License
|
|
475
712
|
|
|
713
|
+
[↑ Table of contents](#table-of-contents)
|
|
714
|
+
|
|
476
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" }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--ral-bg: #f5f5f5;
|
|
5
|
+
--ral-surface: #ffffff;
|
|
6
|
+
--ral-border: #e5e5e5;
|
|
7
|
+
--ral-border-subtle: #f0f0f0;
|
|
8
|
+
--ral-text: #1a1a1a;
|
|
9
|
+
--ral-muted: #666666;
|
|
10
|
+
--ral-primary: #1d4ed8;
|
|
11
|
+
--ral-danger: #991b1b;
|
|
12
|
+
--ral-success: #166534;
|
|
13
|
+
--ral-radius: 6px;
|
|
14
|
+
--ral-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
15
|
+
--ral-surface-hover: #fafafa;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
20
|
+
font-size: 14px;
|
|
21
|
+
line-height: 1.5;
|
|
22
|
+
color: var(--ral-text);
|
|
23
|
+
background: var(--ral-bg);
|
|
24
|
+
min-height: 100vh;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ral-link { color: var(--ral-primary); text-decoration: none; }
|
|
28
|
+
.ral-link:hover { text-decoration: underline; }
|
|
29
|
+
|
|
30
|
+
.ral-muted { color: var(--ral-muted); font-size: 13px; }
|
|
31
|
+
.ral-timestamp { color: var(--ral-muted); font-size: 13px; white-space: nowrap; }
|
|
32
|
+
.ral-reason { font-size: 12px; color: var(--ral-muted); font-style: italic; }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.ral-header {
|
|
2
|
+
background: var(--ral-text);
|
|
3
|
+
color: #fff;
|
|
4
|
+
height: 48px;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
padding: 0 24px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ral-header__inner {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: 16px;
|
|
14
|
+
width: 100%;
|
|
15
|
+
max-width: 1200px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ral-header__logo {
|
|
20
|
+
color: #fff;
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
font-size: 15px;
|
|
24
|
+
}
|
|
25
|
+
.ral-header__logo:hover { opacity: 0.8; }
|
|
26
|
+
|
|
27
|
+
.ral-main { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
28
|
+
|
|
29
|
+
.ral-page-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 20px; }
|
|
30
|
+
.ral-page-header__title { font-size: 20px; font-weight: 600; }
|
|
31
|
+
.ral-page-header__meta { font-size: 13px; color: var(--ral-muted); }
|
|
32
|
+
|
|
33
|
+
.ral-breadcrumb { font-size: 12px; color: var(--ral-muted); margin-bottom: 4px; }
|
|
34
|
+
.ral-section-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #333; }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.ral-table-wrap {
|
|
2
|
+
background: var(--ral-surface);
|
|
3
|
+
border: 1px solid var(--ral-border);
|
|
4
|
+
border-radius: var(--ral-radius);
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
box-shadow: var(--ral-shadow);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.ral-table { width: 100%; border-collapse: collapse; }
|
|
10
|
+
|
|
11
|
+
.ral-table th {
|
|
12
|
+
background: var(--ral-bg);
|
|
13
|
+
padding: 10px 16px;
|
|
14
|
+
text-align: left;
|
|
15
|
+
font-size: 12px;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
text-transform: uppercase;
|
|
18
|
+
letter-spacing: 0.04em;
|
|
19
|
+
color: var(--ral-muted);
|
|
20
|
+
border-bottom: 1px solid var(--ral-border);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ral-table td {
|
|
24
|
+
padding: 10px 16px;
|
|
25
|
+
border-bottom: 1px solid var(--ral-border-subtle);
|
|
26
|
+
vertical-align: middle;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ral-table tbody tr:last-child td { border-bottom: none; }
|
|
30
|
+
.ral-table tbody tr:hover { background: var(--ral-surface-hover); }
|
|
31
|
+
|
|
32
|
+
.ral-empty {
|
|
33
|
+
background: var(--ral-surface);
|
|
34
|
+
border: 1px solid var(--ral-border);
|
|
35
|
+
border-radius: var(--ral-radius);
|
|
36
|
+
padding: 40px;
|
|
37
|
+
text-align: center;
|
|
38
|
+
color: var(--ral-muted);
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.ral-badge {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
padding: 2px 8px;
|
|
4
|
+
border-radius: 10px;
|
|
5
|
+
font-size: 11px;
|
|
6
|
+
font-weight: 600;
|
|
7
|
+
text-transform: uppercase;
|
|
8
|
+
letter-spacing: 0.04em;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.ral-badge--create { background: #dcfce7; color: var(--ral-success); }
|
|
12
|
+
.ral-badge--update { background: #dbeafe; color: var(--ral-primary); }
|
|
13
|
+
.ral-badge--destroy { background: #fee2e2; color: var(--ral-danger); }
|