shared_broker 1.4.0 → 1.6.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: b161a64893a9f15f9bb4568c1b281c2851904bc9e8730e8992d5f6d0847a4de4
4
- data.tar.gz: 6a771b8df2dc371516b2d7f97d6e157ff493141034106c40236f952c6aef0f2e
3
+ metadata.gz: 24d5ac30bc2bef8c5fd99edbcb8e7e986fa8005fde7cfdb39bfb4f3c6010efa8
4
+ data.tar.gz: d66d983238edb492bddf145c48866ce83a55b88bf6806472910bdafdbbd418ec
5
5
  SHA512:
6
- metadata.gz: '06494d11f1575ac5e13353acad3e9896f9ff7e5b7f64b164cecaa39b492aadeb170590e2ec9c9628c80691d08fc521e07e384fa43d2fe4e7874424704e39aedb'
7
- data.tar.gz: 6781b513d75bd232c5c75738a6078aad08d096133e410335ec707ac203548b653af222bbb37ee7594fff40044e8122f8e362b12f68927939a7a19655f021ca16
6
+ metadata.gz: c25d32bc67d7c28427c533b1f3e23b048a7513740f0e8a51e067f5f5d861b761a3189b7d84d06b977400b21bf022c003c50d4515e46cfdc6cee064d36acbdb38
7
+ data.tar.gz: 1ab4165d6750a4f1d28f3ce67c070d58135d10c793b4eac944ba257b8b01f3dcd0665e36018aa0c32b27076439297e6e8ee0b789f5b943fa2820ef9472d2f09f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.6.0] - 2026-06-16
9
+
10
+ ### Added
11
+ - **Encryption Key Rotation & Granularity**: Introduced flexible topic-based encryption keys and multi-version key rotation.
12
+ - `SharedBroker::KeyProvider::Registry` to register multiple keys by key ID and route active keys based on topic glob patterns.
13
+ - `SharedBroker::KeyProvider::Static` for seamless backward compatibility with single global encryption keys.
14
+ - Automatic versioning of encrypted payloads with `_key_id` metadata, enabling decrypting historical messages using rotated keys.
15
+
16
+ ## [1.5.0] - 2026-06-10
17
+
18
+ ### Added
19
+ - Concurrency Control & Adaptive Backpressure.
20
+ - `SharedBroker::Concurrency::Semaphore` pure-Ruby thread-safe semaphore implementation.
21
+ - `SharedBroker::Concurrency::Limiter` to manage dynamic backpressure checks and concurrency limits on message ingestion.
22
+ - Integrated concurrency parameters (`max_concurrency`, `backpressure_check`, `backpressure_backoff`) into `Client#subscribe`.
23
+
8
24
  ## [1.4.0] - 2026-06-10
9
25
 
10
26
  ### Added
data/README.md CHANGED
@@ -160,6 +160,41 @@ HYBRID_BROKER = SharedBroker::Client.new(
160
160
  )
161
161
  ```
162
162
 
163
+ #### E. Encryption Key Rotation & Granularity
164
+
165
+ If your system requires encrypting payloads with different keys based on the topic (e.g., highly sensitive financial data vs. general notifications) or rotating keys without breaking the decryption of historical messages in queues, you can configure a Key Provider Registry:
166
+
167
+ ```ruby
168
+ # 1. Initialize Registry with a map of historical/current keys and active key mappings
169
+ key_registry = SharedBroker::KeyProvider::Registry.new(
170
+ keys: {
171
+ "v1" => "a" * 32, # historical key
172
+ "v2" => "b" * 32, # current general key
173
+ "finance_key_1" => "c" * 32 # current finance key
174
+ },
175
+ active_keys: {
176
+ # Topic-specific key mapping using glob patterns
177
+ "payment.*" => "finance_key_1",
178
+ # Fallback key mapping for all other topics
179
+ "*" => "v2"
180
+ }
181
+ )
182
+
183
+ # 2. Register key provider globally or pass it to Client initialize
184
+ SharedBroker.key_provider = key_registry
185
+
186
+ # Alternatively, pass it directly to the client
187
+ SPOT_BROKER = SharedBroker::Client.new(
188
+ adapter: BROKER_ADAPTER,
189
+ key_provider: key_registry
190
+ )
191
+ ```
192
+
193
+ With a key provider registry configured:
194
+ - **Publishing**: Payloads are encrypted using the active key matching the topic pattern, and a `_key_id` metadata tag is automatically appended to the envelope.
195
+ - **Subscribing**: The gem automatically reads the `_key_id` from the payload envelope and decrypts it using the correct historical key version.
196
+ - **Fallback**: If no `_key_id` is present on a received message (e.g., legacy message), it falls back to the key associated with the topic pattern.
197
+
163
198
  ---
164
199
 
165
200
  ## Usage
@@ -177,14 +212,26 @@ event_data = {
177
212
  SPOT_BROKER.publish("user.created", event_data)
178
213
  ```
