jetstream_bridge 4.4.0 → 4.5.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/CHANGELOG.md +92 -337
- data/README.md +1 -5
- data/docs/GETTING_STARTED.md +11 -7
- data/docs/PRODUCTION.md +51 -11
- data/docs/TESTING.md +24 -35
- data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +1 -1
- data/lib/generators/jetstream_bridge/initializer/initializer_generator.rb +1 -1
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +0 -4
- data/lib/generators/jetstream_bridge/install/install_generator.rb +5 -5
- data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +2 -2
- data/lib/jetstream_bridge/consumer/consumer.rb +34 -96
- data/lib/jetstream_bridge/consumer/health_monitor.rb +107 -0
- data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +51 -34
- data/lib/jetstream_bridge/core/config.rb +153 -46
- data/lib/jetstream_bridge/core/connection_manager.rb +513 -0
- data/lib/jetstream_bridge/core/debug_helper.rb +9 -3
- data/lib/jetstream_bridge/core/health_checker.rb +184 -0
- data/lib/jetstream_bridge/core.rb +0 -2
- data/lib/jetstream_bridge/facade.rb +212 -0
- data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +110 -0
- data/lib/jetstream_bridge/publisher/publisher.rb +87 -117
- data/lib/jetstream_bridge/rails/integration.rb +8 -5
- data/lib/jetstream_bridge/rails/railtie.rb +4 -3
- data/lib/jetstream_bridge/tasks/install.rake +0 -1
- data/lib/jetstream_bridge/topology/topology.rb +6 -1
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +206 -297
- metadata +7 -5
- data/lib/jetstream_bridge/core/bridge_helpers.rb +0 -109
- data/lib/jetstream_bridge/core/connection.rb +0 -464
- data/lib/jetstream_bridge/core/connection_factory.rb +0 -100
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'logging'
|
|
4
|
+
|
|
5
|
+
module JetstreamBridge
|
|
6
|
+
# Health checking service for JetStream Bridge
|
|
7
|
+
#
|
|
8
|
+
# Responsible for:
|
|
9
|
+
# - Connection health monitoring
|
|
10
|
+
# - Stream information gathering
|
|
11
|
+
# - Performance metrics collection
|
|
12
|
+
# - Rate limiting health check requests
|
|
13
|
+
class HealthChecker
|
|
14
|
+
# Rate limit window for uncached health checks in seconds
|
|
15
|
+
RATE_LIMIT_WINDOW = 5
|
|
16
|
+
|
|
17
|
+
def initialize(connection_manager, config)
|
|
18
|
+
@connection_manager = connection_manager
|
|
19
|
+
@config = config
|
|
20
|
+
@rate_limiter_mutex = Mutex.new
|
|
21
|
+
@last_uncached_check = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Perform comprehensive health check
|
|
25
|
+
#
|
|
26
|
+
# @param skip_cache [Boolean] Force fresh health check (rate limited)
|
|
27
|
+
# @return [Hash] Health status including connection, stream, performance, config, and version
|
|
28
|
+
def check(skip_cache: false)
|
|
29
|
+
enforce_rate_limit! if skip_cache
|
|
30
|
+
|
|
31
|
+
start_time = Time.now
|
|
32
|
+
|
|
33
|
+
connection_health = fetch_connection_health(skip_cache)
|
|
34
|
+
stream_info = fetch_stream_info if should_fetch_stream_info?(connection_health)
|
|
35
|
+
rtt_ms = measure_nats_rtt if should_measure_rtt?(connection_health)
|
|
36
|
+
|
|
37
|
+
health_check_duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
healthy: overall_healthy?(connection_health, stream_info),
|
|
41
|
+
connection: connection_health,
|
|
42
|
+
stream: stream_info || default_stream_info,
|
|
43
|
+
performance: {
|
|
44
|
+
nats_rtt_ms: rtt_ms,
|
|
45
|
+
health_check_duration_ms: health_check_duration_ms
|
|
46
|
+
},
|
|
47
|
+
config: config_summary,
|
|
48
|
+
version: JetstreamBridge::VERSION
|
|
49
|
+
}
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
{
|
|
52
|
+
healthy: false,
|
|
53
|
+
connection: {
|
|
54
|
+
state: :failed,
|
|
55
|
+
connected: false
|
|
56
|
+
},
|
|
57
|
+
error: "#{e.class}: #{e.message}"
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def enforce_rate_limit!
|
|
64
|
+
@rate_limiter_mutex.synchronize do
|
|
65
|
+
now = Time.now
|
|
66
|
+
if @last_uncached_check
|
|
67
|
+
time_since = now - @last_uncached_check
|
|
68
|
+
if time_since < RATE_LIMIT_WINDOW
|
|
69
|
+
raise HealthCheckFailedError,
|
|
70
|
+
"Health check rate limit exceeded. Please wait #{(RATE_LIMIT_WINDOW - time_since).ceil} second(s)"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
@last_uncached_check = now
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fetch_connection_health(skip_cache)
|
|
78
|
+
if @connection_manager
|
|
79
|
+
@connection_manager.health_check(skip_cache: skip_cache)
|
|
80
|
+
else
|
|
81
|
+
{
|
|
82
|
+
connected: false,
|
|
83
|
+
state: :disconnected,
|
|
84
|
+
connected_at: nil,
|
|
85
|
+
last_error: 'Not connected',
|
|
86
|
+
last_error_at: nil
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def should_fetch_stream_info?(connection_health)
|
|
92
|
+
connection_health[:connected] && !@config.disable_js_api
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def should_measure_rtt?(connection_health)
|
|
96
|
+
connection_health[:connected] && !@config.disable_js_api
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def overall_healthy?(connection_health, stream_info)
|
|
100
|
+
return false unless connection_health[:connected]
|
|
101
|
+
return true if @config.disable_js_api
|
|
102
|
+
|
|
103
|
+
stream_info&.fetch(:exists, false) || false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def default_stream_info
|
|
107
|
+
{ exists: nil, name: @config.stream_name }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def config_summary
|
|
111
|
+
{
|
|
112
|
+
app_name: @config.app_name,
|
|
113
|
+
destination_app: @config.destination_app,
|
|
114
|
+
use_outbox: @config.use_outbox,
|
|
115
|
+
use_inbox: @config.use_inbox,
|
|
116
|
+
use_dlq: @config.use_dlq,
|
|
117
|
+
disable_js_api: @config.disable_js_api
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def fetch_stream_info
|
|
122
|
+
return nil unless @connection_manager
|
|
123
|
+
|
|
124
|
+
jts = @connection_manager.jetstream
|
|
125
|
+
return nil unless jts
|
|
126
|
+
|
|
127
|
+
info = jts.stream_info(@config.stream_name)
|
|
128
|
+
|
|
129
|
+
# Handle both object-style and hash-style access for compatibility
|
|
130
|
+
config_data = info.config
|
|
131
|
+
state_data = info.state
|
|
132
|
+
subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
|
|
133
|
+
messages = state_data.respond_to?(:messages) ? state_data.messages : state_data[:messages]
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
exists: true,
|
|
137
|
+
name: @config.stream_name,
|
|
138
|
+
subjects: subjects,
|
|
139
|
+
messages: messages
|
|
140
|
+
}
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
{
|
|
143
|
+
exists: false,
|
|
144
|
+
name: @config.stream_name,
|
|
145
|
+
error: "#{e.class}: #{e.message}"
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def measure_nats_rtt
|
|
150
|
+
return nil unless @connection_manager
|
|
151
|
+
|
|
152
|
+
nc = @connection_manager.nats_client
|
|
153
|
+
return nil unless nc
|
|
154
|
+
|
|
155
|
+
# Prefer native RTT API when available
|
|
156
|
+
if nc.respond_to?(:rtt)
|
|
157
|
+
rtt_value = normalize_ms(nc.rtt)
|
|
158
|
+
return rtt_value if rtt_value
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Fallback: measure ping/pong via flush
|
|
162
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
163
|
+
nc.flush(1)
|
|
164
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
165
|
+
normalize_ms(duration)
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
Logging.warn(
|
|
168
|
+
"Failed to measure NATS RTT: #{e.class} #{e.message}",
|
|
169
|
+
tag: 'JetstreamBridge::HealthChecker'
|
|
170
|
+
)
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def normalize_ms(value)
|
|
175
|
+
return nil if value.nil?
|
|
176
|
+
return nil unless value.respond_to?(:to_f)
|
|
177
|
+
|
|
178
|
+
numeric = value.to_f
|
|
179
|
+
# Heuristic: sub-1 values are likely seconds; convert them to ms
|
|
180
|
+
ms = numeric < 1 ? numeric * 1000 : numeric
|
|
181
|
+
ms.round(2)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'core/config'
|
|
4
|
+
require_relative 'core/connection_manager'
|
|
5
|
+
require_relative 'core/health_checker'
|
|
6
|
+
require_relative 'core/logging'
|
|
7
|
+
require_relative 'publisher/publisher'
|
|
8
|
+
require_relative 'consumer/consumer'
|
|
9
|
+
require_relative 'topology/topology'
|
|
10
|
+
|
|
11
|
+
module JetstreamBridge
|
|
12
|
+
# Facade that coordinates all subsystems
|
|
13
|
+
#
|
|
14
|
+
# Responsible for:
|
|
15
|
+
# - Managing configuration
|
|
16
|
+
# - Managing connection lifecycle
|
|
17
|
+
# - Creating publishers and consumers
|
|
18
|
+
# - Delegating health checks
|
|
19
|
+
class Facade
|
|
20
|
+
attr_reader :config
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@config = Config.new
|
|
24
|
+
@connection_manager = nil
|
|
25
|
+
@publisher = nil
|
|
26
|
+
@health_checker = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Configure JetStream Bridge
|
|
30
|
+
#
|
|
31
|
+
# @yield [Config] Configuration object
|
|
32
|
+
# @return [Config] The configured instance
|
|
33
|
+
def configure
|
|
34
|
+
yield(@config) if block_given?
|
|
35
|
+
@config
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Connect to NATS and ensure topology
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def connect!
|
|
42
|
+
ensure_connection_manager!
|
|
43
|
+
@connection_manager.connect!
|
|
44
|
+
Logging.info('JetStream Bridge connected successfully', tag: 'JetstreamBridge')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Disconnect from NATS
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def disconnect!
|
|
51
|
+
return unless @connection_manager
|
|
52
|
+
|
|
53
|
+
@connection_manager.disconnect!
|
|
54
|
+
Logging.info('JetStream Bridge disconnected', tag: 'JetstreamBridge')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reconnect to NATS
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
def reconnect!
|
|
61
|
+
Logging.info('Reconnecting to NATS...', tag: 'JetstreamBridge')
|
|
62
|
+
disconnect!
|
|
63
|
+
connect!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Publish an event
|
|
67
|
+
#
|
|
68
|
+
# @param event_type [String] Event type
|
|
69
|
+
# @param payload [Hash] Event payload
|
|
70
|
+
# @param options [Hash] Additional options
|
|
71
|
+
# @return [Models::PublishResult] Result object
|
|
72
|
+
def publish(event_type:, payload:, **)
|
|
73
|
+
connect_if_needed!
|
|
74
|
+
publisher.publish(event_type: event_type, payload: payload, **)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Publish a complete event envelope (advanced)
|
|
78
|
+
#
|
|
79
|
+
# @param envelope [Hash] Complete event envelope
|
|
80
|
+
# @param subject [String, nil] Optional subject override
|
|
81
|
+
# @return [Models::PublishResult] Result object
|
|
82
|
+
def publish_envelope(envelope, subject: nil)
|
|
83
|
+
connect_if_needed!
|
|
84
|
+
publisher.publish_envelope(envelope, subject: subject)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Subscribe to events
|
|
88
|
+
#
|
|
89
|
+
# @param handler [Proc, #call, nil] Message handler
|
|
90
|
+
# @param options [Hash] Consumer options
|
|
91
|
+
# @yield [event] Optional block as handler
|
|
92
|
+
# @return [Consumer] Consumer instance
|
|
93
|
+
def subscribe(handler = nil, **, &block)
|
|
94
|
+
connect_if_needed!
|
|
95
|
+
create_consumer(handler || block, **)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if connected to NATS
|
|
99
|
+
#
|
|
100
|
+
# @param skip_cache [Boolean] Force fresh check
|
|
101
|
+
# @return [Boolean] true if connected and healthy
|
|
102
|
+
def connected?(skip_cache: false)
|
|
103
|
+
return false unless @connection_manager
|
|
104
|
+
|
|
105
|
+
@connection_manager.connected?(skip_cache: skip_cache)
|
|
106
|
+
rescue StandardError
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get comprehensive health status (unified method)
|
|
111
|
+
#
|
|
112
|
+
# Provides complete system health information including connection state,
|
|
113
|
+
# stream info, performance metrics, and configuration.
|
|
114
|
+
#
|
|
115
|
+
# Rate limited to prevent abuse (max 1 uncached check per 5 seconds).
|
|
116
|
+
#
|
|
117
|
+
# @param skip_cache [Boolean] Force fresh health check (rate limited)
|
|
118
|
+
# @return [Hash] Health status including NATS connection, stream, and version
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# health = facade.health
|
|
122
|
+
# puts "Healthy: #{health[:healthy]}"
|
|
123
|
+
# puts "Connected: #{health[:connection][:connected]}"
|
|
124
|
+
# puts "Stream exists: #{health[:stream][:exists]}"
|
|
125
|
+
# puts "RTT: #{health[:performance][:nats_rtt_ms]}ms"
|
|
126
|
+
def health(skip_cache: false)
|
|
127
|
+
health_checker.check(skip_cache: skip_cache)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Backward compatibility: Alias for health method
|
|
131
|
+
#
|
|
132
|
+
# @deprecated Use {#health} instead
|
|
133
|
+
# @param skip_cache [Boolean] Force fresh health check
|
|
134
|
+
# @return [Hash] Health status
|
|
135
|
+
def health_check(skip_cache: false)
|
|
136
|
+
health(skip_cache: skip_cache)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check if system is healthy (convenience method)
|
|
140
|
+
#
|
|
141
|
+
# @return [Boolean] true if connected and stream exists (or JS API disabled)
|
|
142
|
+
#
|
|
143
|
+
# @example
|
|
144
|
+
# if facade.healthy?
|
|
145
|
+
# puts "System is healthy and ready"
|
|
146
|
+
# end
|
|
147
|
+
def healthy?
|
|
148
|
+
health[:healthy]
|
|
149
|
+
rescue StandardError
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get stream information
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash] Stream information
|
|
156
|
+
def stream_info
|
|
157
|
+
health_checker.send(:fetch_stream_info)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def ensure_connection_manager!
|
|
163
|
+
return if @connection_manager
|
|
164
|
+
|
|
165
|
+
@config.validate!
|
|
166
|
+
@connection_manager = ConnectionManager.new(@config)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def connect_if_needed!
|
|
170
|
+
return if @connection_manager&.connected?
|
|
171
|
+
|
|
172
|
+
connect!
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get or create publisher instance (cached)
|
|
176
|
+
def publisher
|
|
177
|
+
@publisher ||= begin
|
|
178
|
+
ensure_connection_manager!
|
|
179
|
+
Publisher.new(
|
|
180
|
+
connection: @connection_manager.jetstream,
|
|
181
|
+
config: @config
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Create a new consumer instance (not cached - each subscription is independent)
|
|
187
|
+
def create_consumer(handler, durable_name: nil, batch_size: nil)
|
|
188
|
+
ensure_connection_manager!
|
|
189
|
+
Consumer.new(
|
|
190
|
+
handler,
|
|
191
|
+
connection: @connection_manager.jetstream,
|
|
192
|
+
config: @config,
|
|
193
|
+
durable_name: durable_name,
|
|
194
|
+
batch_size: batch_size
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Get or create health checker instance (cached)
|
|
199
|
+
def health_checker
|
|
200
|
+
@health_checker ||= begin
|
|
201
|
+
# Try to ensure connection manager, but don't fail if config is invalid
|
|
202
|
+
# HealthChecker will handle the nil connection_manager gracefully
|
|
203
|
+
begin
|
|
204
|
+
ensure_connection_manager!
|
|
205
|
+
rescue ConfigurationError
|
|
206
|
+
# Config not valid yet, health checker will report as unhealthy
|
|
207
|
+
end
|
|
208
|
+
HealthChecker.new(@connection_manager, @config)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module JetstreamBridge
|
|
7
|
+
# Builds event envelopes for publishing
|
|
8
|
+
#
|
|
9
|
+
# Single responsibility: Convert event parameters into a valid envelope hash.
|
|
10
|
+
# All envelope construction logic is centralized here.
|
|
11
|
+
class EventEnvelopeBuilder
|
|
12
|
+
# Build an event envelope from parameters
|
|
13
|
+
#
|
|
14
|
+
# @param event_type [String] Event type (e.g., 'created', 'user.created')
|
|
15
|
+
# @param payload [Hash] Event payload data
|
|
16
|
+
# @param resource_type [String, nil] Resource type (inferred from event_type if nil)
|
|
17
|
+
# @param options [Hash] Optional envelope fields
|
|
18
|
+
# @option options [String] :event_id Custom event ID (auto-generated if not provided)
|
|
19
|
+
# @option options [String] :producer Producer name (defaults to app_name from config)
|
|
20
|
+
# @option options [String, Time] :occurred_at Event timestamp (defaults to current time)
|
|
21
|
+
# @option options [String] :trace_id Distributed trace ID (auto-generated if not provided)
|
|
22
|
+
# @option options [String] :resource_id Resource ID (extracted from payload if not provided)
|
|
23
|
+
# @option options [Integer] :schema_version Schema version (defaults to 1)
|
|
24
|
+
#
|
|
25
|
+
# @return [Hash] Complete event envelope
|
|
26
|
+
# @raise [ArgumentError] If required parameters are missing
|
|
27
|
+
def self.build(event_type:, payload:, resource_type: nil, **options)
|
|
28
|
+
new(event_type, payload, resource_type, options).build
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(event_type, payload, resource_type, options)
|
|
32
|
+
@event_type = event_type.to_s
|
|
33
|
+
@payload = payload
|
|
34
|
+
@resource_type = resource_type
|
|
35
|
+
@options = options
|
|
36
|
+
|
|
37
|
+
validate_required!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build
|
|
41
|
+
{
|
|
42
|
+
'event_id' => event_id,
|
|
43
|
+
'schema_version' => schema_version,
|
|
44
|
+
'event_type' => @event_type,
|
|
45
|
+
'producer' => producer,
|
|
46
|
+
'resource_type' => resource_type,
|
|
47
|
+
'resource_id' => resource_id,
|
|
48
|
+
'occurred_at' => occurred_at,
|
|
49
|
+
'trace_id' => trace_id,
|
|
50
|
+
'payload' => @payload
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate_required!
|
|
57
|
+
raise ArgumentError, 'event_type is required' if @event_type.empty?
|
|
58
|
+
raise ArgumentError, 'payload is required' if @payload.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def event_id
|
|
62
|
+
@options[:event_id] || SecureRandom.uuid
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def schema_version
|
|
66
|
+
@options[:schema_version] || 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def producer
|
|
70
|
+
@options[:producer] || JetstreamBridge.config.app_name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resource_type
|
|
74
|
+
@resource_type || infer_resource_type || 'event'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def infer_resource_type
|
|
78
|
+
# Extract from "user.created" -> "user"
|
|
79
|
+
@event_type.split('.').first if @event_type.include?('.')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resource_id
|
|
83
|
+
@options[:resource_id] || extract_resource_id_from_payload
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extract_resource_id_from_payload
|
|
87
|
+
return '' unless @payload.respond_to?(:[])
|
|
88
|
+
|
|
89
|
+
payload_normalized = @payload.transform_keys(&:to_s) if @payload.respond_to?(:transform_keys)
|
|
90
|
+
payload_normalized ||= @payload
|
|
91
|
+
|
|
92
|
+
(payload_normalized['id'] || payload_normalized[:id] ||
|
|
93
|
+
payload_normalized['resource_id'] || payload_normalized[:resource_id]).to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def occurred_at
|
|
97
|
+
value = @options[:occurred_at]
|
|
98
|
+
return Time.now.utc.iso8601 if value.nil?
|
|
99
|
+
return value.iso8601 if value.is_a?(Time)
|
|
100
|
+
|
|
101
|
+
Time.parse(value.to_s).iso8601
|
|
102
|
+
rescue ArgumentError
|
|
103
|
+
Time.now.utc.iso8601
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def trace_id
|
|
107
|
+
@options[:trace_id] || SecureRandom.hex(8)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|