bug_bunny 4.5.3 → 4.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: 9432574fbe3a1b19c3f9eb4317cc66a10fc1e0f5b536f13d4fffdba1aa086a3d
4
- data.tar.gz: 779dd27c82ef03139dea34dc6969ef66385793377214149a8f34d416f46e5b79
3
+ metadata.gz: 9c9f75619decb8d892d2bfe8c56424822672b6b6b5f3be85442be1399c38e76a
4
+ data.tar.gz: 7fc1e66a10f46d0a6120dd6164b10b0f1b30d2ff91e8d5daed4358f771f2cc49
5
5
  SHA512:
6
- metadata.gz: 20091c38f5f4b049aa4273e58f6b067a4502a388681048041ac35b341f88331412d36f2d3d3cf4baae218bb0a6a53759629761d9d50d2ca4b3ee048254f5de46
7
- data.tar.gz: df4c691e043949d459f52d77e899f7a91fdad7ba2e9122068f46e973ba3cc670d50b17019167318a1290f73b7b8f9cff28dbd30773a18cb933d8de39f6dac34d
6
+ metadata.gz: fd5f4141f5a7892f84d5b3894535632fe63ce24320637a5896530b025c4f3dc8f002f75c41f7cbec57258bca857d58cd1d531c0abf3d6f2047e049cc6119ae98
7
+ data.tar.gz: c13414d24ee769acb483fe4a9ee1f9aa2073ea6bf8bea81e1ac290499ad45cd85471ffd022a304bb6da9cc56e737f868bc441870a1b084b05a60892c6f3b6366
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.6.0] - 2026-03-31
4
+
5
+ ### ✨ New Features
6
+ * **Consumer Middleware Stack:** Se introdujo `BugBunny::ConsumerMiddleware::Stack`, un pipeline de middlewares que se ejecuta antes de que la gema procese cada mensaje AMQP (antes del primer log `consumer.message_received`). Es el punto de extensión oficial para tracing distribuido, autenticación y auditoría a nivel de consumer.
7
+ * Clase base `BugBunny::ConsumerMiddleware::Base` con interfaz `call(delivery_info, properties, body)`.
8
+ * Acceso directo via `BugBunny.consumer_middlewares.use MyMiddleware`.
9
+ * Soporte de **auto-registro transparente**: gemas externas como `exis_ray` pueden registrarse al ser requeridas sin que el usuario toque el bloque `configure`.
10
+ * Orden de ejecución FIFO. Sin middlewares registrados, el overhead es cero.
11
+ * **RPC Trace Propagation (bidireccional):** BugBunny ahora propaga trace context en ambas direcciones del ciclo RPC:
12
+ * `config.rpc_reply_headers` (Proc) — callback invocado en el consumer justo antes del `basic_publish` del reply. Retorna headers AMQP a inyectar en la respuesta (ej: `X-Amzn-Trace-Id` actualizado con el span del consumer).
13
+ * `config.on_rpc_reply` (Proc) — callback invocado en el thread llamante del publisher tras recibir el reply, con los headers AMQP de la respuesta. Permite hidratar trace context en el publisher sin exponer los headers en la interfaz pública del método `rpc`.
14
+ * Ejemplo consumer: `config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => ExisRay::Tracer.generate_trace_header } }`
15
+ * Ejemplo publisher: `config.on_rpc_reply = ->(headers) { ExisRay::Tracer.hydrate(headers['X-Amzn-Trace-Id']) }` Retorna un Hash de headers AMQP que se inyectan en la respuesta, permitiendo propagar trace context generado por el consumer (ej: `X-Amzn-Trace-Id` actualizado). Cero overhead cuando no está configurado.
16
+ * Ejemplo: `config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => ExisRay::Tracer.generate_trace_header } }`
17
+ * **Observability — Hash quoting:** `safe_log` ahora aplica la misma regla de quoting a valores `Hash` que a `String`: si el JSON contiene espacios, se inspecciona; si no, se emite sin comillas, facilitando el parseo automático en motores de logs.
18
+
3
19
  ## [4.5.3] - 2026-03-30
4
20
 
5
21
  ### 🐛 Bug Fixes
data/README.md CHANGED
@@ -206,6 +206,59 @@ end
206
206
 
207
207
  ---
208
208
 
