event_engine-delivery 0.1.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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +493 -0
  5. data/Rakefile +9 -0
  6. data/app/assets/config/event_engine_manifest.js +1 -0
  7. data/app/assets/stylesheets/event_engine/application.css +15 -0
  8. data/app/controllers/event_engine/application_controller.rb +4 -0
  9. data/app/helpers/event_engine/application_helper.rb +4 -0
  10. data/app/jobs/event_engine/application_job.rb +4 -0
  11. data/app/jobs/event_engine/outbox_cleanup_job.rb +13 -0
  12. data/app/jobs/event_engine/publish_outbox_events_job.rb +16 -0
  13. data/app/mailers/event_engine/application_mailer.rb +6 -0
  14. data/app/models/event_engine/application_record.rb +5 -0
  15. data/app/models/event_engine/outbox_event.rb +123 -0
  16. data/app/views/layouts/event_engine/application.html.erb +15 -0
  17. data/config/routes.rb +2 -0
  18. data/db/migrate/20251216190654_create_event_engine_outbox_events.rb +9 -0
  19. data/db/migrate/20251216194009_add_event_type_to_event_engine_outbox_events.rb +5 -0
  20. data/db/migrate/20251216201941_add_payload_to_event_engine_outbox_events.rb +9 -0
  21. data/db/migrate/20251216203339_add_published_at_to_event_engine_outbox_events.rb +5 -0
  22. data/db/migrate/20251216213819_add_idempotency_key_to_event_engine_outbox_events.rb +6 -0
  23. data/db/migrate/20251217155422_add_attempts_to_event_engine_outbox_events.rb +5 -0
  24. data/db/migrate/20251217174013_add_dead_lettered_at_to_event_engine_outbox_events.rb +5 -0
  25. data/db/migrate/20251220213944_add_event_version_to_outbox_events.rb +20 -0
  26. data/db/migrate/20251221193107_add_occurred_at_and_metadata_to_outbox_events.rb +6 -0
  27. data/db/migrate/20260127000000_add_indexes_to_event_engine_outbox_events.rb +8 -0
  28. data/db/migrate/20260212000001_add_constraints_and_indexes_to_event_engine_outbox_events.rb +6 -0
  29. data/db/migrate/20260212000002_add_error_context_to_event_engine_outbox_events.rb +6 -0
  30. data/db/migrate/20260212000003_add_aggregate_columns_to_event_engine_outbox_events.rb +9 -0
  31. data/db/migrate/20260212000005_add_process_type_to_event_engine_outbox_events.rb +5 -0
  32. data/db/migrate/20260921000000_add_subject_and_domain_to_event_engine_outbox_events.rb +6 -0
  33. data/lib/event_engine/cloud/api_client.rb +62 -0
  34. data/lib/event_engine/cloud/batch.rb +50 -0
  35. data/lib/event_engine/cloud/reporter.rb +155 -0
  36. data/lib/event_engine/cloud/serializer.rb +56 -0
  37. data/lib/event_engine/cloud/subscribers.rb +46 -0
  38. data/lib/event_engine/definition_transport_check.rb +35 -0
  39. data/lib/event_engine/delivery/configuration.rb +65 -0
  40. data/lib/event_engine/delivery/engine.rb +25 -0
  41. data/lib/event_engine/delivery/handler.rb +52 -0
  42. data/lib/event_engine/delivery/version.rb +5 -0
  43. data/lib/event_engine/delivery.rb +61 -0
  44. data/lib/event_engine/kafka_producer.rb +16 -0
  45. data/lib/event_engine/locking_strategy.rb +31 -0
  46. data/lib/event_engine/outbox_publisher.rb +89 -0
  47. data/lib/event_engine/outbox_router.rb +58 -0
  48. data/lib/event_engine/outbox_writer.rb +7 -0
  49. data/lib/event_engine/transports/in_memory_transport.rb +27 -0
  50. data/lib/event_engine/transports/kafka.rb +45 -0
  51. data/lib/event_engine/transports/null_transport.rb +27 -0
  52. data/lib/event_engine-delivery.rb +20 -0
  53. data/lib/generators/event_engine/install_generator.rb +45 -0
  54. data/lib/generators/event_engine/templates/event_schema.rb +10 -0
  55. data/lib/generators/event_engine/templates/initializer.rb +14 -0
  56. data/lib/tasks/event_engine_dead_letters.rake +65 -0
  57. data/lib/tasks/event_engine_outbox.rake +25 -0
  58. data/lib/tasks/event_engine_schema.rake +49 -0
  59. data/lib/tasks/event_engine_schema_check.rake +20 -0
  60. data/lib/tasks/event_engine_tasks.rake +4 -0
  61. metadata +183 -0
