standard_audit 0.3.0 → 0.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: ae2015728333aa6f6549bf138de8c27017c78c23f0d930ab3df9cf73c99b14cf
4
- data.tar.gz: 6e479ec25dcb88c2538a23efbec0796762b4274fd0baa2c5ad6707f57e90b65d
3
+ metadata.gz: 6a9a29ab9754c6dbc24134d000864e8d99f60966227691ee782b782423ac7dc7
4
+ data.tar.gz: 0ee9864f7b073c3376041facfafe575006179b443937883377a5bdda605bc81a
5
5
  SHA512:
6
- metadata.gz: d3ec930cf81109adf17c563d15dba60a0e82ab33bcb0cb6ce422009f5b157b8d1335c46da73269053ed3e09e8169bbada9c548409080871d6dd9e3c928b65ce8
7
- data.tar.gz: 64906fb08b3d97c7e25626790c32fcf6eb08885cd4c0d5f7cf22a6acf9bd2516101f768fe90d93bfd3db4a3120f726b1e63eaa49036beffcf513d139f68d58f8
6
+ metadata.gz: e156d464655ca40f791b064e264b18ac587b63819f454d8bc98b9b7a9168cbd02519795c14d0333cd148be191715d29498244d1218f505365a1ef669c6f591fe
7
+ data.tar.gz: 2816bb0d249b20d8464cde9655b5799d8b7525cba615b9a97528bd9ef78ca1b1254fd528bca77ac3390001bdf34adc384add86c5b924d43a2e93bf695620ae89
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-04-19
11
+
12
+ ### Added
13
+
14
+ - Rails 8.1+ structured event reporter (`Rails.event`) integration. A new `StandardAudit::EventSubscriber` is registered automatically when `Rails.event` is available, so `Rails.event.notify("myapp.orders.created", actor: user, target: order)` persists an `AuditLog` the same way an `ActiveSupport::Notifications.instrument` call does. Event name is matched against the existing `subscribe_to` patterns (supports `*`, `**`, and `Regexp`). `Rails.event.set_context(...)` values take precedence over the `Current.*` resolvers for `request_id`, `ip_address`, `user_agent`, and `session_id`. `Rails.event.tagged(...)` and `source_location` are captured under the reserved metadata keys `_tags` and `_source`.
15
+
10
16
  ## [0.3.0] - 2026-03-31
11
17
 
12
18
  ### Added
data/README.md CHANGED
@@ -52,11 +52,38 @@ StandardAudit::AuditLog.for_actor(current_user).this_week
52
52
 
53
53
  ## Recording Events
54
54
 
55
- StandardAudit provides three ways to record audit events.
55
+ StandardAudit provides four ways to record audit events. On Rails 8.1+, prefer `Rails.event` — it is the standard Rails interface for structured events.
56
+
57
+ ### Rails.event (Rails 8.1+)
58
+
59
+ StandardAudit registers a `Rails.event` subscriber at boot, so any `notify` call whose name matches a configured `subscribe_to` pattern is persisted automatically:
60
+
61
+ ```ruby
62
+ class ApplicationController < ActionController::Base
63
+ before_action do
64
+ Rails.event.set_context(
65
+ request_id: request.request_id,
66
+ ip_address: request.remote_ip,
67
+ user_agent: request.user_agent
68
+ )
69
+ end
70
+ end
71
+
72
+ Rails.event.tagged("checkout") do
73
+ Rails.event.notify("myapp.orders.created",
74
+ actor: current_user,
75
+ target: @order,
76
+ scope: current_organisation,
77
+ total: @order.total
78
+ )
79
+ end
80
+ ```
81
+
82
+ `Rails.event.set_context` values override the `Current.*` resolvers for `request_id`, `ip_address`, `user_agent`, and `session_id`. Tags and `source_location` are captured as metadata under the reserved keys `_tags` and `_source`.
56
83
 
57
84
  ### Convenience API
58
85
 
59
- The simplest approach — call `StandardAudit.record` directly:
86
+ Call `StandardAudit.record` directly:
60
87
 
61
88
  ```ruby
62
89
  StandardAudit.record("orders.created",
@@ -71,7 +98,7 @@ When `actor` is omitted, it falls back to the configured `current_actor_resolver
71
98
 
72
99
  ### ActiveSupport::Notifications
73
100
 