209
+ ## 🔗 Consumer Middleware Stack
210
+
211
+ BugBunny expone un middleware stack que se ejecuta **antes** de que la gema procese cada mensaje entrante (antes del primer log `consumer.message_received`). Es el punto ideal para hidratar contexto de tracing distribuido, autenticación, auditoría, etc.
212
+
213
+ ### Implementar un middleware
214
+
215
+ ```ruby
216
+ class MyMiddleware < BugBunny::ConsumerMiddleware::Base
217
+ def call(delivery_info, properties, body)
218
+ # lógica pre-procesamiento
219
+ # properties.headers contiene todos los headers AMQP custom
220
+ @app.call(delivery_info, properties, body)
221
+ # lógica post-procesamiento (opcional)
222
+ end
223
+ end
224
+ ```
225
+
226
+ ### Registrar un middleware
227
+
228
+ ```ruby
229
+ BugBunny.configure do |config|
230
+ # ...
231
+ end
232
+
233
+ BugBunny.consumer_middlewares.use MyMiddleware
234
+ ```
235
+
236
+ ### Auto-registro desde una gema externa
237
+
238
+ Las gemas de integración pueden registrarse automáticamente al ser requeridas, sin que el usuario tenga que tocar el bloque `configure`:
239
+
240
+ ```ruby
241
+ # lib/exis_ray/bug_bunny/consumer_tracing.rb
242
+ require 'exis_ray/bug_bunny/consumer_tracing_middleware'
243
+ BugBunny.consumer_middlewares.use ExisRay::BugBunny::ConsumerTracingMiddleware
244
+
245
+ # El usuario solo necesita:
246
+ # require 'exis_ray/bug_bunny/consumer_tracing'
247
+ ```
248
+
249
+ ### Datos disponibles en el middleware
250
+
251
+ | Argumento | Tipo | Contenido |
252
+ |---|---|---|
253
+ | `delivery_info` | `Bunny::DeliveryInfo` | `routing_key`, `exchange`, `delivery_tag` |
254
+ | `properties` | `Bunny::MessageProperties` | `headers` (headers AMQP custom), `correlation_id`, `reply_to`, `content_type` |
255
+ | `body` | `String` | Payload crudo del mensaje |
256
+
257
+ > **Orden de ejecución:** FIFO — el primero en registrarse es el primero en ejecutarse.
258
+ > `Middleware A → Middleware B → process_message`
259
+
260
+ ---
261
+
209
262
  ## 🔎 Observabilidad y Tracing
210
263
 
211
264
  BugBunny implementa Distributed Tracing nativo y sigue los estándares de observabilidad de **ExisRay** para logs estructurados.
@@ -96,6 +96,23 @@ module BugBunny
96
96
  # @example { durable: true, exclusive: false }
97
97
  attr_accessor :queue_options
98
98
 
99
+ # @return [BugBunny::ConsumerMiddleware::Stack] Stack de middlewares ejecutados antes de procesar cada mensaje.
100
+ # Los middlewares se registran con {BugBunny::ConsumerMiddleware::Stack#use}.
101
+ attr_reader :consumer_middlewares
102
+
103
+ # @return [Proc, nil] Callback invocado justo antes del `basic_publish` del reply RPC.
104
+ # Debe retornar un Hash de headers AMQP a inyectar en la respuesta.
105
+ # Ideal para propagar trace context (ej: X-Amzn-Trace-Id) generado por el consumer.
106
+ # @example
107
+ # config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => ExisRay::Tracer.generate_trace_header } }
108
+ attr_accessor :rpc_reply_headers
109
+
110
+ # @return [Proc, nil] Callback invocado en el thread llamante tras recibir el reply RPC,
111
+ # con los headers AMQP de la respuesta. Permite hidratar trace context en el publisher.
112
+ # @example
113
+ # config.on_rpc_reply = ->(headers) { ExisRay::Tracer.hydrate(headers['X-Amzn-Trace-Id']) }
114
+ attr_accessor :on_rpc_reply
115
+
99
116
  # @!endgroup
100
117
 
101
118
  # Inicializa la configuración con valores por defecto seguros.
@@ -135,6 +152,10 @@ module BugBunny
135
152
  # Inicialización de opciones de infraestructura como hashes vacíos para permitir fusiones posteriores.
136
153
  @exchange_options = {}