179
214
 
180
- ### Subscribing to Events (Consumer with Retry and DLQ)
181
- To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries and backoff rate:
215
+ ### Subscribing to Events (Consumer with Retry, DLQ, Concurrency & Backpressure)
216
+ To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries, backoff rate, concurrency limits, and dynamic backpressure checks:
182
217
 
183
218
  ```ruby
219
+ # Basic subscriber
184
220
  SPOT_BROKER.subscribe("user.created", "my_consumption_queue", max_retries: 3, backoff_base: 2) do |payload|
185
221
  puts "Decrypted event successfully validated & consumed! ID: #{payload[:id]}"
186
222
  # execute your business logic here...
187
223
  end
224
+
225
+ # Advanced subscriber with Concurrency limits and Dynamic Backpressure
226
+ SPOT_BROKER.subscribe(
227
+ "user.created",
228
+ "my_consumption_queue",
229
+ max_concurrency: 5, # Process up to 5 events concurrently
230
+ backpressure_check: -> { db_overloaded? }, # Stop pulling new events if true
231
+ backpressure_backoff: 2.0 # Check health again after 2 seconds
232
+ ) do |payload|
233
+ # business logic running within the concurrency wrapper
234
+ end
188
235
  ```
189
236
 
190
237
  ---
@@ -1,69 +1,98 @@
1
- # frozen_string_literal: true
2
-
3
- require "openssl"
4
- require "base64"
5
- require "json"
6
-
7
- module SharedBroker
8
- module Cipher
9
- class DecryptionError < StandardError; end
10
-
11
- ALGORITHM = "aes-256-gcm"
12
-
13
- def self.encrypt(payload_hash, key)
14
- return payload_hash unless key
15
-
16
- unless payload_hash.is_a?(Hash)
17
- raise ArgumentError, "Expected payload_hash to be a Hash, got #{payload_hash.class} with value #{payload_hash.inspect}"
18
- end
19
-
20
- # We don't want to encrypt correlation ID or special metadata keys if we want the broker to read them,
21
- # but we want to encrypt the actual payload data.
22
- # Let's extract metadata starting with underscore and encrypt the rest.
23
- metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
24
- data_to_encrypt = payload_hash.reject { |k, _| k.to_s.start_with?("_") }
25
-
26
- cipher = OpenSSL::Cipher.new(ALGORITHM)
27
- cipher.encrypt
28
- cipher.key = key
29
- iv = cipher.random_iv
30
-
31
- encrypted_data = cipher.update(data_to_encrypt.to_json) + cipher.final
32
- auth_tag = cipher.auth_tag
33
-
34
- metadata.merge(
35
- _encrypted: true,
36
- _iv: Base64.strict_encode64(iv),
37
- _auth_tag: Base64.strict_encode64(auth_tag),
38
- _data: Base64.strict_encode64(encrypted_data)
39
- )
40
- end
41
-
42
- def self.decrypt(payload_hash, key)
43
- return payload_hash unless payload_hash.is_a?(Hash) && payload_hash[:_encrypted]
44
- return payload_hash unless key
45
-
46
- cipher = OpenSSL::Cipher.new(ALGORITHM)
47
- cipher.decrypt
48
- cipher.key = key
49
- cipher.iv = Base64.strict_decode64(payload_hash[:_iv])
50
- cipher.auth_tag = Base64.strict_decode64(payload_hash[:_auth_tag])
51
-
52
- encrypted_bytes = Base64.strict_decode64(payload_hash[:_data])
53
- decrypted_json = cipher.update(encrypted_bytes) + cipher.final
54
-
55
- decrypted_data = JSON.parse(decrypted_json, symbolize_names: true)
56
-
57
- # Merge back the unencrypted metadata
58
- metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
59
- metadata.delete(:_encrypted)
60
- metadata.delete(:_iv)
61
- metadata.delete(:_auth_tag)
62
- metadata.delete(:_data)
63
-
64
- decrypted_data.merge(metadata)
65
- rescue => e
66
- raise DecryptionError, "Failed to decrypt payload. Error: #{e.message}. Offending payload: #{payload_hash.inspect}"
67
- end
68
- end
69
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+ require "json"
6
+
7
+ module SharedBroker
8
+ module Cipher
9
+ class DecryptionError < StandardError; end
10
+
11
+ ALGORITHM = "aes-256-gcm"
12
+
13
+ def self.encrypt(payload_hash, key_provider_or_key, topic: nil)
14
+ return payload_hash unless key_provider_or_key
15
+
16
+ unless payload_hash.is_a?(Hash)
17
+ raise ArgumentError, "Expected payload_hash to be a Hash, got #{payload_hash.class} with value #{payload_hash.inspect}"
18
+ end
19
+
20
+ key, key_id = resolve_encryption_key(key_provider_or_key, topic)
21
+ return payload_hash unless key
22
+
23
+ metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
24
+ data_to_encrypt = payload_hash.reject { |k, _| k.to_s.start_with?("_") }
25
+
26
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
27
+ cipher.encrypt
28
+ cipher.key = key
29
+ iv = cipher.random_iv
30
+
31
+ encrypted_data = cipher.update(data_to_encrypt.to_json) + cipher.final
32
+ auth_tag = cipher.auth_tag
33
+
34
+ envelope = {
35
+ _encrypted: true,
36
+ _iv: Base64.strict_encode64(iv),
37
+ _auth_tag: Base64.strict_encode64(auth_tag),
38
+ _data: Base64.strict_encode64(encrypted_data)
39
+ }
40
+ envelope[:_key_id] = key_id.to_s if key_id
41
+
42
+ metadata.merge(envelope)
43
+ end
44
+
45
+ def self.decrypt(payload_hash, key_provider_or_key, topic: nil)
46
+ return payload_hash unless payload_hash.is_a?(Hash) && payload_hash[:_encrypted]
47
+ return payload_hash unless key_provider_or_key
48
+
49
+ key = resolve_decryption_key(payload_hash, key_provider_or_key, topic)
50
+ return payload_hash unless key
51
+
52
+ cipher = OpenSSL::Cipher.new(ALGORITHM)
53
+ cipher.decrypt
54
+ cipher.key = key
55
+ cipher.iv = Base64.strict_decode64(payload_hash[:_iv])
56
+ cipher.auth_tag = Base64.strict_decode64(payload_hash[:_auth_tag])
57
+
58
+ encrypted_bytes = Base64.strict_decode64(payload_hash[:_data])
59
+ decrypted_json = cipher.update(encrypted_bytes) + cipher.final
60
+
61
+ decrypted_data = JSON.parse(decrypted_json, symbolize_names: true)
62
+
63
+ metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
64
+ clean_envelope_metadata!(metadata)
65
+
66
+ decrypted_data.merge(metadata)
67
+ rescue => e
68
+ raise DecryptionError, "Failed to decrypt payload. Error: #{e.message}. Offending payload: #{payload_hash.inspect}"
69
+ end
70
+
71
+ private
72
+
73
+ def self.resolve_encryption_key(provider_or_key, topic)
74
+ if provider_or_key.respond_to?(:key_for)
75
+ [provider_or_key.key_for(topic), provider_or_key.active_key_id_for(topic)]
76
+ else
77
+ [provider_or_key, nil]
78
+ end
79
+ end
80
+
81
+ def self.resolve_decryption_key(payload, provider_or_key, topic)
82
+ if provider_or_key.respond_to?(:key_for_id)
83
+ key_id = payload[:_key_id]
84
+ key_id ? provider_or_key.key_for_id(key_id) : provider_or_key.key_for(topic)
85
+ else
86
+ provider_or_key
87
+ end
88
+ end
89
+
90
+ def self.clean_envelope_metadata!(metadata)
91
+ metadata.delete(:_encrypted)
92
+ metadata.delete(:_iv)
93
+ metadata.delete(:_auth_tag)
94
+ metadata.delete(:_data)
95
+ metadata.delete(:_key_id)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "semaphore"
4
+
5
+ module SharedBroker
6
+ module Concurrency
7
+ class Limiter
8
+ def initialize(max_concurrency: nil, backpressure_check: nil, backpressure_backoff: 1.0)
9
+ @backpressure_check = backpressure_check
10
+ @backpressure_backoff = backpressure_backoff
11
+ @semaphore = max_concurrency ? Semaphore.new(max_concurrency) : nil
12
+ end
13
+
14
+ def run
15
+ wait_while_overloaded
16
+ if @semaphore
17
+ @semaphore.synchronize { yield }
18
+ else
19
+ yield
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def wait_while_overloaded
26
+ return unless @backpressure_check
27
+
28
+ while @backpressure_check.call
29
+ sleep(@backpressure_backoff)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+
5
+ module SharedBroker
6
+ module Concurrency
7
+ class Semaphore
8
+ def initialize(limit)
9
+ unless limit.is_a?(Integer) && limit > 0
10
+ raise ArgumentError, "Expected limit to be a positive Integer, got: #{limit.inspect} (class: #{limit.class})"
11
+ end
12
+
13
+ @limit = limit
14
+ @count = 0
15
+ @mutex = Mutex.new
16
+ @cond = ConditionVariable.new
17
+ end
18
+
19
+ def acquire
20
+ @mutex.synchronize do
21
+ while @count >= @limit
22
+ @cond.wait(@mutex)
23
+ end
24
+ @count += 1
25
+ end
26
+ end
27
+
28
+ def release
29
+ @mutex.synchronize do
30
+ @count -= 1
31
+ @cond.signal
32
+ end
33
+ end
34
+
35
+ def synchronize
36
+ acquire
37
+ begin
38
+ yield
39
+ ensure
40
+ release
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedBroker
4
+ module KeyProvider
5
+ class KeyNotFoundError < StandardError; end
6
+
7
+ # Static key provider for backward compatibility
8
+ class Static
9
+ def initialize(key)
10
+ raise ArgumentError, "Expected key to be a 32-byte String, got #{key.inspect}" unless key.is_a?(String)
11
+ @key = key
12
+ end
13
+
14
+ def key_for(_topic)
15
+ @key
16
+ end
17
+
18
+ def key_for_id(_key_id)
19
+ @key
20
+ end
21
+
22
+ def active_key_id_for(_topic)
23
+ nil
24
+ end
25
+ end
26
+
27
+ # Flexible registry for multiple keys and topic-based routing patterns
28
+ class Registry
29
+ def initialize(keys: {}, active_keys: {})
30
+ validate_inputs!(keys, active_keys)
31
+ @keys = keys.transform_keys(&:to_s)
32
+ @active_keys = active_keys.transform_keys(&:to_s)
33
+ end
34
+
35
+ def key_for(topic)
36
+ key_id = active_key_id_for(topic)
37
+ key_for_id(key_id)
38
+ end
39
+
40
+ def key_for_id(key_id)
41
+ return nil if key_id.nil?
42
+
43
+ key = @keys[key_id.to_s]
44
+ return key if key
45
+
46
+ raise KeyNotFoundError, "Key ID #{key_id.inspect} not found in registered keys. Available keys: #{@keys.keys.inspect}"
47
+ end
48
+
49
+ def active_key_id_for(topic)
50
+ topic_str = topic.to_s
51
+ return @active_keys[topic_str] if @active_keys.key?(topic_str)
52
+
53
+ pattern = find_matching_pattern(topic_str)
54
+ return @active_keys[pattern] if pattern
55
+
56
+ fallback_key_id
57
+ end
58
+
59
+ private
60
+
61
+ def validate_inputs!(keys, active_keys)
62
+ unless keys.is_a?(Hash) && active_keys.is_a?(Hash)
63
+ raise ArgumentError, "Expected keys and active_keys to be Hashes, got keys: #{keys.inspect}, active_keys: #{active_keys.inspect}"
64
+ end
65
+ end
66
+
67
+ def find_matching_pattern(topic_str)
68
+ @active_keys.keys.find do |pattern|
69
+ pattern != "*" && File.fnmatch?(pattern, topic_str)
70
+ end
71
+ end
72
+
73
+ def fallback_key_id
74
+ @active_keys["*"] || raise(KeyNotFoundError, "No active key configuration found for fallback '*' pattern")
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharedBroker
4
- VERSION = "1.4.0"
4
+ VERSION = "1.6.0"
5
5
  end