74
- Instrument events and let the subscriber handle persistence:
101
+ For Rails < 8.1, or when `Rails.event` is unavailable, instrument events via `ActiveSupport::Notifications`:
75
102
 
76
103
  ```ruby
77
104
  ActiveSupport::Notifications.instrument("myapp.orders.created", {
@@ -5,6 +5,12 @@ module StandardAudit
5
5
  initializer "standard_audit.subscriber" do
6
6
  ActiveSupport.on_load(:active_record) do
7
7
  StandardAudit.subscriber.setup!
8
+
9
+ # Rails 8.1+ structured event reporter. Feature-detected so the gem
10
+ # still works on older Rails versions that only have AS::Notifications.
11
+ if Rails.respond_to?(:event) && Rails.event.respond_to?(:subscribe)
12
+ Rails.event.subscribe(StandardAudit.event_subscriber)
13
+ end
8
14
  end
9
15
  end
10
16
  end
@@ -0,0 +1,101 @@
1
+ module StandardAudit
2
+ # Subscriber for Rails 8.1+ structured event reporting (`Rails.event`).
3
+ #
4
+ # Registered with `Rails.event.subscribe(...)` so that every `Rails.event.notify`
5
+ # call flows through StandardAudit for persistence. Events whose name does not
6
+ # match any configured `subscribe_to` pattern are ignored.
7
+ #
8
+ # Payload is extracted with the same extractors used by the
9
+ # ActiveSupport::Notifications subscriber. Rails.event `context` supplies
10
+ # request_id/ip_address/user_agent/session_id and takes precedence over the
11
+ # Current.* resolvers. Tags and source_location are captured as metadata
12
+ # under the reserved keys `_tags` and `_source`.
13
+ class EventSubscriber
14
+ RESERVED_PAYLOAD_KEYS = %i[actor target scope request_id ip_address user_agent session_id].freeze
15
+
16
+ def initialize
17
+ @pattern_cache = {}
18
+ @pattern_cache_mutex = Mutex.new
19
+ end
20
+
21
+ def emit(event)
22
+ return unless StandardAudit.config.enabled
23
+
24
+ name = event[:name]
25
+ return if name.nil?
26
+ return unless matches_subscription?(name)
27
+
28
+ config = StandardAudit.config
29
+ payload = event[:payload] || {}
30
+ context = event[:context] || {}
31
+
32
+ actor = config.actor_extractor.call(payload)
33
+ target = config.target_extractor.call(payload)
34
+ scope = config.scope_extractor.call(payload)
35
+
36
+ metadata = build_metadata(payload, event[:tags], event[:source_location], config)
37
+
38
+ StandardAudit.record(
39
+ name,
40
+ actor: actor,
41
+ target: target,
42
+ scope: scope,
43
+ metadata: metadata,
44
+ request_id: context[:request_id] || payload[:request_id],
45
+ ip_address: context[:ip_address] || payload[:ip_address],
46
+ user_agent: context[:user_agent] || payload[:user_agent],
47
+ session_id: context[:session_id] || payload[:session_id]
48
+ )
49
+ rescue => e
50
+ Rails.logger.error("[StandardAudit] Error handling Rails.event: #{e.class}: #{e.message}")
51
+ end
52
+
53
+ private
54
+
55
+ def matches_subscription?(name)
56
+ StandardAudit.config.subscriptions.any? { |pattern| pattern_match?(pattern, name) }
57
+ end
58
+
59
+ # Supports the same pattern shapes as ActiveSupport::Notifications.subscribe:
60
+ # a Regexp, or a String with `*` matching a single segment and `**` matching
61
+ # the remainder.
62
+ def pattern_match?(pattern, name)
63
+ case pattern
64
+ when Regexp
65
+ pattern.match?(name)
66
+ when String
67
+ compiled_pattern_for(pattern).match?(name)
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ def compiled_pattern_for(pattern)
74
+ cached = @pattern_cache[pattern]
75
+ return cached if cached
76
+
77
+ @pattern_cache_mutex.synchronize do
78
+ @pattern_cache[pattern] ||= Regexp.new(
79
+ "\\A" + Regexp.escape(pattern).gsub('\\*\\*', ".*").gsub('\\*', "[^.]*") + "\\z"
80
+ )
81
+ end
82
+ end
83
+
84
+ # `_tags` and `_source` are reserved metadata keys owned by this
85
+ # subscriber. Sensitive-key filtering is handled downstream by
86
+ # `StandardAudit.record`, so we don't re-run it here.
87
+ def build_metadata(payload, tags, source_location, config)
88
+ reserved = RESERVED_PAYLOAD_KEYS.map(&:to_s)
89
+ raw = payload.reject { |k, _| reserved.include?(k.to_s) }
90
+ raw = config.metadata_builder.call(raw) if config.metadata_builder
91
+
92
+ if tags.is_a?(Hash) && tags.any?
93
+ raw[:_tags] = tags
94
+ elsif tags && !tags.is_a?(Hash)
95
+ Rails.logger.warn("[StandardAudit] Dropping Rails.event tags of unexpected type: #{tags.class}")
96
+ end
97
+ raw[:_source] = source_location if source_location
98
+ raw
99
+ end
100
+ end
101
+ end
@@ -67,7 +67,7 @@ module StandardAudit
67
67
  log.save!
68
68
  end
69
69
  rescue => e
70
- Rails.logger.error("[StandardAudit] Error creating audit log: #{e.message}")
70
+ Rails.logger.error("[StandardAudit] Error creating audit log: #{e.class}: #{e.message}")
71
71
  end
72
72
 
73
73
  def extract_metadata(payload, config)
@@ -1,3 +1,3 @@
1
1
  module StandardAudit
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -2,10 +2,15 @@ require "standard_audit/version"
2
2
  require "standard_audit/engine"
3
3
  require "standard_audit/configuration"
4
4
  require "standard_audit/subscriber"
5
+ require "standard_audit/event_subscriber"
5
6
  require "standard_audit/auditable"
6
7
  require "standard_audit/audit_scope"
7
8
 
8
9
  module StandardAudit
10
+ # Metadata keys owned internally by StandardAudit. Never filtered by
11
+ # `sensitive_keys` even if a user adds them there.
12
+ RESERVED_METADATA_KEYS = %w[_tags _source].freeze
13
+
9
14
  class << self
10
15
  def configure
11
16
  yield(config) if block_given?
@@ -20,8 +25,9 @@ module StandardAudit
20
25
 
21
26
  actor ||= config.current_actor_resolver.call
22
27
 
23
- # Filter sensitive keys
24
- sensitive = config.sensitive_keys.map(&:to_s)
28
+ # Filter sensitive keys. `_tags` and `_source` are reserved internal
29
+ # metadata keys owned by EventSubscriber and are never stripped.
30
+ sensitive = config.sensitive_keys.map(&:to_s) - RESERVED_METADATA_KEYS
25
31
  filtered_metadata = metadata.reject { |k, _| sensitive.include?(k.to_s) }
26
32
 
27
33
  attrs = {
@@ -90,6 +96,10 @@ module StandardAudit
90
96
  @subscriber ||= Subscriber.new
91
97
  end
92
98
 
99
+ def event_subscriber
100
+ @event_subscriber ||= EventSubscriber.new
101
+ end
102
+
93
103
  def reset_configuration!
94
104
  @configuration = nil
95
105
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -65,8 +65,9 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.0'
68
- description: StandardAudit is a standalone Rails gem for database-backed audit logging
69
- via ActiveSupport::Notifications. Generic, flexible, and works with any Rails application.
68
+ description: StandardAudit is a standalone Rails gem for database-backed audit logging.
69
+ On Rails 8.1+ it subscribes to Rails.event; on earlier versions it subscribes to
70
+ ActiveSupport::Notifications. Generic, flexible, and works with any Rails application.
70
71
  email:
71
72
  - code@jaryl.dev
72
73
  executables: []
@@ -92,6 +93,7 @@ files:
92
93
  - lib/standard_audit/auditable.rb
93
94
  - lib/standard_audit/configuration.rb
94
95
  - lib/standard_audit/engine.rb
96
+ - lib/standard_audit/event_subscriber.rb
95
97
  - lib/standard_audit/presets/standard_id.rb
96
98
  - lib/standard_audit/subscriber.rb
97
99
  - lib/standard_audit/version.rb
@@ -120,5 +122,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
122
  requirements: []
121
123
  rubygems_version: 4.0.3
122
124
  specification_version: 4
123
- summary: Database-backed audit logging for Rails via ActiveSupport::Notifications.
125
+ summary: Database-backed audit logging for Rails via Rails.event and ActiveSupport::Notifications.
124
126
  test_files: []