@@ -0,0 +1,123 @@
1
+ module EventEngine
2
+ # Represents an event persisted in the outbox table. Events are written here
3
+ # before being published through a transport, implementing the outbox pattern
4
+ # for reliable delivery.
5
+ #
6
+ # @!attribute [rw] event_name
7
+ # @return [String] the event name (e.g. "cow_fed")
8
+ # @!attribute [rw] event_type
9
+ # @return [String] the event type (e.g. "domain")
10
+ # @!attribute [rw] event_version
11
+ # @return [Integer] the schema version
12
+ # @!attribute [rw] payload
13
+ # @return [Hash] the event payload data
14
+ # @!attribute [rw] metadata
15
+ # @return [Hash, nil] optional contextual metadata
16
+ # @!attribute [rw] idempotency_key
17
+ # @return [String, nil] unique key for deduplication
18
+ # @!attribute [rw] attempts
19
+ # @return [Integer] number of publish attempts
20
+ # @!attribute [rw] published_at
21
+ # @return [Time, nil] when the event was successfully published
22
+ # @!attribute [rw] dead_lettered_at
23
+ # @return [Time, nil] when the event was dead-lettered
24
+ # @!attribute [rw] occurred_at
25
+ # @return [Time] when the event occurred
26
+ class OutboxEvent < ApplicationRecord
27
+ self.table_name = "event_engine_outbox_events"
28
+
29
+ attr_readonly :event_name, :event_type, :event_version, :payload,
30
+ :metadata, :occurred_at, :idempotency_key
31
+
32
+ validates :event_name, presence: true
33
+ validates :event_type, presence: true
34
+ validates :payload, presence: true
35
+ validates :idempotency_key, uniqueness: true, allow_nil: true
36
+
37
+ # @!scope class
38
+ # @!method active
39
+ # Events that have not been dead-lettered.
40
+ # @return [ActiveRecord::Relation]
41
+ scope :active, -> { where(dead_lettered_at: nil) }
42
+
43
+ # @!scope class
44
+ # @!method dead_lettered
45
+ # Events that have been dead-lettered after exceeding max attempts.
46
+ # @return [ActiveRecord::Relation]
47
+ scope :dead_lettered, -> { where.not(dead_lettered_at: nil) }
48
+
49
+ # @!scope class
50
+ # @!method ordered
51
+ # Events ordered by creation time (FIFO).
52
+ # @return [ActiveRecord::Relation]
53
+ scope :ordered, -> { order(:created_at) }
54
+
55
+ # @!scope class
56
+ # @!method retryable(max_attempts)
57
+ # Events with fewer attempts than the maximum.
58
+ # @param max_attempts [Integer]
59
+ # @return [ActiveRecord::Relation]
60
+ scope :retryable, ->(max_attempts) { where("attempts < ?", max_attempts) }
61
+
62
+ # @!scope class
63
+ # @!method unpublished
64
+ # Events not yet published.
65
+ # @return [ActiveRecord::Relation]
66
+ scope :unpublished, -> { where(published_at: nil) }
67
+
68
+ # @!scope class
69
+ # @!method published_before(time)
70
+ # Events published before a given time (for cleanup).
71
+ # @param time [Time]
72
+ # @return [ActiveRecord::Relation]
73
+ scope :published_before, ->(time) { where("published_at < ?", time) }
74
+
75
+ # @!scope class
76
+ # @!method cleanable
77
+ # Published events that are not dead-lettered (safe to delete).
78
+ # @return [ActiveRecord::Relation]
79
+ scope :cleanable, -> { where.not(published_at: nil).where(dead_lettered_at: nil) }
80
+
81
+ scope :for_aggregate, ->(type, id) { where(aggregate_type: type, aggregate_id: id).ordered }
82
+
83
+ def self.next_aggregate_version(type, id)
84
+ max = where(aggregate_type: type, aggregate_id: id).maximum(:aggregate_version)
85
+ (max || 0) + 1
86
+ end
87
+
88
+ # Marks the event as dead-lettered.
89
+ #
90
+ # @return [void]
91
+ def dead_letter!
92
+ update!(dead_lettered_at: Time.current)
93
+ end
94
+
95
+ # Whether the event has been dead-lettered.
96
+ #
97
+ # @return [Boolean]
98
+ def dead_lettered?
99
+ dead_lettered_at.present?
100
+ end
101
+
102
+ # Resets the event for retry by clearing attempts and dead-letter status.
103
+ #
104
+ # @return [void]
105
+ def retry!
106
+ update!(attempts: 0, dead_lettered_at: nil, last_error_message: nil, last_error_class: nil)
107
+ end
108
+
109
+ # Increments the attempt counter.
110
+ #
111
+ # @return [void]
112
+ def increment_attempts!
113
+ update!(attempts: (attempts || 0) + 1)
114
+ end
115
+
116
+ # Marks the event as successfully published.
117
+ #
118
+ # @return [void]
119
+ def mark_published!
120
+ update!(published_at: Time.current)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Event engine</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "event_engine/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ EventEngine::Delivery::Engine.routes.draw do
2
+ end
@@ -0,0 +1,9 @@
1
+ class CreateEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :event_engine_outbox_events do |t|
4
+ t.string :event_name
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddEventTypeToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :event_type, :string, null: false
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class AddPayloadToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ if connection.adapter_name.downcase.include?("postgres")
4
+ add_column :event_engine_outbox_events, :payload, :jsonb, null: false
5
+ else
6
+ add_column :event_engine_outbox_events, :payload, :json, null: false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddPublishedAtToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :published_at, :datetime
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddIdempotencyKeyToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :idempotency_key, :string
4
+ add_index :event_engine_outbox_events, :idempotency_key, unique: true
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddAttemptsToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :attempts, :integer, null: false, default: 0
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddDeadLetteredAtToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :dead_lettered_at, :datetime
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ class AddEventVersionToOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def up
3
+ # 1. Add column allowing NULLs
4
+ add_column :event_engine_outbox_events, :event_version, :integer
5
+
6
+ # 2. Backfill existing rows
7
+ execute <<~SQL
8
+ UPDATE event_engine_outbox_events
9
+ SET event_version = 1
10
+ WHERE event_version IS NULL
11
+ SQL
12
+
13
+ # 3. Enforce NOT NULL constraint
14
+ change_column_null :event_engine_outbox_events, :event_version, false
15
+ end
16
+
17
+ def down
18
+ remove_column :event_engine_outbox_events, :event_version
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ class AddOccurredAtAndMetadataToOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :occurred_at, :datetime, null: false
4
+ add_column :event_engine_outbox_events, :metadata, :json
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ class AddIndexesToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_index :event_engine_outbox_events, :published_at
4
+ add_index :event_engine_outbox_events, :dead_lettered_at
5
+ add_index :event_engine_outbox_events, [:published_at, :dead_lettered_at, :created_at],
6
+ name: "idx_outbox_events_publishable"
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ class AddConstraintsAndIndexesToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ change_column_null :event_engine_outbox_events, :event_name, false
4
+ add_index :event_engine_outbox_events, :created_at
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddErrorContextToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :last_error_message, :text
4
+ add_column :event_engine_outbox_events, :last_error_class, :string
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ class AddAggregateColumnsToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :event_engine_outbox_events, :aggregate_type, :string
4
+ add_column :event_engine_outbox_events, :aggregate_id, :string
5
+ add_column :event_engine_outbox_events, :aggregate_version, :integer
6
+ add_index :event_engine_outbox_events, [:aggregate_type, :aggregate_id],
7
+ name: "idx_outbox_events_aggregate"
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddProcessTypeToEventEngineOutboxEvents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :event_engine_outbox_events, :process_type, :string
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddSubjectAndDomainToEventEngineOutboxEvents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :event_engine_outbox_events, :subject, :string
4
+ add_column :event_engine_outbox_events, :domain, :string
5
+ end
6
+ end
@@ -0,0 +1,62 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "uri"
4
+
5
+ module EventEngine
6
+ module Cloud
7
+ # HTTP client for the EventEngine Cloud ingestion API.
8
+ # Uses +Net::HTTP+ (stdlib) with a 5-second timeout. All errors are
9
+ # rescued and logged — network failures never propagate to the host app.
10
+ class ApiClient
11
+ # @return [Integer] HTTP timeout in seconds
12
+ TIMEOUT = 5
13
+
14
+ # @param api_key [String] the Cloud API key
15
+ # @param endpoint [String] the Cloud API base URL
16
+ def initialize(api_key:, endpoint:)
17
+ @api_key = api_key
18
+ @endpoint = endpoint
19
+ end
20
+
21
+ # Posts a batch of event entries to the Cloud API.
22
+ #
23
+ # @param entries [Array<Hash>] serialized event metadata entries
24
+ # @return [Boolean] true on success, false on failure
25
+ def send_batch(entries)
26
+ post("/events", { entries: entries })
27
+ end
28
+
29
+ # Posts a heartbeat with app and schema info to the Cloud API.
30
+ #
31
+ # @param heartbeat [Hash] heartbeat data
32
+ # @return [Boolean] true on success, false on failure
33
+ def send_heartbeat(heartbeat)
34
+ post("/heartbeat", heartbeat)
35
+ end
36
+
37
+ private
38
+
39
+ def post(path, body)
40
+ uri = URI("#{@endpoint}#{path}")
41
+ http = Net::HTTP.new(uri.host, uri.port)
42
+ http.use_ssl = uri.scheme == "https"
43
+ http.open_timeout = TIMEOUT
44
+ http.read_timeout = TIMEOUT
45
+
46
+ request = Net::HTTP::Post.new(uri.path)
47
+ request["Authorization"] = "Bearer #{@api_key}"
48
+ request["Content-Type"] = "application/json"
49
+ request["X-EventEngine-Gem-Version"] = EventEngine::VERSION
50
+ request.body = JSON.generate(body)
51
+
52
+ response = http.request(request)
53
+ response.code.start_with?("2")
54
+ rescue StandardError => e
55
+ EventEngine::Delivery.configuration.logger.error(
56
+ "[EventEngine::Cloud] API request failed: #{e.class} - #{e.message}"
57
+ )
58
+ false
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,50 @@
1
+ module EventEngine
2
+ module Cloud
3
+ # Thread-safe accumulator for Cloud Reporter entries.
4
+ # Entries are pushed individually and drained in bulk for flushing.
5
+ class Batch
6
+ # @param max_size [Integer] triggers auto-flush when reached
7
+ def initialize(max_size:)
8
+ @max_size = max_size
9
+ @entries = []
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ # Adds an entry to the batch.
14
+ #
15
+ # @param entry [Hash] serialized event metadata
16
+ # @return [Integer] current batch size after push
17
+ def push(entry)
18
+ @mutex.synchronize do
19
+ @entries << entry
20
+ @entries.size
21
+ end
22
+ end
23
+
24
+ # Removes and returns all entries, emptying the batch.
25
+ #
26
+ # @return [Array<Hash>]
27
+ def drain
28
+ @mutex.synchronize do
29
+ entries = @entries.dup
30
+ @entries.clear
31
+ entries
32
+ end
33
+ end
34
+
35
+ # Whether the batch has reached its max size.
36
+ #
37
+ # @return [Boolean]
38
+ def full?
39
+ @mutex.synchronize { @entries.size >= @max_size }
40
+ end
41
+
42
+ # Returns the current number of entries.
43
+ #
44
+ # @return [Integer]
45
+ def size
46
+ @mutex.synchronize { @entries.size }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,155 @@
1
+ module EventEngine
2
+ module Cloud
3
+ # Singleton that collects event metadata and sends it to EventEngine Cloud.
4
+ # Manages the full lifecycle: start, collect via {Subscribers}, batch, flush, shutdown.
5
+ #
6
+ # Activated automatically at boot when +cloud_api_key+ is configured.
7
+ # All errors are rescued — reporter failures never affect the host application.
8
+ class Reporter
9
+ class << self
10
+ # Returns the singleton instance.
11
+ #
12
+ # @return [Reporter]
13
+ def instance
14
+ @instance ||= new
15
+ end
16
+
17
+ # Resets the singleton, discarding state without flushing.
18
+ #
19
+ # @return [void]
20
+ def reset!
21
+ if @instance
22
+ @instance.instance_variable_set(:@running, false)
23
+ timer = @instance.instance_variable_get(:@timer_thread)
24
+ timer&.kill
25
+ @instance = nil
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize
31
+ @running = false
32
+ @batch = nil
33
+ @client = nil
34
+ @mutex = Mutex.new
35
+ @timer_thread = nil
36
+ end
37
+
38
+ # Initializes the batch and API client, begins accepting entries.
39
+ #
40
+ # @return [void]
41
+ def start
42
+ config = EventEngine::Delivery.configuration
43
+ @batch = Batch.new(max_size: config.cloud_batch_size)
44
+ @client = ApiClient.new(
45
+ api_key: config.cloud_api_key,
46
+ endpoint: config.cloud_endpoint
47
+ )
48
+ @running = true
49
+ @flush_interval = config.cloud_flush_interval
50
+ start_timer
51
+
52
+ logger.info("[EventEngine] Cloud Reporter started — reporting to #{config.cloud_endpoint}")
53
+ end
54
+
55
+ # Flushes remaining entries and stops the reporter.
56
+ #
57
+ # @return [void]
58
+ def shutdown
59
+ return unless @running
60
+
61
+ @running = false
62
+ stop_timer
63
+ flush
64
+
65
+ logger.info("[EventEngine] Cloud Reporter stopped")
66
+ end
67
+
68
+ # Whether the reporter is currently active.
69
+ #
70
+ # @return [Boolean]
71
+ def running?
72
+ @running
73
+ end
74
+
75
+ # Records an emitted event entry.
76
+ # @param entry [Hash] serialized event metadata
77
+ # @return [void]
78
+ def track_emit(entry)
79
+ push(entry)
80
+ end
81
+
82
+ # Records a published event entry.
83
+ # @param entry [Hash] serialized event metadata
84
+ # @return [void]
85
+ def track_publish(entry)
86
+ push(entry)
87
+ end
88
+
89
+ # Records a dead-lettered event entry.
90
+ # @param entry [Hash] serialized event metadata
91
+ # @return [void]
92
+ def track_dead_letter(entry)
93
+ push(entry)
94
+ end
95
+
96
+ # Drains the batch and sends entries to the Cloud API.
97
+ # Does nothing if the batch is empty. Errors are logged, never raised.
98
+ #
99
+ # @return [void]
100
+ def flush
101
+ return unless @batch
102
+
103
+ entries = @batch.drain
104
+ return if entries.empty?
105
+
106
+ @client.send_batch(entries)
107
+ rescue StandardError => e
108
+ EventEngine::Delivery.configuration.logger.error(
109
+ "[EventEngine::Cloud] Flush failed: #{e.class} - #{e.message}"
110
+ )
111
+ end
112
+
113
+ # Returns the number of entries currently queued.
114
+ #
115
+ # @return [Integer]
116
+ def batch_size
117
+ @batch&.size || 0
118
+ end
119
+
120
+ private
121
+
122
+ def start_timer
123
+ @timer_thread = Thread.new do
124
+ while @running
125
+ sleep(@flush_interval)
126
+ begin
127
+ flush if @running
128
+ rescue StandardError => e
129
+ logger.error("[EventEngine::Cloud] Timer thread error: #{e.class} - #{e.message}")
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def stop_timer
136
+ return unless @timer_thread
137
+
138
+ @timer_thread.join([@flush_interval * 2, 5].min)
139
+ @timer_thread.kill if @timer_thread.alive?
140
+ @timer_thread = nil
141
+ end
142
+
143
+ def push(entry)
144
+ return unless @running && @batch
145
+
146
+ @batch.push(entry)
147
+ flush if @batch.full?
148
+ end
149
+
150
+ def logger
151
+ EventEngine::Delivery.configuration.logger
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,56 @@
1
+ module EventEngine
2
+ module Cloud
3
+ # Converts +ActiveSupport::Notifications+ payloads into metadata-only
4
+ # entries for the Cloud API. Event payloads and PII are never included.
5
+ class Serializer
6
+ # Serializes an event emission notification.
7
+ #
8
+ # @param notification_payload [Hash] the AS::Notifications payload
9
+ # @return [Hash] metadata entry with +:status+ set to +"emitted"+
10
+ def self.serialize_emit(notification_payload)
11
+ {
12
+ event_id: notification_payload[:event_id],
13
+ event_name: notification_payload[:event_name],
14
+ event_version: notification_payload[:event_version],
15
+ idempotency_key: notification_payload[:idempotency_key],
16
+ aggregate_type: notification_payload[:aggregate_type],
17
+ aggregate_id: notification_payload[:aggregate_id],
18
+ aggregate_version: notification_payload[:aggregate_version],
19
+ status: "emitted",
20
+ timestamp: Time.current.iso8601
21
+ }
22
+ end
23
+
24
+ # Serializes a publish notification.
25
+ #
26
+ # @param notification_payload [Hash] the AS::Notifications payload
27
+ # @return [Hash] metadata entry with +:status+ set to +"published"+
28
+ def self.serialize_publish(notification_payload)
29
+ {
30
+ event_id: notification_payload[:event_id],
31
+ event_name: notification_payload[:event_name],
32
+ event_version: notification_payload[:event_version],
33
+ status: "published",
34
+ timestamp: Time.current.iso8601
35
+ }
36
+ end
37
+
38
+ # Serializes a dead-letter notification. Includes error details.
39
+ #
40
+ # @param notification_payload [Hash] the AS::Notifications payload
41
+ # @return [Hash] metadata entry with +:status+ set to +"dead_lettered"+
42
+ def self.serialize_dead_letter(notification_payload)
43
+ {
44
+ event_id: notification_payload[:event_id],
45
+ event_name: notification_payload[:event_name],
46
+ event_version: notification_payload[:event_version],
47
+ status: "dead_lettered",
48
+ attempts: notification_payload[:attempts],
49
+ error_message: notification_payload[:error_message],
50
+ error_class: notification_payload[:error_class],
51
+ timestamp: Time.current.iso8601
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ module EventEngine
2
+ module Cloud
3
+ # Subscribes to +ActiveSupport::Notifications+ and feeds serialized
4
+ # event metadata to the {Reporter}.
5
+ class Subscribers
6
+ class << self
7
+ # Subscribes to all EventEngine notifications.
8
+ #
9
+ # @param reporter [Reporter] the reporter to send entries to
10
+ # @return [void]
11
+ def subscribe!(reporter:)
12
+ @subscriptions = []
13
+ @reporter = reporter
14
+
15
+ @subscriptions << ActiveSupport::Notifications.subscribe("event_engine.event_emitted") do |*, payload|
16
+ entry = Serializer.serialize_emit(payload)
17
+ @reporter.track_emit(entry)
18
+ end
19
+
20
+ @subscriptions << ActiveSupport::Notifications.subscribe("event_engine.event_published") do |*, payload|
21
+ entry = Serializer.serialize_publish(payload)
22
+ @reporter.track_publish(entry)
23
+ end
24
+
25
+ @subscriptions << ActiveSupport::Notifications.subscribe("event_engine.event_dead_lettered") do |*, payload|
26
+ entry = Serializer.serialize_dead_letter(payload)
27
+ @reporter.track_dead_letter(entry)
28
+ end
29
+ end
30
+
31
+ # Removes all notification subscriptions.
32
+ #
33
+ # @return [void]
34
+ def unsubscribe!
35
+ return unless @subscriptions
36
+
37
+ @subscriptions.each do |subscription|
38
+ ActiveSupport::Notifications.unsubscribe(subscription)
39
+ end
40
+ @subscriptions = nil
41
+ @reporter = nil
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end