jetstream_bridge 5.1.0 → 7.0.1
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 +37 -0
- data/README.md +6 -3
- data/docs/API.md +395 -0
- data/docs/ARCHITECTURE.md +123 -171
- data/docs/GETTING_STARTED.md +72 -1
- data/docs/PRODUCTION.md +10 -3
- data/docs/RESTRICTED_PERMISSIONS.md +7 -14
- data/docs/TESTING.md +3 -3
- data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb +29 -13
- data/lib/jetstream_bridge/config_helpers/lifecycle.rb +34 -0
- data/lib/jetstream_bridge/config_helpers.rb +118 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +131 -41
- data/lib/jetstream_bridge/consumer/consumer_state.rb +58 -0
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +12 -2
- data/lib/jetstream_bridge/consumer/pull_subscription_builder.rb +6 -6
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +72 -110
- data/lib/jetstream_bridge/core/config.rb +31 -0
- data/lib/jetstream_bridge/core/connection.rb +97 -31
- data/lib/jetstream_bridge/core/consumer_mode_resolver.rb +64 -0
- data/lib/jetstream_bridge/core/duration.rb +30 -0
- data/lib/jetstream_bridge/models/inbox_event.rb +1 -1
- data/lib/jetstream_bridge/models/outbox_event.rb +1 -1
- data/lib/jetstream_bridge/provisioner.rb +108 -13
- data/lib/jetstream_bridge/publisher/outbox_repository.rb +35 -20
- data/lib/jetstream_bridge/publisher/publisher.rb +4 -4
- data/lib/jetstream_bridge/tasks/install.rake +2 -2
- data/lib/jetstream_bridge/topology/stream.rb +6 -1
- data/lib/jetstream_bridge/topology/topology.rb +1 -1
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +8 -12
- metadata +7 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'logger'
|
|
3
4
|
require_relative 'topology/topology'
|
|
4
5
|
require_relative 'consumer/subscription_manager'
|
|
5
6
|
require_relative 'core/logging'
|
|
6
7
|
require_relative 'core/config'
|
|
7
8
|
require_relative 'core/connection'
|
|
9
|
+
require_relative 'core/consumer_mode_resolver'
|
|
8
10
|
|
|
9
11
|
module JetstreamBridge
|
|
10
12
|
# Dedicated provisioning orchestrator to keep connection concerns separate.
|
|
@@ -13,52 +15,145 @@ module JetstreamBridge
|
|
|
13
15
|
# deploy-time with admin credentials or during runtime when auto_provision
|
|
14
16
|
# is enabled.
|
|
15
17
|
class Provisioner
|
|
18
|
+
class << self
|
|
19
|
+
# Provision both directions (A->B and B->A) with shared defaults.
|
|
20
|
+
#
|
|
21
|
+
# @param app_a [String] First app name
|
|
22
|
+
# @param app_b [String] Second app name
|
|
23
|
+
# @param stream_name [String] Stream used for both directions
|
|
24
|
+
# @param nats_url [String] NATS connection URL
|
|
25
|
+
# @param logger [Logger] Logger used for progress output
|
|
26
|
+
# @param shared_config [Hash] Additional config applied to both directions
|
|
27
|
+
# @param consumer_modes [Hash,nil] Per-app consumer modes { 'system_a' => :pull, 'system_b' => :push }
|
|
28
|
+
# @param consumer_mode [Symbol] Legacy/shared consumer mode for both directions (overridden by consumer_modes)
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
def provision_bidirectional!(
|
|
32
|
+
app_a:,
|
|
33
|
+
app_b:,
|
|
34
|
+
stream_name: 'sync-stream',
|
|
35
|
+
nats_url: ENV.fetch('NATS_URL', 'nats://nats:4222'),
|
|
36
|
+
logger: Logger.new($stdout),
|
|
37
|
+
consumer_modes: nil,
|
|
38
|
+
consumer_mode: :pull,
|
|
39
|
+
**shared_config
|
|
40
|
+
)
|
|
41
|
+
modes = build_consumer_mode_map(app_a, app_b, consumer_modes, consumer_mode)
|
|
42
|
+
|
|
43
|
+
[
|
|
44
|
+
{ app_name: app_a, destination_app: app_b },
|
|
45
|
+
{ app_name: app_b, destination_app: app_a }
|
|
46
|
+
].each do |direction|
|
|
47
|
+
direction_mode = modes[direction[:app_name]] || consumer_mode
|
|
48
|
+
logger&.info "Provisioning #{direction[:app_name]} -> #{direction[:destination_app]}"
|
|
49
|
+
configure_direction(
|
|
50
|
+
direction,
|
|
51
|
+
stream_name: stream_name,
|
|
52
|
+
nats_url: nats_url,
|
|
53
|
+
logger: logger,
|
|
54
|
+
consumer_mode: direction_mode,
|
|
55
|
+
shared_config: shared_config
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
JetstreamBridge.startup!
|
|
60
|
+
new.provision!
|
|
61
|
+
ensure
|
|
62
|
+
JetstreamBridge.shutdown!
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_consumer_mode_map(app_a, app_b, consumer_modes, fallback_mode)
|
|
68
|
+
app_a_key = app_a.to_s
|
|
69
|
+
app_b_key = app_b.to_s
|
|
70
|
+
normalized_fallback = ConsumerModeResolver.normalize(fallback_mode)
|
|
71
|
+
|
|
72
|
+
if consumer_modes
|
|
73
|
+
normalized = consumer_modes.transform_keys(&:to_s).transform_values do |v|
|
|
74
|
+
ConsumerModeResolver.normalize(v)
|
|
75
|
+
end
|
|
76
|
+
normalized[app_a_key] ||= normalized_fallback
|
|
77
|
+
normalized[app_b_key] ||= normalized_fallback
|
|
78
|
+
return normalized
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
app_a_key => ConsumerModeResolver.resolve(app_name: app_a_key, fallback: normalized_fallback),
|
|
83
|
+
app_b_key => ConsumerModeResolver.resolve(app_name: app_b_key, fallback: normalized_fallback)
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
private :build_consumer_mode_map
|
|
87
|
+
|
|
88
|
+
def configure_direction(direction, stream_name:, nats_url:, logger:, consumer_mode:, shared_config:)
|
|
89
|
+
JetstreamBridge.configure do |cfg|
|
|
90
|
+
cfg.nats_urls = nats_url
|
|
91
|
+
cfg.app_name = direction[:app_name]
|
|
92
|
+
cfg.destination_app = direction[:destination_app]
|
|
93
|
+
cfg.stream_name = stream_name
|
|
94
|
+
cfg.auto_provision = true
|
|
95
|
+
cfg.use_outbox = false
|
|
96
|
+
cfg.use_inbox = false
|
|
97
|
+
cfg.logger = logger if logger
|
|
98
|
+
cfg.consumer_mode = consumer_mode
|
|
99
|
+
|
|
100
|
+
shared_config.each do |key, value|
|
|
101
|
+
next if key.to_sym == :consumer_mode
|
|
102
|
+
|
|
103
|
+
setter = "#{key}="
|
|
104
|
+
cfg.public_send(setter, value) if cfg.respond_to?(setter)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
private :configure_direction
|
|
109
|
+
end
|
|
110
|
+
|
|
16
111
|
def initialize(config: JetstreamBridge.config)
|
|
17
112
|
@config = config
|
|
18
113
|
end
|
|
19
114
|
|
|
20
|
-
#
|
|
115
|
+
# Provision stream (and optionally consumer) with desired config.
|
|
21
116
|
#
|
|
22
117
|
# @param jts [Object, nil] Existing JetStream context (optional)
|
|
23
|
-
# @param
|
|
118
|
+
# @param provision_consumer [Boolean] Whether to create/align the consumer too
|
|
24
119
|
# @return [Object] JetStream context used for provisioning
|
|
25
|
-
def
|
|
120
|
+
def provision!(jts: nil, provision_consumer: true)
|
|
26
121
|
js = jts || Connection.connect!(verify_js: true)
|
|
27
122
|
|
|
28
|
-
|
|
29
|
-
|
|
123
|
+
provision_stream!(jts: js)
|
|
124
|
+
provision_consumer!(jts: js) if provision_consumer
|
|
30
125
|
|
|
31
126
|
Logging.info(
|
|
32
|
-
"Provisioned stream=#{@config.stream_name} consumer=#{@config.durable_name if
|
|
127
|
+
"Provisioned stream=#{@config.stream_name} consumer=#{@config.durable_name if provision_consumer}",
|
|
33
128
|
tag: 'JetstreamBridge::Provisioner'
|
|
34
129
|
)
|
|
35
130
|
|
|
36
131
|
js
|
|
37
132
|
end
|
|
38
133
|
|
|
39
|
-
#
|
|
134
|
+
# Provision stream only.
|
|
40
135
|
#
|
|
41
136
|
# @param jts [Object, nil] Existing JetStream context (optional)
|
|
42
137
|
# @return [Object] JetStream context used
|
|
43
|
-
def
|
|
138
|
+
def provision_stream!(jts: nil)
|
|
44
139
|
js = jts || Connection.connect!(verify_js: true)
|
|
45
|
-
Topology.
|
|
140
|
+
Topology.provision!(js)
|
|
46
141
|
Logging.info(
|
|
47
|
-
"Stream
|
|
142
|
+
"Stream provisioned: #{@config.stream_name}",
|
|
48
143
|
tag: 'JetstreamBridge::Provisioner'
|
|
49
144
|
)
|
|
50
145
|
js
|
|
51
146
|
end
|
|
52
147
|
|
|
53
|
-
#
|
|
148
|
+
# Provision durable consumer only.
|
|
54
149
|
#
|
|
55
150
|
# @param jts [Object, nil] Existing JetStream context (optional)
|
|
56
151
|
# @return [Object] JetStream context used
|
|
57
|
-
def
|
|
152
|
+
def provision_consumer!(jts: nil)
|
|
58
153
|
js = jts || Connection.connect!(verify_js: true)
|
|
59
154
|
SubscriptionManager.new(js, @config.durable_name, @config).ensure_consumer!(force: true)
|
|
60
155
|
Logging.info(
|
|
61
|
-
"Consumer
|
|
156
|
+
"Consumer provisioned: #{@config.durable_name}",
|
|
62
157
|
tag: 'JetstreamBridge::Provisioner'
|
|
63
158
|
)
|
|
64
159
|
js
|
|
@@ -35,29 +35,15 @@ module JetstreamBridge
|
|
|
35
35
|
(record.respond_to?(:status) && record.status == 'sent')
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def record_publish_attempt(record, subject, envelope)
|
|
39
39
|
ActiveRecord::Base.transaction do
|
|
40
|
-
|
|
41
|
-
event_id = envelope['event_id'].to_s
|
|
42
|
-
|
|
43
|
-
attrs = {
|
|
44
|
-
event_id: event_id,
|
|
45
|
-
subject: subject,
|
|
46
|
-
payload: ModelUtils.json_dump(envelope),
|
|
47
|
-
headers: ModelUtils.json_dump({ 'nats-msg-id' => event_id }),
|
|
48
|
-
status: 'publishing',
|
|
49
|
-
last_error: nil
|
|
50
|
-
}
|
|
51
|
-
attrs[:attempts] = 1 + (record.attempts || 0) if record.respond_to?(:attempts)
|
|
52
|
-
attrs[:enqueued_at] = (record.enqueued_at || now) if record.respond_to?(:enqueued_at)
|
|
53
|
-
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
|
54
|
-
|
|
40
|
+
attrs = build_publish_attrs(record, subject, envelope)
|
|
55
41
|
ModelUtils.assign_known_attrs(record, attrs)
|
|
56
42
|
record.save!
|
|
57
43
|
end
|
|
58
44
|
end
|
|
59
45
|
|
|
60
|
-
def
|
|
46
|
+
def record_publish_success(record)
|
|
61
47
|
ActiveRecord::Base.transaction do
|
|
62
48
|
now = Time.now.utc
|
|
63
49
|
attrs = { status: 'sent' }
|
|
@@ -68,7 +54,7 @@ module JetstreamBridge
|
|
|
68
54
|
end
|
|
69
55
|
end
|
|
70
56
|
|
|
71
|
-
def
|
|
57
|
+
def record_publish_failure(record, message)
|
|
72
58
|
ActiveRecord::Base.transaction do
|
|
73
59
|
now = Time.now.utc
|
|
74
60
|
attrs = { status: 'failed', last_error: message }
|
|
@@ -78,13 +64,42 @@ module JetstreamBridge
|
|
|
78
64
|
end
|
|
79
65
|
end
|
|
80
66
|
|
|
81
|
-
def
|
|
67
|
+
def record_publish_exception(record, error)
|
|
82
68
|
return unless record
|
|
83
69
|
|
|
84
|
-
|
|
70
|
+
record_publish_failure(record, "#{error.class}: #{error.message}")
|
|
85
71
|
rescue StandardError => e
|
|
86
72
|
Logging.warn("Failed to persist outbox failure: #{e.class}: #{e.message}",
|
|
87
73
|
tag: 'JetstreamBridge::Publisher')
|
|
88
74
|
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def build_publish_attrs(record, subject, envelope)
|
|
79
|
+
now = Time.now.utc
|
|
80
|
+
event_id = envelope['event_id'].to_s
|
|
81
|
+
|
|
82
|
+
attrs = {
|
|
83
|
+
event_id: event_id,
|
|
84
|
+
subject: subject,
|
|
85
|
+
payload: ModelUtils.json_dump(envelope),
|
|
86
|
+
headers: ModelUtils.json_dump({ 'nats-msg-id' => event_id }),
|
|
87
|
+
status: 'publishing',
|
|
88
|
+
last_error: nil,
|
|
89
|
+
resource_type: envelope['resource_type'],
|
|
90
|
+
resource_id: envelope['resource_id'],
|
|
91
|
+
event_type: envelope['type'] || envelope['event_type']
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assign_optional_publish_attrs(record, attrs, now)
|
|
95
|
+
attrs
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def assign_optional_publish_attrs(record, attrs, now)
|
|
99
|
+
attrs[:destination_app] = JetstreamBridge.config.destination_app if record.respond_to?(:destination_app=)
|
|
100
|
+
attrs[:attempts] = 1 + (record.attempts || 0) if record.respond_to?(:attempts)
|
|
101
|
+
attrs[:enqueued_at] = (record.enqueued_at || now) if record.respond_to?(:enqueued_at)
|
|
102
|
+
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
|
103
|
+
end
|
|
89
104
|
end
|
|
90
105
|
end
|
|
@@ -252,17 +252,17 @@ module JetstreamBridge
|
|
|
252
252
|
)
|
|
253
253
|
end
|
|
254
254
|
|
|
255
|
-
repo.
|
|
255
|
+
repo.record_publish_attempt(record, subject, envelope)
|
|
256
256
|
|
|
257
257
|
result = with_retries { publish_to_nats(subject, envelope) }
|
|
258
258
|
if result.success?
|
|
259
|
-
repo.
|
|
259
|
+
repo.record_publish_success(record)
|
|
260
260
|
else
|
|
261
|
-
repo.
|
|
261
|
+
repo.record_publish_failure(record, result.error&.message || 'Publish failed')
|
|
262
262
|
end
|
|
263
263
|
result
|
|
264
264
|
rescue StandardError => e
|
|
265
|
-
repo.
|
|
265
|
+
repo.record_publish_exception(record, e) if defined?(repo) && defined?(record)
|
|
266
266
|
Models::PublishResult.new(
|
|
267
267
|
success: false,
|
|
268
268
|
event_id: envelope['event_id'],
|
|
@@ -113,12 +113,12 @@ namespace :jetstream_bridge do
|
|
|
113
113
|
|
|
114
114
|
begin
|
|
115
115
|
provision_enabled = JetstreamBridge.config.auto_provision
|
|
116
|
-
jts = JetstreamBridge.
|
|
116
|
+
jts = JetstreamBridge.connect_and_provision!
|
|
117
117
|
|
|
118
118
|
if provision_enabled
|
|
119
119
|
puts '✓ Successfully connected to NATS'
|
|
120
120
|
puts '✓ JetStream is available'
|
|
121
|
-
puts '✓ Stream topology
|
|
121
|
+
puts '✓ Stream topology provisioned'
|
|
122
122
|
|
|
123
123
|
# Check if we can get account info
|
|
124
124
|
info = jts.account_info
|
|
@@ -174,7 +174,12 @@ module JetstreamBridge
|
|
|
174
174
|
|
|
175
175
|
# Only include mutable fields on update (subjects, storage). Never retention.
|
|
176
176
|
def apply_update(jts, name, subjects, storage: nil)
|
|
177
|
-
|
|
177
|
+
# Fetch existing stream config to preserve retention
|
|
178
|
+
info = jts.stream_info(name)
|
|
179
|
+
config_data = info.config
|
|
180
|
+
existing_retention = config_data.respond_to?(:retention) ? config_data.retention : config_data[:retention]
|
|
181
|
+
|
|
182
|
+
params = { name: name, subjects: subjects, retention: existing_retention }
|
|
178
183
|
params[:storage] = storage if storage
|
|
179
184
|
jts.update_stream(**params)
|
|
180
185
|
end
|
data/lib/jetstream_bridge.rb
CHANGED
|
@@ -129,6 +129,7 @@ module JetstreamBridge
|
|
|
129
129
|
def reset!
|
|
130
130
|
@config = nil
|
|
131
131
|
@connection_initialized = false
|
|
132
|
+
Consumer.reset_signal_handlers! if defined?(Consumer)
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
# Initialize the JetStream Bridge connection and topology
|
|
@@ -140,7 +141,7 @@ module JetstreamBridge
|
|
|
140
141
|
return if @connection_initialized
|
|
141
142
|
|
|
142
143
|
config.validate!
|
|
143
|
-
|
|
144
|
+
connect_and_provision!
|
|
144
145
|
@connection_initialized = true
|
|
145
146
|
Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
|
|
146
147
|
end
|
|
@@ -197,10 +198,10 @@ module JetstreamBridge
|
|
|
197
198
|
config.use_dlq
|
|
198
199
|
end
|
|
199
200
|
|
|
200
|
-
# Establishes a connection and
|
|
201
|
+
# Establishes a connection and provisions stream topology.
|
|
201
202
|
#
|
|
202
203
|
# @return [Object] JetStream context
|
|
203
|
-
def
|
|
204
|
+
def connect_and_provision!
|
|
204
205
|
config.validate!
|
|
205
206
|
provision = config.auto_provision
|
|
206
207
|
Connection.connect!(verify_js: provision)
|
|
@@ -208,7 +209,7 @@ module JetstreamBridge
|
|
|
208
209
|
raise ConnectionNotEstablishedError, 'JetStream connection not available' unless jts
|
|
209
210
|
|
|
210
211
|
if provision
|
|
211
|
-
Provisioner.new(config: config).
|
|
212
|
+
Provisioner.new(config: config).provision_stream!(jts: jts)
|
|
212
213
|
else
|
|
213
214
|
Logging.info(
|
|
214
215
|
'auto_provision=false: skipping stream provisioning and JetStream account_info. ' \
|
|
@@ -222,16 +223,11 @@ module JetstreamBridge
|
|
|
222
223
|
|
|
223
224
|
# Provision stream/consumer using management credentials (out of band from runtime).
|
|
224
225
|
#
|
|
225
|
-
# @param
|
|
226
|
+
# @param provision_consumer [Boolean] Whether to create/align the consumer along with the stream.
|
|
226
227
|
# @return [Object] JetStream context
|
|
227
|
-
def provision!(
|
|
228
|
+
def provision!(provision_consumer: true)
|
|
228
229
|
config.validate!
|
|
229
|
-
Provisioner.new(config: config).
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Backwards-compatible alias for the previous method name
|
|
233
|
-
def ensure_topology!
|
|
234
|
-
connect_and_ensure_stream!
|
|
230
|
+
Provisioner.new(config: config).provision!(provision_consumer: provision_consumer)
|
|
235
231
|
end
|
|
236
232
|
|
|
237
233
|
# Active health check for monitoring and readiness probes
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jetstream_bridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 7.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Attara
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -121,6 +121,7 @@ files:
|
|
|
121
121
|
- CHANGELOG.md
|
|
122
122
|
- LICENSE
|
|
123
123
|
- README.md
|
|
124
|
+
- docs/API.md
|
|
124
125
|
- docs/ARCHITECTURE.md
|
|
125
126
|
- docs/GETTING_STARTED.md
|
|
126
127
|
- docs/PRODUCTION.md
|
|
@@ -135,7 +136,10 @@ files:
|
|
|
135
136
|
- lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb
|
|
136
137
|
- lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb
|
|
137
138
|
- lib/jetstream_bridge.rb
|
|
139
|
+
- lib/jetstream_bridge/config_helpers.rb
|
|
140
|
+
- lib/jetstream_bridge/config_helpers/lifecycle.rb
|
|
138
141
|
- lib/jetstream_bridge/consumer/consumer.rb
|
|
142
|
+
- lib/jetstream_bridge/consumer/consumer_state.rb
|
|
139
143
|
- lib/jetstream_bridge/consumer/dlq_publisher.rb
|
|
140
144
|
- lib/jetstream_bridge/consumer/inbox/inbox_message.rb
|
|
141
145
|
- lib/jetstream_bridge/consumer/inbox/inbox_processor.rb
|
|
@@ -150,6 +154,7 @@ files:
|
|
|
150
154
|
- lib/jetstream_bridge/core/config_preset.rb
|
|
151
155
|
- lib/jetstream_bridge/core/connection.rb
|
|
152
156
|
- lib/jetstream_bridge/core/connection_factory.rb
|
|
157
|
+
- lib/jetstream_bridge/core/consumer_mode_resolver.rb
|
|
153
158
|
- lib/jetstream_bridge/core/debug_helper.rb
|
|
154
159
|
- lib/jetstream_bridge/core/duration.rb
|
|
155
160
|
- lib/jetstream_bridge/core/logging.rb
|