rails_audit_log 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb6120f7ce9f0f2a2fb19553a77ff4c6bf5c24a1945270e2e6393403b9ffc3a0
4
- data.tar.gz: a978f7082c30a68c37344e0b819af1e70d077b3092337c5ea31c4e35473ffef6
3
+ metadata.gz: 9e3de1e31a64d33c94042ea45ff4eeb391d48cc65db934bf7b7256656326c480
4
+ data.tar.gz: e34d558b4687f61336af9a7f7e6ae3faebe6ea77533e7da8e360d27dbfc70060
5
5
  SHA512:
6
- metadata.gz: 8670a4ceb147508b45d03679eb3765c6984393c144434f4ad90b40bf69c64478ff8b92b0ab3bdb0b054cb0f64d2794f05044d546f81fa2fde17496ff65fde4de
7
- data.tar.gz: 5ba060114539abc8555a5873aa7671c226cdc2c19a3bd66a4549595f68ea170372a968de324485dc0bf2eabb0bd253fa14e5c14c98b7b4ad9fe691bc15973fbb
6
+ metadata.gz: cd8d51146c5b915b5c00cc863f23cffc70066cf4cfaafc5e667e9b064b225168b71f45d5699199ba37de78f0089cebabb8a1707f6f837642c32b0fb73c290a6c
7
+ data.tar.gz: 3abf1d5c1894de5496a76b8827eb23f6f8b430e9a4a192505e1012a3be022cf01290b5ef38cc3d9b43e2e4dc5f46c2e2667fdd77015ce0dfb111fab6141530b5
data/README.md CHANGED
@@ -27,6 +27,7 @@ Audit logging for Rails. Tracks `create`, `update`, and `destroy` events as stru
27
27
  - [Scheduled and manual pruning](#scheduled-and-manual-pruning)
28
28
  - [Encrypting audit data](#encrypting-audit-data)
29
29
  - [Multi-tenancy](#multi-tenancy)
30
+ - [Event streaming](#event-streaming)
30
31
  - [Selective tracking](#selective-tracking)
31
32
  - [Disabling auditing](#disabling-auditing)
32
33
  - [Object reconstruction](#object-reconstruction)
@@ -477,6 +478,82 @@ RailsAuditLog.acts_as_tenant!
477
478
 
478
479
  This is equivalent to `RailsAuditLog.current_tenant { ActsAsTenant.current_tenant&.id }`.
479
480
 
481
+ ### Event streaming
482
+
483
+ Publish every audit entry to an external consumer as it is written. Set any object implementing `#publish(entry)` as the adapter:
484
+
485
+ ```ruby
486
+ # config/initializers/rails_audit_log.rb
487
+ RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::NotificationsAdapter.new
488
+ ```
489
+
490
+ #### NotificationsAdapter (built-in, zero dependencies)
491
+
492
+ Publishes `rails_audit_log.entry_created` synchronously via `ActiveSupport::Notifications`:
493
+
494
+ ```ruby
495
+ ActiveSupport::Notifications.subscribe("rails_audit_log.entry_created") do |*, payload|
496
+ entry = payload[:entry]
497
+ Rails.logger.info "Audit: #{entry.event} #{entry.item_type}##{entry.item_id}"
498
+ end
499
+ ```
500
+
501
+ #### ActiveJobAdapter (async)
502
+
503
+ Enqueues `PublishEntryJob` so publishing does not block the request. The job fires the same `rails_audit_log.entry_created` notification when performed:
504
+
505
+ ```ruby
506
+ RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::ActiveJobAdapter.new
507
+ # or with a custom queue:
508
+ RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::ActiveJobAdapter.new(queue: :streaming)
509
+ ```
510
+
511
+ #### Custom adapters
512
+
513
+ Any object implementing `#publish(entry)` works — no companion gem needed for Kafka, SQS, or any other transport:
514
+
515
+ ```ruby
516
+ # Kafka via WaterDrop
517
+ class KafkaAuditAdapter
518
+ def initialize(producer: WaterDrop::SyncProducer)
519
+ @producer = producer
520
+ end
521
+
522
+ def publish(entry)
523
+ @producer.call(entry.attributes.to_json, topic: "audit_log")
524
+ end
525
+ end
526
+
527
+ RailsAuditLog.streaming_adapter = KafkaAuditAdapter.new
528
+ ```
529
+
530
+ ```ruby
531
+ # SQS via aws-sdk-sqs
532
+ class SqsAuditAdapter
533
+ def initialize(queue_url:, client: Aws::SQS::Client.new)
534
+ @queue_url = queue_url
535
+ @client = client
536
+ end
537
+
538
+ def publish(entry)
539
+ @client.send_message(
540
+ queue_url: @queue_url,
541
+ message_body: entry.attributes.to_json
542
+ )
543
+ end
544
+ end
545
+
546
+ RailsAuditLog.streaming_adapter = SqsAuditAdapter.new(
547
+ queue_url: ENV.fetch("AUDIT_SQS_URL")
548
+ )
549
+ ```
550
+
551
+ Wrap the adapter in `ActiveJobAdapter` if you want publishing to be asynchronous and not block the request thread.
552
+
553
+ #### Batch mode
554
+
555
+ `batch_audit` flushes the bulk `INSERT` first, then calls `#publish` for each entry individually — streaming consumers receive every entry even in batch mode.
556
+
480
557
  ### Selective tracking
481
558
 
482
559
  Track only specific attributes, or exclude noisy ones:
@@ -234,8 +234,9 @@ module RailsAuditLog
234
234
  period = self.class._audit_log_retain_for || RailsAuditLog.retention_period
235
235
  WriteAuditLogJob.perform_later(entry_attrs.stringify_keys, version_limit: limit, retention_period: period)
236
236
  else
237
- RailsAuditLog::AuditLogEntry.create!(entry_attrs)
237
+ entry = RailsAuditLog::AuditLogEntry.create!(entry_attrs)
238
238
  prune_audit_entries
239
+ RailsAuditLog.publish_entry(entry)
239
240
  end
240
241
  end
241
242
 
@@ -0,0 +1,13 @@
1
+ module RailsAuditLog
2
+ module Streaming
3
+ class PublishEntryJob < ApplicationJob
4
+ def perform(entry_attrs)
5
+ entry = AuditLogEntry.new(entry_attrs)
6
+ ActiveSupport::Notifications.instrument(
7
+ NotificationsAdapter::EVENT,
8
+ entry: entry
9
+ )
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,7 +1,8 @@
1
1
  module RailsAuditLog
2
2
  class WriteAuditLogJob < ApplicationJob
3
3
  def perform(entry_attrs, version_limit: nil, retention_period: nil)
4
- AuditLogEntry.create!(entry_attrs)
4
+ entry = AuditLogEntry.create!(entry_attrs)
5
+ RailsAuditLog.publish_entry(entry)
5
6
 
6
7
  item_type = entry_attrs["item_type"]
7
8
  item_id = entry_attrs["item_id"]
@@ -0,0 +1,25 @@
1
+ module RailsAuditLog
2
+ module Streaming
3
+ # Publishes each audit entry asynchronously by enqueuing
4
+ # {RailsAuditLog::Streaming::PublishEntryJob}. The job fires a
5
+ # +rails_audit_log.entry_created+ notification when performed, so subscribers
6
+ # receive the entry out-of-band without blocking the request.
7
+ #
8
+ # @example
9
+ # RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::ActiveJobAdapter.new
10
+ #
11
+ # # Custom queue:
12
+ # RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::ActiveJobAdapter.new(queue: :streaming)
13
+ class ActiveJobAdapter
14
+ def initialize(queue: nil)
15
+ @queue = queue
16
+ end
17
+
18
+ def publish(entry)
19
+ job = RailsAuditLog::Streaming::PublishEntryJob
20
+ job = job.set(queue: @queue) if @queue
21
+ job.perform_later(entry.attributes.compact)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module RailsAuditLog
2
+ module Streaming
3
+ # Publishes each audit entry synchronously via +ActiveSupport::Notifications+.
4
+ # Zero external dependencies — subscribe with +ActiveSupport::Notifications.subscribe+.
5
+ #
6
+ # @example
7
+ # RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::NotificationsAdapter.new
8
+ #
9
+ # ActiveSupport::Notifications.subscribe("rails_audit_log.entry_created") do |*, payload|
10
+ # entry = payload[:entry]
11
+ # Rails.logger.info "Audit: #{entry.event} on #{entry.item_type}##{entry.item_id}"
12
+ # end
13
+ class NotificationsAdapter
14
+ EVENT = "rails_audit_log.entry_created"
15
+
16
+ def publish(entry)
17
+ ActiveSupport::Notifications.instrument(EVENT, entry: entry)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -1,5 +1,7 @@
1
1
  require "rails_audit_log/version"
2
2
  require "rails_audit_log/engine"
3
+ require "rails_audit_log/streaming/notifications_adapter"
4
+ require "rails_audit_log/streaming/active_job_adapter"
3
5
 
4
6
  # RailsAuditLog is a Rails engine that tracks ActiveRecord +create+, +update+,
5
7
  # and +destroy+ events as {AuditLogEntry} records with JSON-first storage and
@@ -90,6 +92,15 @@ module RailsAuditLog
90
92
  # @return [Integer]
91
93
  mattr_accessor :page_size, default: 25
92
94
 
95
+ # The active streaming adapter. Any object implementing +#publish(entry)+.
96
+ # Called after every audit entry is persisted, including batch writes.
97
+ # Set to +nil+ (default) to disable streaming.
98
+ #
99
+ # @return [#publish, nil]
100
+ # @example
101
+ # RailsAuditLog.streaming_adapter = RailsAuditLog::Streaming::NotificationsAdapter.new
102
+ mattr_accessor :streaming_adapter, default: nil
103
+
93
104
  # Controls how an actor object is serialised into the +whodunnit_snapshot+
94
105
  # string column. Defaults to +actor.name+ when available, otherwise +to_s+.
95
106
  #
@@ -143,6 +154,16 @@ module RailsAuditLog
143
154
  current_tenant { ActsAsTenant.current_tenant&.id }
144
155
  end
145
156
 
157
+ # Passes +entry+ to the configured {.streaming_adapter} if one is set.
158
+ # No-ops when no adapter is configured.
159
+ #
160
+ # @api private
161
+ # @param entry [AuditLogEntry]
162
+ # @return [void]
163
+ def self.publish_entry(entry)
164
+ streaming_adapter&.publish(entry)
165
+ end
166
+
146
167
  # Sets or returns the authentication block used to gate the web dashboard.
147
168
  # The block is evaluated in controller context, so controller helpers
148
169
  # (e.g. +current_user+) are available directly.
@@ -273,7 +294,10 @@ module RailsAuditLog
273
294
  begin
274
295
  result = yield
275
296
  batch = Thread.current[:rails_audit_log_batch]
276
- AuditLogEntry.insert_all!(batch) if batch.any?
297
+ if batch.any?
298
+ AuditLogEntry.insert_all!(batch)
299
+ batch.each { |attrs| publish_entry(AuditLogEntry.new(attrs)) } if streaming_adapter
300
+ end
277
301
  result
278
302
  ensure
279
303
  Thread.current[:rails_audit_log_batch] = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_audit_log
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -99,6 +99,7 @@ files:
99
99
  - app/javascript/rails_audit_log/search_controller.js
100
100
  - app/jobs/rails_audit_log/application_job.rb
101
101
  - app/jobs/rails_audit_log/prune_audit_log_job.rb
102
+ - app/jobs/rails_audit_log/streaming/publish_entry_job.rb
102
103
  - app/jobs/rails_audit_log/write_audit_log_job.rb
103
104
  - app/models/rails_audit_log/application_record.rb
104
105
  - app/models/rails_audit_log/audit_log_entry.rb
@@ -125,6 +126,8 @@ files:
125
126
  - lib/rails_audit_log/matchers.rb
126
127
  - lib/rails_audit_log/minitest_assertions.rb
127
128
  - lib/rails_audit_log/paper_trail_compat.rb
129
+ - lib/rails_audit_log/streaming/active_job_adapter.rb
130
+ - lib/rails_audit_log/streaming/notifications_adapter.rb
128
131
  - lib/rails_audit_log/test_helpers.rb
129
132
  - lib/rails_audit_log/version.rb
130
133
  - lib/tasks/rails_audit_log_tasks.rake