137
154
  @queue_options = {}
155
+
156
+ @consumer_middlewares = ConsumerMiddleware::Stack.new
157
+ @rpc_reply_headers = nil
158
+ @on_rpc_reply = nil
138
159
  end
139
160
 
140
161
  # Construye la URL de conexión AMQP basada en los atributos configurados.
@@ -87,16 +87,19 @@ module BugBunny
87
87
 
88
88
  q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
89
89
  trace_id = properties.correlation_id
90
-
91
90
  logger = BugBunny.configuration.logger
92
91
 
93
- if logger.respond_to?(:tagged)
94
- logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
95
- elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
96
- Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
97
- else
98
- process_message(delivery_info, properties, body)
99
- end
92
+ core = -> {
93
+ if logger.respond_to?(:tagged)
94
+ logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
95
+ elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
96
+ Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
97
+ else
98
+ process_message(delivery_info, properties, body)
99
+ end
100
+ }
101
+
102
+ BugBunny.configuration.consumer_middlewares.call(delivery_info, properties, body, &core)
100
103
  end
101
104
  rescue StandardError => e
102
105
  attempt += 1
@@ -241,11 +244,13 @@ module BugBunny
241
244
  # @return [void]
242
245
  def reply(payload, reply_to, correlation_id)
243
246
  safe_log(:debug, "consumer.rpc_reply", reply_to: reply_to, correlation_id: correlation_id)
247
+ extra_headers = BugBunny.configuration.rpc_reply_headers&.call || {}
244
248
  session.channel.default_exchange.publish(
245
249
  payload.to_json,
246
250
  routing_key: reply_to,
247
251
  correlation_id: correlation_id,
248
- content_type: 'application/json'
252
+ content_type: 'application/json',
253
+ headers: extra_headers
249
254
  )
250
255
  end
251
256
 
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BugBunny
4
+ # Infraestructura de middleware para el Consumer AMQP.
5
+ #
6
+ # Permite inyectar lógica transversal (tracing, autenticación, logging) en el
7
+ # pipeline de procesamiento de mensajes, antes de que la gema procese el mensaje.
8
+ #
9
+ # Cada middleware recibe `(delivery_info, properties, body)` y debe llamar a
10
+ # `@app.call(delivery_info, properties, body)` para continuar la cadena.
11
+ #
12
+ # @example Registrar un middleware desde una gema externa (auto-registro al hacer require)
13
+ # BugBunny.consumer_middlewares.use MyTracing::ConsumerMiddleware
14
+ #
15
+ # @example Implementar un middleware propio
16
+ # class MyMiddleware < BugBunny::ConsumerMiddleware::Base
17
+ # def call(delivery_info, properties, body)
18
+ # # lógica pre-procesamiento (ej: hidratar contexto de tracing)
19
+ # @app.call(delivery_info, properties, body)
20
+ # # lógica post-procesamiento
21
+ # end
22
+ # end
23
+ module ConsumerMiddleware
24
+ # Clase base para middlewares del Consumer.
25
+ #
26
+ # @abstract Subclasificá e implementá {#call}.
27
+ class Base
28
+ # @param app [#call] El siguiente eslabón en la cadena (otro middleware o el core).
29
+ def initialize(app)
30
+ @app = app
31
+ end
32
+
33
+ # Procesa el mensaje y delega al siguiente eslabón.
34
+ #
35
+ # @param delivery_info [Bunny::DeliveryInfo] Metadatos de entrega AMQP.
36
+ # @param properties [Bunny::MessageProperties] Headers y propiedades AMQP.
37
+ # @param body [String] Payload crudo del mensaje.
38
+ # @return [void]
39
+ def call(delivery_info, properties, body)
40
+ @app.call(delivery_info, properties, body)
41
+ end
42
+ end
43
+
44
+ # Gestiona y ejecuta la cadena de middlewares del Consumer.
45
+ class Stack
46
+ def initialize
47
+ @middlewares = []
48
+ end
49
+
50
+ # Registra un middleware en la cadena.
51
+ #
52
+ # @param middleware_class [Class] Clase que hereda de {Base}.
53
+ # @return [self]
54
+ def use(middleware_class)
55
+ @middlewares << middleware_class
56
+ self
57
+ end
58
+
59
+ # @return [Boolean] `true` si no hay middlewares registrados.
60
+ def empty?
61
+ @middlewares.empty?
62
+ end
63
+
64
+ # Ejecuta la cadena de middlewares envolviendo el bloque core.
65
+ #
66
+ # @param delivery_info [Bunny::DeliveryInfo]
67
+ # @param properties [Bunny::MessageProperties]
68
+ # @param body [String]
69
+ # @yieldreturn [void] El bloque core a ejecutar al final de la cadena.
70
+ # @return [void]
71
+ def call(delivery_info, properties, body, &core)
72
+ terminal = ->(_di, _props, _body) { core.call }
73
+
74
+ chain = @middlewares.reverse.inject(terminal) do |next_step, middleware_class|
75
+ middleware_class.new(next_step)
76
+ end
77
+
78
+ chain.call(delivery_info, properties, body)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -23,7 +23,9 @@ module BugBunny
23
23
 
