auditron 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 +7 -0
- data/CHANGELOG.md +67 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +395 -0
- data/Rakefile +12 -0
- data/lib/auditron/audit_log.rb +70 -0
- data/lib/auditron/auditable.rb +107 -0
- data/lib/auditron/configuration.rb +26 -0
- data/lib/auditron/railtie.rb +26 -0
- data/lib/auditron/sweeper.rb +19 -0
- data/lib/auditron/version.rb +5 -0
- data/lib/auditron.rb +27 -0
- data/lib/generators/auditron/install/install_generator.rb +121 -0
- data/lib/generators/auditron/install/templates/create_audit_logs.rb.erb +21 -0
- data/sig/auditron.rbs +44 -0
- metadata +179 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 44f360e57980c1ff2fd3b0c87daefd2bad45eb643b588cd38dea1e5912f2af80
|
|
4
|
+
data.tar.gz: 1f14c7d53bd1689b5ee39e5b5bc1fc99d05bf58d2e5f0426287193a95b3edb72
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 278d96b4441fa49d9f4124dff4ac5917c4ab38b09265e53d2346bea57140771dece7d3f70e0049ea6d33f31aa53b7d0e08541f3521e7a233e9f2821a55e5ddf9
|
|
7
|
+
data.tar.gz: 56c8b00ff1b7d71f7b6a7942039fd19c70c4a7716a9cafdf9f54545bcdaf2898741a2610b088c6f844625e8ba0a6fd4d4123c5830498efeff30372e7ffd3e5a3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Auditron will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-04-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Core audit logging** — tracks `created`, `updated`, and `deleted` on any ActiveRecord model
|
|
15
|
+
- **Diff-only storage** — stores only changed fields with before/after values, not full snapshots
|
|
16
|
+
- **`auditable` DSL** — include `Auditron::Auditable` and call `auditable` on any model
|
|
17
|
+
- `auditable` — track all fields
|
|
18
|
+
- `auditable only: [:email, :role]` — track specific fields only
|
|
19
|
+
- `auditable except: [:last_sign_in_at]` — exclude specific fields
|
|
20
|
+
- **Thread-safe actor tracking** — set `Auditron.current_actor = @current_user` in any controller
|
|
21
|
+
- Works with JWT, Devise, session-based auth, or any custom auth system
|
|
22
|
+
- Automatically cleared after every request — no leaking between requests
|
|
23
|
+
- **Custom metadata** — attach any extra context to a log entry via `audit_with`
|
|
24
|
+
- `account.audit_with(reason: "GDPR request").destroy`
|
|
25
|
+
- Stored as JSON, returned as Hash
|
|
26
|
+
- **Chainable query DSL** on `Auditron::AuditLog`
|
|
27
|
+
- `.for(record)` — all logs for a specific record
|
|
28
|
+
- `.by(actor)` — all changes made by a specific actor
|
|
29
|
+
- `.action(:updated)` — filter by action
|
|
30
|
+
- `.since(1.week.ago)` — filter by time
|
|
31
|
+
- Fully chainable: `.by(admin).action(:deleted).since(1.week.ago)`
|
|
32
|
+
- **`log.actor`** — polymorphic association returns the full actor object directly
|
|
33
|
+
- **`log.summary`** — human readable description e.g. `"Account #8 was updated by Account #1"`
|
|
34
|
+
- **`log.changed_fields`** — parses JSON automatically, returns Hash
|
|
35
|
+
- **`log.metadata`** — parses JSON automatically, returns Hash or nil
|
|
36
|
+
- **Built-in log retention** via `config.retention_days`
|
|
37
|
+
- `Auditron::Sweeper.purge!` — deletes all logs older than configured retention days
|
|
38
|
+
- Safe to call from Sidekiq, GoodJob, cron, or any background job
|
|
39
|
+
- **Request IP tracking** — opt-in via `config.store_ip = true`
|
|
40
|
+
- Handled automatically by Rack middleware — no extra setup needed
|
|
41
|
+
- **Global ignored fields** — `config.ignored_fields` excludes fields across all models
|
|
42
|
+
- **Install generator** — `rails generate auditron:install`
|
|
43
|
+
- Interactive CLI with step-by-step guidance
|
|
44
|
+
- Generates `audit_logs` migration with indexes
|
|
45
|
+
- Shows next steps after install
|
|
46
|
+
- **Migration** — creates `audit_logs` table with indexes on
|
|
47
|
+
`auditable`, `actor`, `action`, and `created_at`
|
|
48
|
+
- **Railtie** — auto-integrates into Rails apps, no manual setup needed
|
|
49
|
+
- **Database agnostic** — works with PostgreSQL, MySQL, and SQLite
|
|
50
|
+
- **Zero hard dependencies** beyond `activerecord`
|
|
51
|
+
- **Rails optional** — works in plain Ruby + ActiveRecord projects
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- `actor_type` and `actor_id` will be `nil` on unauthenticated requests
|
|
56
|
+
(e.g. signup) — this is correct behavior, not a bug
|
|
57
|
+
- Do not set `current_actor` in the initializer — set it in the controller
|
|
58
|
+
after authentication so the actor is always the authenticated user
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Compatibility
|
|
63
|
+
|
|
64
|
+
- Ruby `>= 3.0`
|
|
65
|
+
- ActiveRecord `>= 7.0`
|
|
66
|
+
- Rails `>= 7.0` (optional)
|
|
67
|
+
- PostgreSQL, MySQL, SQLite
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"auditron" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["shailendrapatidar00@gmail.com"](mailto:"shailendrapatidar00@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shailendra Patidar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Auditron
|
|
2
|
+
|
|
3
|
+
> Audit logging for API-first Rails apps — built-in retention, flexible actor tracking, and a clean query DSL.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why Auditron?
|
|
8
|
+
|
|
9
|
+
Most audit gems were built for traditional Rails apps with session-based auth.
|
|
10
|
+
If you are building an API with JWT, service objects, or background jobs — they
|
|
11
|
+
get in your way fast.
|
|
12
|
+
|
|
13
|
+
`paper_trail` stores full object snapshots on every change. Change one column
|
|
14
|
+
on a model with 30 attributes and it writes all 30 to the database, every time.
|
|
15
|
+
At scale, this becomes a serious storage problem.
|
|
16
|
+
|
|
17
|
+
`audited` and `paper_trail` both assume controller-based actor tracking tied to
|
|
18
|
+
`current_user` — which does not exist in JWT or service-layer contexts.
|
|
19
|
+
|
|
20
|
+
Neither gem ships with log retention. You always end up writing your own cleanup job.
|
|
21
|
+
|
|
22
|
+
**Auditron was designed for how modern Rails APIs are actually built:**
|
|
23
|
+
|
|
24
|
+
- JWT and service-layer friendly — set the actor anywhere, not just in controllers
|
|
25
|
+
- Diff-only storage — stores only what changed, not the full object
|
|
26
|
+
- Built-in retention — configure once, run a job, logs clean themselves up
|
|
27
|
+
- Chainable query DSL — find exactly what you need without writing raw SQL
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Who needs this?
|
|
32
|
+
|
|
33
|
+
- API-first Rails apps using JWT or token-based auth
|
|
34
|
+
- Apps under **GDPR, HIPAA, or SOC2** compliance requirements that need audit trails
|
|
35
|
+
- Teams tired of paper_trail bloating their database
|
|
36
|
+
- Apps that need to answer **"who changed this, when, and why?"**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## At a glance
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Account < ApplicationRecord
|
|
44
|
+
include Auditron::Auditable
|
|
45
|
+
auditable only: [:email, :role, :status]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# someone updates their profile...
|
|
49
|
+
account.update!(first_name: "Jane")
|
|
50
|
+
|
|
51
|
+
# now you know exactly what happened
|
|
52
|
+
account.audit_logs.last
|
|
53
|
+
# => #<AuditLog
|
|
54
|
+
# action: "updated"
|
|
55
|
+
# changed_fields: { "first_name" => ["John", "Jane"] }
|
|
56
|
+
# actor_type: "Account"
|
|
57
|
+
# actor_id: 1
|
|
58
|
+
# ip_address: "192.168.1.1"
|
|
59
|
+
# created_at: "2026-04-04T08:00:00Z"
|
|
60
|
+
# >
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
Add to your Gemfile:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
gem "auditron"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then run:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bundle install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Generate and run the migration:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
rails generate auditron:install
|
|
83
|
+
rails db:migrate
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
Create an initializer at `config/initializers/auditron.rb`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
Auditron.configure do |config|
|
|
94
|
+
# Fields to never log across all models
|
|
95
|
+
config.ignored_fields = %i[updated_at created_at]
|
|
96
|
+
|
|
97
|
+
# Include request IP in every log entry (default: false)
|
|
98
|
+
config.store_ip = true
|
|
99
|
+
|
|
100
|
+
# Auto-purge logs older than N days (default: nil — keep forever)
|
|
101
|
+
# Call Auditron::Sweeper.purge! from a scheduled job
|
|
102
|
+
config.retention_days = 90
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> **Note:** Do not set `current_actor` in the initializer.
|
|
107
|
+
> Instance variables like `@current_user` are not available there —
|
|
108
|
+
> they only exist during a request. See Controller Setup below.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Controller Setup
|
|
113
|
+
|
|
114
|
+
Auditron needs to know who is making changes. Set the current actor
|
|
115
|
+
in your controller after authentication — works with **any auth system**.
|
|
116
|
+
|
|
117
|
+
Auditron stores it in a **thread-safe variable** and clears it
|
|
118
|
+
automatically after every request. Safe for Puma and any threaded server.
|
|
119
|
+
|
|
120
|
+
### JWT (API apps)
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class ApplicationController < ActionController::API
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def authenticate_account!
|
|
127
|
+
token = request.headers["Authorization"]&.split(" ")&.last
|
|
128
|
+
payload = JsonAuthToken.decode(token)
|
|
129
|
+
@current_user = Account.find_by(id: payload[:account_id])
|
|
130
|
+
|
|
131
|
+
render json: { error: "Invalid token" }, status: :unauthorized and return unless @current_user
|
|
132
|
+
|
|
133
|
+
# Set the actor — Auditron reads this on every model change
|
|
134
|
+
Auditron.current_actor = @current_user
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Devise
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
class ApplicationController < ActionController::Base
|
|
143
|
+
before_action :set_audit_actor
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def set_audit_actor
|
|
148
|
+
Auditron.current_actor = current_user
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Session based (no Devise)
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class ApplicationController < ActionController::Base
|
|
157
|
+
before_action :set_audit_actor
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def current_user
|
|
162
|
+
@current_user ||= User.find_by(id: session[:user_id])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def set_audit_actor
|
|
166
|
+
Auditron.current_actor = current_user
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Service objects or background jobs
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Set and clear manually — Auditron does not clear this automatically
|
|
175
|
+
# outside of a request cycle
|
|
176
|
+
Auditron.current_actor = admin_user
|
|
177
|
+
account.update!(status: "suspended")
|
|
178
|
+
Auditron.current_actor = nil
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> **Important:** On signup or any unauthenticated request, `actor_type`
|
|
182
|
+
> and `actor_id` will be `nil` — this is correct behavior.
|
|
183
|
+
> The user does not exist yet so there is no actor to record.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Usage
|
|
188
|
+
|
|
189
|
+
### Track all changes on a model
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class Account < ApplicationRecord
|
|
193
|
+
include Auditron::Auditable
|
|
194
|
+
auditable
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Track only specific fields
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
class Account < ApplicationRecord
|
|
202
|
+
include Auditron::Auditable
|
|
203
|
+
auditable only: [:email, :role, :status]
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Exclude specific fields
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
class Account < ApplicationRecord
|
|
211
|
+
include Auditron::Auditable
|
|
212
|
+
auditable except: [:last_sign_in_at, :login_count]
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Attach custom metadata to a log entry
|
|
217
|
+
|
|
218
|
+
Pass any extra context you want stored alongside the log:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# Simple reason
|
|
222
|
+
account.audit_with(reason: "user requested name change").update!(first_name: "Jane")
|
|
223
|
+
|
|
224
|
+
# Support ticket reference
|
|
225
|
+
account.audit_with(
|
|
226
|
+
reason: "admin override",
|
|
227
|
+
ticket: "SUPPORT-1234",
|
|
228
|
+
note: "user forgot old email"
|
|
229
|
+
).update!(email: "new@example.com")
|
|
230
|
+
|
|
231
|
+
# GDPR deletion
|
|
232
|
+
account.audit_with(reason: "GDPR deletion request").destroy
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Metadata is stored as JSON and returned as a Hash:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
account.audit_logs.last.metadata
|
|
239
|
+
# => { "reason" => "admin override", "ticket" => "SUPPORT-1234" }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Querying audit logs
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# All logs for a specific record
|
|
248
|
+
account.audit_logs
|
|
249
|
+
|
|
250
|
+
# Same, via class method
|
|
251
|
+
Auditron::AuditLog.for(account)
|
|
252
|
+
|
|
253
|
+
# All changes made by a specific actor
|
|
254
|
+
Auditron::AuditLog.by(admin)
|
|
255
|
+
|
|
256
|
+
# Filter by action
|
|
257
|
+
Auditron::AuditLog.action(:updated)
|
|
258
|
+
Auditron::AuditLog.action(:deleted)
|
|
259
|
+
Auditron::AuditLog.action(:created)
|
|
260
|
+
|
|
261
|
+
# Filter by time
|
|
262
|
+
Auditron::AuditLog.since(1.week.ago)
|
|
263
|
+
Auditron::AuditLog.since(1.month.ago)
|
|
264
|
+
|
|
265
|
+
# Chain them
|
|
266
|
+
Auditron::AuditLog.by(admin).action(:deleted).since(1.week.ago)
|
|
267
|
+
|
|
268
|
+
# Get actor object directly from log
|
|
269
|
+
log = account.audit_logs.last
|
|
270
|
+
log.actor # => full Account/User/Admin object
|
|
271
|
+
log.actor_type # => "Account"
|
|
272
|
+
log.actor_id # => 1
|
|
273
|
+
|
|
274
|
+
# Human readable summary
|
|
275
|
+
log.summary
|
|
276
|
+
# => "Account #8 was updated by Account #1"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Log retention (Sweeper)
|
|
282
|
+
|
|
283
|
+
No other major audit gem ships with built-in log retention.
|
|
284
|
+
Configure once and run from any scheduled job:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# config/initializers/auditron.rb
|
|
288
|
+
config.retention_days = 90 # keep logs for 90 days
|
|
289
|
+
|
|
290
|
+
# Call from a scheduled job (Sidekiq, GoodJob, cron)
|
|
291
|
+
Auditron::Sweeper.purge! # deletes all logs older than retention_days
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Example with a background job:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
class AuditLogCleanupJob < ApplicationJob
|
|
298
|
+
def perform
|
|
299
|
+
Auditron::Sweeper.purge!
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Log entry structure
|
|
307
|
+
|
|
308
|
+
Every `AuditLog` record contains:
|
|
309
|
+
|
|
310
|
+
| Field | Type | Description |
|
|
311
|
+
|-------|------|-------------|
|
|
312
|
+
| `auditable_type` | String | Model class name e.g. `"Account"` |
|
|
313
|
+
| `auditable_id` | Integer | Record ID |
|
|
314
|
+
| `action` | String | `created`, `updated`, or `deleted` |
|
|
315
|
+
| `changed_fields` | JSON | Only changed fields with before/after values |
|
|
316
|
+
| `actor_id` | Integer | ID of the actor who made the change |
|
|
317
|
+
| `actor_type` | String | Class name of the actor e.g. `"Account"` |
|
|
318
|
+
| `ip_address` | String | Request IP (when `store_ip: true`) |
|
|
319
|
+
| `metadata` | JSON | Custom data attached via `audit_with` |
|
|
320
|
+
| `created_at` | DateTime | When the change happened |
|
|
321
|
+
|
|
322
|
+
### Example log entry
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
#<Auditron::AuditLog
|
|
326
|
+
id: 7,
|
|
327
|
+
auditable_type: "Account",
|
|
328
|
+
auditable_id: 8,
|
|
329
|
+
action: "updated",
|
|
330
|
+
changed_fields: { "first_name" => ["John", "Jane"] },
|
|
331
|
+
actor_type: "Account",
|
|
332
|
+
actor_id: 1,
|
|
333
|
+
ip_address: "::1",
|
|
334
|
+
metadata: { "reason" => "user requested name change" },
|
|
335
|
+
created_at: Sat, 04 Apr 2026 13:38:08 UTC
|
|
336
|
+
>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## How it compares
|
|
342
|
+
|
|
343
|
+
This is an honest comparison. Every gem has strengths — pick the right tool for your use case.
|
|
344
|
+
|
|
345
|
+
| Feature | Auditron | PaperTrail | Audited | Logidze |
|
|
346
|
+
|---------|----------|------------|---------|---------|
|
|
347
|
+
| Storage model | Diff only | Full snapshot | Diff (changes) | Diff (JSONB) |
|
|
348
|
+
| PostgreSQL | ✅ | ✅ | ✅ | ✅ |
|
|
349
|
+
| MySQL | ✅ | ✅ | ✅ | ❌ |
|
|
350
|
+
| SQLite | ✅ | ✅ | ✅ | ❌ |
|
|
351
|
+
| Built-in retention | ✅ | ❌ | ❌ | ❌ |
|
|
352
|
+
| Custom metadata | ✅ clean DSL | ⚠️ via `meta` config | ⚠️ limited | ❌ |
|
|
353
|
+
| Actor tracking | Thread-local, set anywhere | `whodunnit` (controller) | `current_user` (controller) | Custom |
|
|
354
|
+
| JWT / API friendly | ✅ | ⚠️ needs workaround | ⚠️ needs workaround | ⚠️ moderate |
|
|
355
|
+
| Background job support | ✅ set manually | ⚠️ manual wiring | ⚠️ manual wiring | ⚠️ manual |
|
|
356
|
+
| Chainable query DSL | ✅ | ❌ raw AR queries | ❌ raw AR queries | ❌ raw JSON ops |
|
|
357
|
+
| Rails required | ⚠️ ActiveRecord required | ✅ Rails required | ✅ Rails required | ✅ Rails required |
|
|
358
|
+
| Performance (large data) | ⚠️ unverified | ❌ heavy (full snapshots) | ⚠️ medium | ✅ optimized (JSONB) |
|
|
359
|
+
| Maturity | 🆕 new | ✅ battle-tested | ✅ battle-tested | ✅ stable |
|
|
360
|
+
|
|
361
|
+
**When to choose Auditron:**
|
|
362
|
+
- You are building an API-first app with JWT or token-based auth
|
|
363
|
+
- You need built-in log retention without writing your own cleanup
|
|
364
|
+
- You want a clean query DSL instead of raw ActiveRecord queries
|
|
365
|
+
- You set the actor from service objects or background jobs
|
|
366
|
+
|
|
367
|
+
**When to choose PaperTrail:**
|
|
368
|
+
- You need full version history and the ability to revert records
|
|
369
|
+
- You are on a traditional session-based Rails app
|
|
370
|
+
- You need a battle-tested, widely supported gem
|
|
371
|
+
|
|
372
|
+
**When to choose Logidze:**
|
|
373
|
+
- You are on PostgreSQL and need maximum query performance
|
|
374
|
+
- You want diff storage backed by native JSONB operations
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Compatibility
|
|
379
|
+
|
|
380
|
+
- Ruby `>= 3.0`
|
|
381
|
+
- ActiveRecord `>= 7.0`
|
|
382
|
+
- Rails `>= 7.0` (optional — works with ActiveRecord outside Rails)
|
|
383
|
+
- PostgreSQL, MySQL, SQLite
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Contributing
|
|
388
|
+
|
|
389
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditron
|
|
4
|
+
class AuditLog < ActiveRecord::Base
|
|
5
|
+
self.table_name = "audit_logs"
|
|
6
|
+
|
|
7
|
+
# Polymorphic — belongs to any audited model
|
|
8
|
+
belongs_to :auditable, polymorphic: true, optional: true
|
|
9
|
+
|
|
10
|
+
# Polymorphic — belongs to any actor (User, AdminUser, etc.)
|
|
11
|
+
belongs_to :actor, polymorphic: true, optional: true
|
|
12
|
+
|
|
13
|
+
validates :action, presence: true
|
|
14
|
+
validates :action, inclusion: { in: %w[created updated deleted] }
|
|
15
|
+
|
|
16
|
+
# -----------------------------------------------------------------------
|
|
17
|
+
# Scopes
|
|
18
|
+
# -----------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
# Filter by audited record
|
|
21
|
+
# @example AuditLog.for(user)
|
|
22
|
+
scope :for, ->(record) {
|
|
23
|
+
where(auditable_type: record.class.name, auditable_id: record.id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Filter by actor
|
|
27
|
+
# @example AuditLog.by(admin)
|
|
28
|
+
scope :by, ->(actor) {
|
|
29
|
+
where(actor_type: actor.class.name, actor_id: actor.id)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Filter by action
|
|
33
|
+
# @example AuditLog.action(:updated)
|
|
34
|
+
scope :action, ->(action) { where(action: action.to_s) }
|
|
35
|
+
|
|
36
|
+
# Filter by time
|
|
37
|
+
# @example AuditLog.since(1.week.ago)
|
|
38
|
+
scope :since, ->(time) { where("created_at >= ?", time) }
|
|
39
|
+
|
|
40
|
+
# -----------------------------------------------------------------------
|
|
41
|
+
# Helpers
|
|
42
|
+
# -----------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def changed_fields
|
|
45
|
+
value = self[:changed_fields]
|
|
46
|
+
return value if value.is_a?(Hash)
|
|
47
|
+
JSON.parse(value.to_s)
|
|
48
|
+
rescue JSON::ParserError
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def metadata
|
|
53
|
+
value = self[:metadata]
|
|
54
|
+
return nil if value.nil?
|
|
55
|
+
return value if value.is_a?(Hash)
|
|
56
|
+
JSON.parse(value.to_s)
|
|
57
|
+
rescue JSON::ParserError
|
|
58
|
+
{}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns a human readable summary of the log entry
|
|
62
|
+
# @example log.summary
|
|
63
|
+
# => "Account #1 was updated by Account #2"
|
|
64
|
+
def summary
|
|
65
|
+
actor_info = actor ? "#{actor_type} ##{actor_id}" : "anonymous"
|
|
66
|
+
subject_info = auditable ? "#{auditable_type} ##{auditable_id}" : "#{auditable_type} ##{auditable_id}"
|
|
67
|
+
"#{subject_info} was #{action} by #{actor_info}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditron
|
|
4
|
+
module Auditable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Stores options passed to auditable — :only, :except
|
|
9
|
+
class_attribute :_auditron_options, default: {}
|
|
10
|
+
|
|
11
|
+
has_many :audit_logs,
|
|
12
|
+
class_name: "Auditron::AuditLog",
|
|
13
|
+
as: :auditable,
|
|
14
|
+
dependent: :destroy
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_methods do
|
|
18
|
+
# @param only [Array<Symbol>] track only these fields
|
|
19
|
+
# @param except [Array<Symbol>] track all fields except these
|
|
20
|
+
def auditable(only: nil, except: nil)
|
|
21
|
+
self._auditron_options = { only: only, except: except }
|
|
22
|
+
|
|
23
|
+
after_create :auditron_log_create
|
|
24
|
+
after_update :auditron_log_update
|
|
25
|
+
after_destroy :auditron_log_destroy
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Dev calls this manually to attach metadata to the next audit log
|
|
30
|
+
# @example account.audit_with(reason: "admin override")
|
|
31
|
+
def audit_with(metadata = {})
|
|
32
|
+
@_auditron_metadata = metadata
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def auditron_log_create
|
|
39
|
+
write_audit_log("created", {})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def auditron_log_update
|
|
43
|
+
diff = filtered_changes
|
|
44
|
+
return if diff.empty?
|
|
45
|
+
write_audit_log("updated", diff)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def auditron_log_destroy
|
|
49
|
+
write_audit_log("deleted", {})
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_audit_log(action, changed_fields)
|
|
53
|
+
actor = resolve_actor
|
|
54
|
+
metadata = @_auditron_metadata.presence
|
|
55
|
+
|
|
56
|
+
Auditron::AuditLog.create!(
|
|
57
|
+
auditable: self,
|
|
58
|
+
action: action,
|
|
59
|
+
changed_fields: changed_fields.to_json,
|
|
60
|
+
actor_id: actor&.respond_to?(:id) ? actor.id : nil,
|
|
61
|
+
actor_type: actor&.class&.name,
|
|
62
|
+
ip_address: resolve_ip,
|
|
63
|
+
metadata: metadata&.to_json
|
|
64
|
+
)
|
|
65
|
+
ensure
|
|
66
|
+
# Always clear after write — prevents metadata leaking to next operation
|
|
67
|
+
@_auditron_metadata = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Build a diff hash of only the fields that changed
|
|
71
|
+
# Format: { field: [old_value, new_value] }
|
|
72
|
+
def filtered_changes
|
|
73
|
+
opts = self.class._auditron_options
|
|
74
|
+
global_ignored = Auditron.config.ignored_fields.map(&:to_s)
|
|
75
|
+
raw_changes = saved_changes.except(*global_ignored)
|
|
76
|
+
|
|
77
|
+
# Apply :only filter
|
|
78
|
+
if opts[:only].present?
|
|
79
|
+
allowed = opts[:only].map(&:to_s)
|
|
80
|
+
raw_changes = raw_changes.slice(*allowed)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Apply :except filter
|
|
84
|
+
if opts[:except].present?
|
|
85
|
+
excluded = opts[:except].map(&:to_s)
|
|
86
|
+
raw_changes = raw_changes.except(*excluded)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raw_changes
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_actor
|
|
93
|
+
# Priority 1: thread-local set directly in controller
|
|
94
|
+
return Auditron.current_actor if Auditron.current_actor
|
|
95
|
+
|
|
96
|
+
# Priority 2: lambda fallback from config
|
|
97
|
+
Auditron.config.current_actor&.call
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def resolve_ip
|
|
103
|
+
return nil unless Auditron.config.store_ip
|
|
104
|
+
Auditron.current_request&.remote_ip
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditron
|
|
4
|
+
class Configuration
|
|
5
|
+
# Lambda that returns the current actor
|
|
6
|
+
# @example config.current_actor = -> { Current.user }
|
|
7
|
+
attr_accessor :current_actor
|
|
8
|
+
|
|
9
|
+
# Fields to never log across all models
|
|
10
|
+
# @example config.ignored_fields = [:updated_at, :created_at]
|
|
11
|
+
attr_accessor :ignored_fields
|
|
12
|
+
|
|
13
|
+
# When true, stores request IP in every log entry
|
|
14
|
+
attr_accessor :store_ip
|
|
15
|
+
|
|
16
|
+
# Auto-purge logs older than N days. nil = keep forever
|
|
17
|
+
attr_accessor :retention_days
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@current_actor = -> { nil }
|
|
21
|
+
@ignored_fields = %i[updated_at created_at]
|
|
22
|
+
@store_ip = false
|
|
23
|
+
@retention_days = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditron
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
# Store current request so auditable concern can read IP
|
|
6
|
+
initializer "auditron.request_store" do |app|
|
|
7
|
+
app.middleware.use(Auditron::RequestMiddleware)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Minimal Rack middleware — stores the current request thread-locally
|
|
12
|
+
class RequestMiddleware
|
|
13
|
+
def initialize(app)
|
|
14
|
+
@app = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
Auditron.current_request = ActionDispatch::Request.new(env)
|
|
19
|
+
@app.call(env)
|
|
20
|
+
ensure
|
|
21
|
+
# Clear both after every request — prevents leaking between requests
|
|
22
|
+
Auditron.current_actor = nil
|
|
23
|
+
Auditron.current_request = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditron
|
|
4
|
+
module Sweeper
|
|
5
|
+
# Delete all logs older than config.retention_days
|
|
6
|
+
# Call this from a scheduled job e.g. daily Sidekiq/GoodJob cron
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# Auditron::Sweeper.purge!
|
|
10
|
+
def self.purge!
|
|
11
|
+
days = Auditron.config.retention_days
|
|
12
|
+
return unless days.is_a?(Integer) && days.positive?
|
|
13
|
+
|
|
14
|
+
cutoff = Time.current - days.days
|
|
15
|
+
deleted = Auditron::AuditLog.where("created_at < ?", cutoff).delete_all
|
|
16
|
+
deleted
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/auditron.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_support/core_ext/module/attribute_accessors_per_thread"
|
|
5
|
+
require "auditron/version"
|
|
6
|
+
require "auditron/configuration"
|
|
7
|
+
require "auditron/audit_log"
|
|
8
|
+
require "auditron/auditable"
|
|
9
|
+
require "auditron/sweeper"
|
|
10
|
+
|
|
11
|
+
module Auditron
|
|
12
|
+
# Explicitly require active_support thread accessor
|
|
13
|
+
thread_mattr_accessor :current_actor
|
|
14
|
+
thread_mattr_accessor :current_request
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def configure
|
|
18
|
+
yield config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def config
|
|
22
|
+
@config ||= Configuration.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require "auditron/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Auditron
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
def install
|
|
14
|
+
display_banner
|
|
15
|
+
display_intro
|
|
16
|
+
return cancel_install unless confirm_install?
|
|
17
|
+
|
|
18
|
+
say ""
|
|
19
|
+
say " Creating migration...", :cyan
|
|
20
|
+
migration_template(
|
|
21
|
+
"create_audit_logs.rb.erb",
|
|
22
|
+
"db/migrate/create_audit_logs.rb"
|
|
23
|
+
)
|
|
24
|
+
say ""
|
|
25
|
+
|
|
26
|
+
display_initializer_hint
|
|
27
|
+
display_success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def display_banner
|
|
33
|
+
say ""
|
|
34
|
+
say " +===================================================+", :cyan
|
|
35
|
+
say " | |", :cyan
|
|
36
|
+
say " | AUDITRON -- Audit Logging Gem |", :cyan
|
|
37
|
+
say " | v#{Auditron::VERSION.ljust(6)} |", :cyan
|
|
38
|
+
say " | |", :cyan
|
|
39
|
+
say " +===================================================+", :cyan
|
|
40
|
+
say ""
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def display_intro
|
|
44
|
+
say " Auditron will set up audit logging for your Rails app.", :white
|
|
45
|
+
say ""
|
|
46
|
+
say " This installer will:", :white
|
|
47
|
+
say ""
|
|
48
|
+
say " [+] Create the audit_logs migration", :green
|
|
49
|
+
say " [+] Add indexes for fast querying", :green
|
|
50
|
+
say " [+] Track: auditable, actor, action, changed fields, IP", :green
|
|
51
|
+
say ""
|
|
52
|
+
say " ---------------------------------------------------", :cyan
|
|
53
|
+
say ""
|
|
54
|
+
say " After install, add to any model:", :yellow
|
|
55
|
+
say ""
|
|
56
|
+
say " class User < ApplicationRecord", :white
|
|
57
|
+
say " auditable only: [:email, :role, :status]", :green
|
|
58
|
+
say " end", :white
|
|
59
|
+
say ""
|
|
60
|
+
say " ---------------------------------------------------", :cyan
|
|
61
|
+
say ""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def confirm_install?
|
|
65
|
+
answer = ask(
|
|
66
|
+
" Ready to install? This will create the audit_logs migration. [Y/n]:",
|
|
67
|
+
:yellow
|
|
68
|
+
).strip.downcase
|
|
69
|
+
|
|
70
|
+
answer == "y" || answer == "yes" || answer == ""
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def display_initializer_hint
|
|
74
|
+
say " ---------------------------------------------------", :cyan
|
|
75
|
+
say ""
|
|
76
|
+
say " Next steps:", :white
|
|
77
|
+
say ""
|
|
78
|
+
say " 1. Run the migration:", :yellow
|
|
79
|
+
say " rails db:migrate", :green
|
|
80
|
+
say ""
|
|
81
|
+
say " 2. Create config/initializers/auditron.rb:", :yellow
|
|
82
|
+
say " Auditron.configure do |config|", :green
|
|
83
|
+
say " config.ignored_fields = %i[updated_at created_at]", :green
|
|
84
|
+
say " config.store_ip = false", :green
|
|
85
|
+
say " config.retention_days = nil", :green
|
|
86
|
+
say " end", :green
|
|
87
|
+
say ""
|
|
88
|
+
say " 3. Set current actor in ApplicationController:", :yellow
|
|
89
|
+
say " before_action :set_audit_actor", :green
|
|
90
|
+
say " def set_audit_actor", :green
|
|
91
|
+
say " Auditron.current_actor = @current_user", :green
|
|
92
|
+
say " end", :green
|
|
93
|
+
say ""
|
|
94
|
+
say " 4. Add auditable to your models:", :yellow
|
|
95
|
+
say " auditable only: [:email, :role]", :green
|
|
96
|
+
say ""
|
|
97
|
+
say " 5. Query your logs:", :yellow
|
|
98
|
+
say " user.audit_logs", :green
|
|
99
|
+
say " AuditLog.by(admin).action(:deleted).since(1.week.ago)", :green
|
|
100
|
+
say ""
|
|
101
|
+
say " ---------------------------------------------------", :cyan
|
|
102
|
+
say ""
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def display_success
|
|
106
|
+
say " [OK] Auditron installed successfully!", :green
|
|
107
|
+
say " [OK] Run 'rails db:migrate' to complete setup.", :green
|
|
108
|
+
say ""
|
|
109
|
+
say " Happy auditing!", :cyan
|
|
110
|
+
say ""
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cancel_install
|
|
114
|
+
say ""
|
|
115
|
+
say " [CANCELLED] Installation cancelled.", :red
|
|
116
|
+
say " Run 'rails generate auditron:install' again when ready.", :yellow
|
|
117
|
+
say ""
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreateAuditLogs < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :audit_logs do |t|
|
|
4
|
+
t.string :auditable_type, null: false
|
|
5
|
+
t.bigint :auditable_id, null: false
|
|
6
|
+
t.string :action, null: false
|
|
7
|
+
t.text :changed_fields
|
|
8
|
+
t.string :actor_type
|
|
9
|
+
t.bigint :actor_id
|
|
10
|
+
t.string :ip_address
|
|
11
|
+
t.text :metadata
|
|
12
|
+
|
|
13
|
+
t.datetime :created_at, null: false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :audit_logs, [:auditable_type, :auditable_id]
|
|
17
|
+
add_index :audit_logs, [:actor_type, :actor_id]
|
|
18
|
+
add_index :audit_logs, :action
|
|
19
|
+
add_index :audit_logs, :created_at
|
|
20
|
+
end
|
|
21
|
+
end
|
data/sig/auditron.rbs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Auditron
|
|
2
|
+
VERSION: String
|
|
3
|
+
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
|
4
|
+
|
|
5
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
6
|
+
def self.config: () -> Configuration
|
|
7
|
+
|
|
8
|
+
self.current_request: untyped
|
|
9
|
+
|
|
10
|
+
class Configuration
|
|
11
|
+
attr_accessor current_actor: Proc
|
|
12
|
+
attr_accessor ignored_fields: Array[Symbol]
|
|
13
|
+
attr_accessor store_ip: bool
|
|
14
|
+
attr_accessor retention_days: Integer?
|
|
15
|
+
|
|
16
|
+
def initialize: () -> void
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module Auditable
|
|
20
|
+
module ClassMethods
|
|
21
|
+
def auditable: (?only: Array[Symbol]?, ?except: Array[Symbol]?) -> void
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class AuditLog < ActiveRecord::Base
|
|
26
|
+
def self.for: (untyped record) -> ActiveRecord::Relation
|
|
27
|
+
def self.by: (untyped actor) -> ActiveRecord::Relation
|
|
28
|
+
def self.action: (Symbol | String action) -> ActiveRecord::Relation
|
|
29
|
+
def self.since: (Time time) -> ActiveRecord::Relation
|
|
30
|
+
def changed_fields: () -> Hash[String, untyped]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Sweeper
|
|
34
|
+
def self.purge!: () -> Integer
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Railtie < Rails::Railtie
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class RequestMiddleware
|
|
41
|
+
def initialize: (untyped app) -> void
|
|
42
|
+
def call: (untyped env) -> untyped
|
|
43
|
+
end
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: auditron
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Shailendra Kumar
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rails
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: sqlite3
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.1'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec-rails
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '6.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '6.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop-rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '2.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '2.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: simplecov
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.22'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.22'
|
|
125
|
+
description: |
|
|
126
|
+
Auditron tracks who changed what on any ActiveRecord model — storing only
|
|
127
|
+
the fields that changed, not full snapshots. Ships with a chainable query
|
|
128
|
+
DSL, built-in log retention, a simple actor lambda, and works with
|
|
129
|
+
PostgreSQL, MySQL, and SQLite. Zero hard dependencies beyond ActiveRecord.
|
|
130
|
+
email:
|
|
131
|
+
- shailendrapatidar00@gmail.com
|
|
132
|
+
executables: []
|
|
133
|
+
extensions: []
|
|
134
|
+
extra_rdoc_files: []
|
|
135
|
+
files:
|
|
136
|
+
- CHANGELOG.md
|
|
137
|
+
- CODE_OF_CONDUCT.md
|
|
138
|
+
- LICENSE.txt
|
|
139
|
+
- README.md
|
|
140
|
+
- Rakefile
|
|
141
|
+
- lib/auditron.rb
|
|
142
|
+
- lib/auditron/audit_log.rb
|
|
143
|
+
- lib/auditron/auditable.rb
|
|
144
|
+
- lib/auditron/configuration.rb
|
|
145
|
+
- lib/auditron/railtie.rb
|
|
146
|
+
- lib/auditron/sweeper.rb
|
|
147
|
+
- lib/auditron/version.rb
|
|
148
|
+
- lib/generators/auditron/install/install_generator.rb
|
|
149
|
+
- lib/generators/auditron/install/templates/create_audit_logs.rb.erb
|
|
150
|
+
- sig/auditron.rbs
|
|
151
|
+
homepage: https://github.com/spatelpatidar/auditron
|
|
152
|
+
licenses:
|
|
153
|
+
- MIT
|
|
154
|
+
metadata:
|
|
155
|
+
homepage_uri: https://github.com/spatelpatidar/auditron
|
|
156
|
+
source_code_uri: https://github.com/spatelpatidar/auditron
|
|
157
|
+
changelog_uri: https://github.com/spatelpatidar/auditron/blob/main/CHANGELOG.md
|
|
158
|
+
bug_tracker_uri: https://github.com/spatelpatidar/auditron/auditron/issues
|
|
159
|
+
rubygems_mfa_required: 'true'
|
|
160
|
+
post_install_message:
|
|
161
|
+
rdoc_options: []
|
|
162
|
+
require_paths:
|
|
163
|
+
- lib
|
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
165
|
+
requirements:
|
|
166
|
+
- - ">="
|
|
167
|
+
- !ruby/object:Gem::Version
|
|
168
|
+
version: 3.0.0
|
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
requirements: []
|
|
175
|
+
rubygems_version: 3.4.10
|
|
176
|
+
signing_key:
|
|
177
|
+
specification_version: 4
|
|
178
|
+
summary: Lightweight, diff-only audit logging for ActiveRecord models.
|
|
179
|
+
test_files: []
|