data/lib/shared_broker.rb CHANGED
@@ -8,6 +8,9 @@ require_relative "shared_broker/schema_registry/providers/local"
8
8
  require_relative "shared_broker/schema_registry/providers/http"
9
9
  require_relative "shared_broker/validation"
10
10
  require_relative "shared_broker/cipher"
11
+ require_relative "shared_broker/key_provider"
12
+ require_relative "shared_broker/concurrency/semaphore"
13
+ require_relative "shared_broker/concurrency/limiter"
11
14
  require_relative "shared_broker/middleware_pipeline"
12
15
  require_relative "shared_broker/middlewares/open_telemetry_propagation"
13
16
  require_relative "shared_broker/adapters/base"
@@ -18,27 +21,29 @@ require_relative "shared_broker/adapters/redis"
18
21
 
19
22
  module SharedBroker
20
23
  class << self
21
- attr_accessor :encryption_key
24
+ attr_accessor :encryption_key, :key_provider
22
25
  end
23
26
 
24
27
  # Default key for development/test if not set
25
28
  @encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
29
+ @key_provider = nil
26
30
 
27
31
  class Client
28
32
  attr_reader :circuit_breaker, :middleware_pipeline, :adapters, :routing
29
33
 
30
- def initialize(adapter: nil, adapters: nil, routing: nil, circuit_breaker: nil, middlewares: nil)
34
+ def initialize(adapter: nil, adapters: nil, routing: nil, circuit_breaker: nil, middlewares: nil, key_provider: nil)
31
35
  setup_adapters(adapter: adapter, adapters: adapters, routing: routing)