24
24
  formatted = case val
25
25
  when Numeric then val
26
- when Hash then val.to_json # Genera JSON compacto analizable
26
+ when Hash
27
+ json = val.to_json
28
+ json.include?(" ") ? json.inspect : json
27
29
  when String then val.include?(" ") ? val.inspect : val
28
30
  else val.to_s.include?(" ") ? val.to_s.inspect : val
29
31
  end
@@ -82,13 +82,17 @@ module BugBunny
82
82
  safe_log(:debug, "producer.rpc_waiting", correlation_id: cid, timeout_s: wait_timeout)
83
83
 
84
84
  # Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
85
- response_payload = future.value(wait_timeout)
85
+ result = future.value(wait_timeout)
86
86
 
87
- if response_payload.nil?
87
+ if result.nil?
88
88
  raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]"
89
89
  end
90
90
 
91
- parse_response(response_payload)
91
+ BugBunny.configuration.on_rpc_reply&.call(result[:headers])
92
+
93
+ safe_log(:debug, "producer.rpc_response_received", correlation_id: cid)
94
+
95
+ parse_response(result[:body])
92
96
  ensure
93
97
  # Limpieza vital para evitar fugas de memoria en el mapa
94
98
  @pending_requests.delete(cid)
@@ -153,9 +157,8 @@ module BugBunny
153
157
  # Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
154
158
  @session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
155
159
  cid = props.correlation_id.to_s
156
- safe_log(:debug, "producer.rpc_response_received", correlation_id: cid)
157
160
  if (future = @pending_requests[cid])
158
- future.set(body)
161
+ future.set({ body: body, headers: props.headers || {} })
159
162
  else
160
163
  safe_log(:warn, "producer.rpc_response_orphaned", correlation_id: cid)
161
164
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.5.3"
4
+ VERSION = "4.6.0"
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'bug_bunny/observability'
9
9
  require_relative 'bug_bunny/routing/route_set'
10
10
  require_relative 'bug_bunny/middleware/base'
11
11
  require_relative 'bug_bunny/middleware/stack'
12
+ require_relative 'bug_bunny/consumer_middleware'
12
13
  require_relative 'bug_bunny/middleware/raise_error'
13
14
  require_relative 'bug_bunny/middleware/json_response'
14
15
  require_relative 'bug_bunny/client'
@@ -41,6 +42,12 @@ module BugBunny
41
42
  @routes ||= Routing::RouteSet.new
42
43
  end
43
44
 
45
+ # @return [BugBunny::ConsumerMiddleware::Stack] Stack global de middlewares del Consumer.
46
+ # Atajo para `BugBunny.configuration.consumer_middlewares`.
47
+ def self.consumer_middlewares
48
+ configuration.consumer_middlewares
49
+ end
50
+
44
51
  # Configura la librería BugBunny.
45
52
  # Si no se ha configurado previamente, inicializa una nueva configuración por defecto.
46
53
  #
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.3
4
+ version: 4.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-30 00:00:00.000000000 Z
11
+ date: 2026-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -237,6 +237,7 @@ files:
237
237
  - lib/bug_bunny/client.rb
238
238
  - lib/bug_bunny/configuration.rb
239
239
  - lib/bug_bunny/consumer.rb
240
+ - lib/bug_bunny/consumer_middleware.rb
240
241
  - lib/bug_bunny/controller.rb
241
242
  - lib/bug_bunny/exception.rb
242
243
  - lib/bug_bunny/middleware/base.rb