rails_audit_log 0.5.0 → 0.7.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 +128 -0
- data/app/concerns/rails_audit_log/auditable.rb +88 -9
- data/app/jobs/rails_audit_log/write_audit_log_job.rb +20 -0
- data/app/models/rails_audit_log/audit_log_entry.rb +17 -2
- data/lib/rails_audit_log/engine.rb +6 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +25 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3323e92d5f10ecd832a198a65bf6e0fd65c7bf0ff35069230bc8b7791d14e7fa
|
|
4
|
+
data.tar.gz: 4cd882a647d273ee3fe8a6c7b7f89a3b241ee0fa9b22b84aed93b8aea2a30c6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c8d577a7b027d8c300b62545930e94798967f5cdc7f57d4d84a06b3f1968d1357b6ea1db9ae8abf1cb3e36bda8e6fd12be6c2da0798cf7d6d6a02e60849621c
|
|
7
|
+
data.tar.gz: 69a905ee623f28403c53d280a56009cd81696406e22ff2f258ef2d0ec56fc4d7a154f72a0a345557ec8a39e0b36b655b3fba7c1a128bee954834e73d537946b6
|
data/README.md
CHANGED
|
@@ -102,6 +102,134 @@ entry.diff
|
|
|
102
102
|
# => { "title" => { from: "Hello", to: "World" } }
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
### Separate audit database
|
|
106
|
+
|
|
107
|
+
Route all audit writes to a dedicated database by setting `connects_to` in an initializer. The engine applies it to `AuditLogEntry` at boot:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# config/initializers/rails_audit_log.rb
|
|
111
|
+
RailsAuditLog.connects_to = {
|
|
112
|
+
database: { writing: :audit_log, reading: :audit_log }
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
### Lightweight queries
|
|
119
|
+
|
|
120
|
+
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):
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
entries = AuditLogEntry.slim.for_resource(article).since(1.week.ago)
|
|
124
|
+
entries.first.event # => "update"
|
|
125
|
+
entries.first.actor_type # => "User"
|
|
126
|
+
entries.first.object_changes # => raises ActiveModel::MissingAttributeError
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> **Note:** Use `.count(:id)` instead of `.count` when chaining `.slim` with other scopes — Rails' `COUNT` with a multi-column `SELECT` is not supported by all databases.
|
|
130
|
+
|
|
131
|
+
### Association tracking
|
|
132
|
+
|
|
133
|
+
Track `has_many` add and remove events by passing `associations: true` to `audit_log`. Call `audit_log` **before** the `has_many` declarations so the callbacks are wired at class load time:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class Post < ApplicationRecord
|
|
137
|
+
include RailsAuditLog::Auditable
|
|
138
|
+
audit_log associations: true
|
|
139
|
+
has_many :tags
|
|
140
|
+
has_many :comments, dependent: :destroy
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Each add or remove creates an `update` entry on the parent with the associated record's identity in `object_changes`:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
post = Post.create!(title: "Hello")
|
|
148
|
+
tag = post.tags.create!(name: "Ruby")
|
|
149
|
+
|
|
150
|
+
entry = post.audit_log_entries.updated_events.last
|
|
151
|
+
entry.object_changes
|
|
152
|
+
# => { "tags" => [nil, { "id" => 1, "type" => "Tag" }] }
|
|
153
|
+
|
|
154
|
+
post.tags.delete(tag)
|
|
155
|
+
entry = post.audit_log_entries.updated_events.last
|
|
156
|
+
entry.object_changes
|
|
157
|
+
# => { "tags" => [{ "id" => 1, "type" => "Tag" }, nil] }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Track only a named subset of associations:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
audit_log associations: [:tags] # comments changes are not recorded
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`has_many :through` and `has_and_belongs_to_many` work the same way — no extra configuration:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class Post < ApplicationRecord
|
|
170
|
+
include RailsAuditLog::Auditable
|
|
171
|
+
audit_log associations: true
|
|
172
|
+
has_many :taggings
|
|
173
|
+
has_many :tags, through: :taggings # tracked automatically
|
|
174
|
+
has_and_belongs_to_many :categories # tracked automatically
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`belongs_to` foreign-key changes are already tracked as regular column updates and require no extra configuration.
|
|
179
|
+
|
|
180
|
+
### Bulk audit writes
|
|
181
|
+
|
|
182
|
+
Wrap a block with `RailsAuditLog.batch_audit` to buffer all audit entries and flush them in a single `insert_all!` call at the end, eliminating N+1 inserts during imports:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
RailsAuditLog.batch_audit do
|
|
186
|
+
records.each { |attrs| Post.create!(attrs) }
|
|
187
|
+
end
|
|
188
|
+
# All audit entries written in one INSERT
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Nested calls accumulate into the outermost batch — only one `insert_all!` fires. If the block raises, the buffer is discarded and no entries are written.
|
|
192
|
+
|
|
193
|
+
> **Note:** Version pruning (`version_limit`) is deferred in batch mode — the next write outside the batch will trigger pruning as usual.
|
|
194
|
+
|
|
195
|
+
### Async audit writes
|
|
196
|
+
|
|
197
|
+
Offload audit writes to a background job by passing `async: true` to `audit_log`. The entry is enqueued via `RailsAuditLog::WriteAuditLogJob` (a subclass of `ActiveJob::Base`) so the request does not block on the database write:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class Post < ApplicationRecord
|
|
201
|
+
include RailsAuditLog::Auditable
|
|
202
|
+
audit_log async: true
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Enable async globally in an initializer — per-model `async: true` takes precedence:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# config/initializers/rails_audit_log.rb
|
|
210
|
+
RailsAuditLog.async = true
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Configure the queue adapter and queue name through standard ActiveJob settings. Version pruning also runs inside the job when `version_limit` is set.
|
|
214
|
+
|
|
215
|
+
### Capping history per record
|
|
216
|
+
|
|
217
|
+
Limit how many audit entries are kept per record with `version_limit:`. Oldest entries are pruned automatically after each write once the cap is reached:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
class Post < ApplicationRecord
|
|
221
|
+
include RailsAuditLog::Auditable
|
|
222
|
+
audit_log version_limit: 10 # keep only the 10 most recent entries
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Set a global default in an initializer — per-model values take precedence:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# config/initializers/rails_audit_log.rb
|
|
230
|
+
RailsAuditLog.version_limit = 50
|
|
231
|
+
```
|
|
232
|
+
|
|
105
233
|
### Selective tracking
|
|
106
234
|
|
|
107
235
|
Track only specific attributes, or exclude noisy ones:
|
|
@@ -3,9 +3,12 @@ module RailsAuditLog
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
class_attribute :_audit_log_only,
|
|
7
|
-
class_attribute :_audit_log_ignore,
|
|
8
|
-
class_attribute :_audit_log_meta,
|
|
6
|
+
class_attribute :_audit_log_only, default: nil
|
|
7
|
+
class_attribute :_audit_log_ignore, default: nil
|
|
8
|
+
class_attribute :_audit_log_meta, default: nil
|
|
9
|
+
class_attribute :_audit_log_associations, default: nil
|
|
10
|
+
class_attribute :_audit_log_version_limit, default: nil
|
|
11
|
+
class_attribute :_audit_log_async, default: false
|
|
9
12
|
|
|
10
13
|
has_many :audit_log_entries,
|
|
11
14
|
class_name: "RailsAuditLog::AuditLogEntry",
|
|
@@ -15,13 +18,47 @@ module RailsAuditLog
|
|
|
15
18
|
after_create :record_audit_create
|
|
16
19
|
after_update :record_audit_update
|
|
17
20
|
after_destroy :record_audit_destroy
|
|
21
|
+
|
|
22
|
+
# Intercept has_many (including :through) and has_and_belongs_to_many to
|
|
23
|
+
# inject after_add/after_remove callbacks when association tracking is
|
|
24
|
+
# enabled. Must be defined after has_many :audit_log_entries so that the
|
|
25
|
+
# internal association is not affected.
|
|
26
|
+
def self.has_many(name, scope = nil, **options, &extension)
|
|
27
|
+
if _audit_log_associations && name.to_s != "audit_log_entries"
|
|
28
|
+
options = _build_audit_association_options(name.to_s, options)
|
|
29
|
+
end
|
|
30
|
+
scope ? super(name, scope, **options, &extension) : super(name, **options, &extension)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
|
34
|
+
if _audit_log_associations
|
|
35
|
+
options = _build_audit_association_options(name.to_s, options)
|
|
36
|
+
end
|
|
37
|
+
scope ? super(name, scope, **options, &extension) : super(name, **options, &extension)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self._build_audit_association_options(assoc_name, options)
|
|
41
|
+
tracked = _audit_log_associations == true ||
|
|
42
|
+
Array(_audit_log_associations).map(&:to_s).include?(assoc_name)
|
|
43
|
+
return options unless tracked
|
|
44
|
+
|
|
45
|
+
add_cb = ->(owner, rec) { owner.send(:record_audit_association_change, assoc_name, nil, { "id" => rec.id, "type" => rec.class.name }) }
|
|
46
|
+
remove_cb = ->(owner, rec) { owner.send(:record_audit_association_change, assoc_name, { "id" => rec.id, "type" => rec.class.name }, nil) }
|
|
47
|
+
options.merge(
|
|
48
|
+
after_add: [*options[:after_add]] + [add_cb],
|
|
49
|
+
after_remove: [*options[:after_remove]] + [remove_cb]
|
|
50
|
+
)
|
|
51
|
+
end
|
|
18
52
|
end
|
|
19
53
|
|
|
20
54
|
class_methods do
|
|
21
|
-
def audit_log(only: nil, ignore: nil, meta: nil)
|
|
22
|
-
self._audit_log_only
|
|
23
|
-
self._audit_log_ignore
|
|
24
|
-
self._audit_log_meta
|
|
55
|
+
def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, async: nil)
|
|
56
|
+
self._audit_log_only = only.map(&:to_s) if only
|
|
57
|
+
self._audit_log_ignore = ignore.map(&:to_s) if ignore
|
|
58
|
+
self._audit_log_meta = meta if meta
|
|
59
|
+
self._audit_log_associations = associations unless associations.nil?
|
|
60
|
+
self._audit_log_version_limit = version_limit unless version_limit.nil?
|
|
61
|
+
self._audit_log_async = async unless async.nil?
|
|
25
62
|
end
|
|
26
63
|
end
|
|
27
64
|
|
|
@@ -46,6 +83,25 @@ module RailsAuditLog
|
|
|
46
83
|
record_audit_entry("destroy", changes, snapshot)
|
|
47
84
|
end
|
|
48
85
|
|
|
86
|
+
def record_audit_association_change(assoc_name, before, after)
|
|
87
|
+
return unless RailsAuditLog.enabled?
|
|
88
|
+
|
|
89
|
+
actor = RailsAuditLog.actor
|
|
90
|
+
meta = build_audit_metadata
|
|
91
|
+
write_audit_entry(
|
|
92
|
+
event: "update",
|
|
93
|
+
item_type: self.class.name,
|
|
94
|
+
item_id: id,
|
|
95
|
+
object_changes: { assoc_name => [before, after] },
|
|
96
|
+
object: nil,
|
|
97
|
+
reason: RailsAuditLog.reason,
|
|
98
|
+
metadata: meta.presence,
|
|
99
|
+
whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
|
|
100
|
+
actor_type: actor&.class&.name,
|
|
101
|
+
actor_id: actor.respond_to?(:id) ? actor.id : nil
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
49
105
|
def record_audit_entry(event, changes, snapshot = nil)
|
|
50
106
|
return unless RailsAuditLog.enabled?
|
|
51
107
|
|
|
@@ -53,8 +109,8 @@ module RailsAuditLog
|
|
|
53
109
|
return if filtered.empty? && event == "update"
|
|
54
110
|
|
|
55
111
|
actor = RailsAuditLog.actor
|
|
56
|
-
meta
|
|
57
|
-
|
|
112
|
+
meta = build_audit_metadata
|
|
113
|
+
write_audit_entry(
|
|
58
114
|
event: event,
|
|
59
115
|
item_type: self.class.name,
|
|
60
116
|
item_id: id,
|
|
@@ -68,6 +124,29 @@ module RailsAuditLog
|
|
|
68
124
|
)
|
|
69
125
|
end
|
|
70
126
|
|
|
127
|
+
def write_audit_entry(entry_attrs)
|
|
128
|
+
if (buffer = RailsAuditLog.batch_audit_buffer)
|
|
129
|
+
buffer << entry_attrs.stringify_keys.merge("created_at" => Time.current)
|
|
130
|
+
elsif _audit_log_async || RailsAuditLog.async
|
|
131
|
+
limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
|
|
132
|
+
WriteAuditLogJob.perform_later(entry_attrs.stringify_keys, version_limit: limit)
|
|
133
|
+
else
|
|
134
|
+
RailsAuditLog::AuditLogEntry.create!(entry_attrs)
|
|
135
|
+
prune_audit_entries
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def prune_audit_entries
|
|
140
|
+
limit = self.class._audit_log_version_limit || RailsAuditLog.version_limit
|
|
141
|
+
return unless limit
|
|
142
|
+
|
|
143
|
+
count = audit_log_entries.count
|
|
144
|
+
excess = count - limit
|
|
145
|
+
return unless excess > 0
|
|
146
|
+
|
|
147
|
+
audit_log_entries.order(id: :asc).limit(excess).delete_all
|
|
148
|
+
end
|
|
149
|
+
|
|
71
150
|
def build_audit_metadata
|
|
72
151
|
meta = {}
|
|
73
152
|
if self.class._audit_log_meta
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsAuditLog
|
|
2
|
+
class WriteAuditLogJob < ApplicationJob
|
|
3
|
+
def perform(entry_attrs, version_limit: nil)
|
|
4
|
+
AuditLogEntry.create!(entry_attrs)
|
|
5
|
+
|
|
6
|
+
return unless version_limit
|
|
7
|
+
|
|
8
|
+
item_type = entry_attrs["item_type"]
|
|
9
|
+
item_id = entry_attrs["item_id"]
|
|
10
|
+
count = AuditLogEntry.where(item_type: item_type, item_id: item_id).count
|
|
11
|
+
excess = count - version_limit
|
|
12
|
+
return unless excess > 0
|
|
13
|
+
|
|
14
|
+
AuditLogEntry.where(item_type: item_type, item_id: item_id)
|
|
15
|
+
.order(id: :asc)
|
|
16
|
+
.limit(excess)
|
|
17
|
+
.delete_all
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -3,6 +3,13 @@ module RailsAuditLog
|
|
|
3
3
|
self.table_name = "audit_log_entries"
|
|
4
4
|
|
|
5
5
|
EVENTS = %w[create update destroy].freeze
|
|
6
|
+
BLOB_COLUMNS = %w[object_changes object metadata].freeze
|
|
7
|
+
|
|
8
|
+
def self.configure_connection!
|
|
9
|
+
return unless (opts = RailsAuditLog.connects_to)
|
|
10
|
+
|
|
11
|
+
connects_to(**opts)
|
|
12
|
+
end
|
|
6
13
|
|
|
7
14
|
belongs_to :item, polymorphic: true, optional: true
|
|
8
15
|
belongs_to :actor, polymorphic: true, optional: true
|
|
@@ -37,6 +44,9 @@ module RailsAuditLog
|
|
|
37
44
|
scope :since, ->(time) { where(created_at: time..) }
|
|
38
45
|
scope :until, ->(time) { where(created_at: ..time) }
|
|
39
46
|
|
|
47
|
+
# Projection scope — omits JSON blob columns for index/listing queries
|
|
48
|
+
scope :slim, -> { select(column_names - BLOB_COLUMNS) }
|
|
49
|
+
|
|
40
50
|
# Instance methods
|
|
41
51
|
|
|
42
52
|
def reify
|
|
@@ -51,8 +61,13 @@ module RailsAuditLog
|
|
|
51
61
|
return instance
|
|
52
62
|
end
|
|
53
63
|
|
|
54
|
-
# Fallback: diff-only mode or entries recorded before snapshot support
|
|
55
|
-
|
|
64
|
+
# Fallback: diff-only mode or entries recorded before snapshot support.
|
|
65
|
+
# Filter to column names so association-change entries (e.g. tags, comments)
|
|
66
|
+
# don't get assigned to the record as if they were scalar attributes.
|
|
67
|
+
column_names = klass.column_names.map(&:to_s)
|
|
68
|
+
from_attrs = (object_changes || {})
|
|
69
|
+
.select { |k, _| column_names.include?(k) }
|
|
70
|
+
.transform_values { |from_to| from_to[0] }
|
|
56
71
|
|
|
57
72
|
if event == "update"
|
|
58
73
|
record = klass.find_by(id: item_id)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
module RailsAuditLog
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
isolate_namespace RailsAuditLog
|
|
4
|
+
|
|
5
|
+
initializer "rails_audit_log.connect_audit_db" do
|
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
|
7
|
+
RailsAuditLog::AuditLogEntry.configure_connection!
|
|
8
|
+
end
|
|
9
|
+
end
|
|
4
10
|
end
|
|
5
11
|
end
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -7,6 +7,9 @@ module RailsAuditLog
|
|
|
7
7
|
mattr_accessor :ignored_attributes, default: %w[updated_at]
|
|
8
8
|
mattr_accessor :store_snapshot, default: true
|
|
9
9
|
mattr_accessor :capture_request_metadata, default: false
|
|
10
|
+
mattr_accessor :version_limit, default: nil
|
|
11
|
+
mattr_accessor :async, default: false
|
|
12
|
+
mattr_accessor :connects_to, default: nil
|
|
10
13
|
mattr_accessor :whodunnit_display, default: ->(actor) {
|
|
11
14
|
actor.respond_to?(:name) ? actor.name.to_s : actor.to_s
|
|
12
15
|
}
|
|
@@ -63,6 +66,24 @@ module RailsAuditLog
|
|
|
63
66
|
self.reason = previous
|
|
64
67
|
end
|
|
65
68
|
|
|
69
|
+
def self.batch_audit
|
|
70
|
+
return yield if Thread.current[:rails_audit_log_batch]
|
|
71
|
+
|
|
72
|
+
Thread.current[:rails_audit_log_batch] = []
|
|
73
|
+
begin
|
|
74
|
+
result = yield
|
|
75
|
+
batch = Thread.current[:rails_audit_log_batch]
|
|
76
|
+
AuditLogEntry.insert_all!(batch) if batch.any?
|
|
77
|
+
result
|
|
78
|
+
ensure
|
|
79
|
+
Thread.current[:rails_audit_log_batch] = nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.batch_audit_buffer
|
|
84
|
+
Thread.current[:rails_audit_log_batch]
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
def self.version_at(record, time)
|
|
67
88
|
entry = AuditLogEntry
|
|
68
89
|
.where(item_type: record.class.name, item_id: record.id)
|
|
@@ -73,7 +94,10 @@ module RailsAuditLog
|
|
|
73
94
|
return nil if entry.nil? || entry.event == "destroy"
|
|
74
95
|
|
|
75
96
|
klass = record.class
|
|
76
|
-
|
|
97
|
+
column_names = klass.column_names.map(&:to_s)
|
|
98
|
+
to_attrs = (entry.object_changes || {})
|
|
99
|
+
.select { |k, _| column_names.include?(k) }
|
|
100
|
+
.transform_values { |v| v[1] }
|
|
77
101
|
attrs = entry.object.present? ? entry.object.merge(to_attrs) : to_attrs
|
|
78
102
|
|
|
79
103
|
instance = klass.new
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_audit_log
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- app/controllers/rails_audit_log/application_controller.rb
|
|
42
42
|
- app/helpers/rails_audit_log/application_helper.rb
|
|
43
43
|
- app/jobs/rails_audit_log/application_job.rb
|
|
44
|
+
- app/jobs/rails_audit_log/write_audit_log_job.rb
|
|
44
45
|
- app/models/rails_audit_log/application_record.rb
|
|
45
46
|
- app/models/rails_audit_log/audit_log_entry.rb
|
|
46
47
|
- app/views/layouts/rails_audit_log/application.html.erb
|