32
36
  @circuit_breaker = circuit_breaker || CircuitBreaker.new
33
37
  resolved_middlewares = middlewares || [SharedBroker::Middlewares::OpenTelemetryPropagation.new]
34
38
  @middleware_pipeline = MiddlewarePipeline.new(resolved_middlewares)
39
+ @key_provider = key_provider
35
40
  end
36
41
 
37
42
  def publish(topic, message, correlation_id: nil)
38
43
  metadata = { correlation_id: correlation_id, operation: :publish }
39
44
  @middleware_pipeline.execute(topic, message, metadata) do
40
45
  SharedBroker::Validation.validate!(topic, message)
41
- encrypted_msg = SharedBroker::Cipher.encrypt(message, SharedBroker.encryption_key)
46
+ encrypted_msg = SharedBroker::Cipher.encrypt(message, active_key_provider, topic: topic)
42
47
 
43
48
  @circuit_breaker.run do
44
49
  resolve_adapter(topic).publish(topic, encrypted_msg, correlation_id: correlation_id)
@@ -46,20 +51,32 @@ module SharedBroker
46
51
  end
47
52
  end
48
53
 
49
- def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, &block)
50
- resolve_adapter(topic).subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
51
- decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, SharedBroker.encryption_key)
52
- SharedBroker::Validation.validate!(topic, decrypted_msg)
54
+ def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, max_concurrency: nil, backpressure_check: nil, backpressure_backoff: 1.0, &block)
55
+ limiter = SharedBroker::Concurrency::Limiter.new(
56
+ max_concurrency: max_concurrency,
57
+ backpressure_check: backpressure_check,
58
+ backpressure_backoff: backpressure_backoff
59
+ )
53
60
 
54
- metadata = { correlation_id: decrypted_msg[:_correlation_id], operation: :subscribe, queue_name: queue_name }
55
- @middleware_pipeline.execute(topic, decrypted_msg, metadata) do
56
- block.call(decrypted_msg)
61
+ resolve_adapter(topic).subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
62
+ limiter.run do
63
+ decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, active_key_provider, topic: topic)
64
+ SharedBroker::Validation.validate!(topic, decrypted_msg)
65
+
66
+ metadata = { correlation_id: decrypted_msg[:_correlation_id], operation: :subscribe, queue_name: queue_name }
67
+ @middleware_pipeline.execute(topic, decrypted_msg, metadata) do
68
+ block.call(decrypted_msg)
69
+ end
57
70
  end
58
71
  end
59
72
  end
60
73
 
61
74
  private
62
75
 
76
+ def active_key_provider
77
+ @key_provider || SharedBroker.key_provider || SharedBroker::KeyProvider::Static.new(SharedBroker.encryption_key)
78
+ end
79
+
63
80
  def setup_adapters(adapter: nil, adapters: nil, routing: nil)
64
81
  if adapter || (adapters.nil? && routing.nil?)
65
82
  validate_single_adapter!(adapter)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shared_broker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wesley Lima
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-10 00:00:00.000000000 Z
11
+ date: 2026-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -155,6 +155,9 @@ files:
155
155
  - lib/shared_broker/adapters/redis.rb
156
156
  - lib/shared_broker/cipher.rb
157
157
  - lib/shared_broker/circuit_breaker.rb
158
+ - lib/shared_broker/concurrency/limiter.rb
159
+ - lib/shared_broker/concurrency/semaphore.rb
160
+ - lib/shared_broker/key_provider.rb
158
161
  - lib/shared_broker/middleware_pipeline.rb
159
162
  - lib/shared_broker/middlewares/open_telemetry_propagation.rb
160
163
  - lib/shared_broker/schema_registry.rb