bug_bunny 3.0.5 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65b3c20095ebcd609f8b077d5b9775491e88d5a79162c9a55538a21380b5cfe7
4
- data.tar.gz: 387dc4fe344456068bc00a600d3ec861fdbf28785b6ea6138ad0461a57cc9041
3
+ metadata.gz: 3b31963a71fce5b70886e19ee276f174d55052ffce7fdcd90b4b662e70ac6080
4
+ data.tar.gz: 851e107eec43ebe45ec3059d2148197e756299fdcf7a5efb5120e0856a2ca3b8
5
5
  SHA512:
6
- metadata.gz: 64e39a7c76216cd27abd2308d1e926922aff6e5f57d15b188308ab583297b706aa6c85cd3e3302ab174f79fc653a07c08ff61b9da87f7661337db1d4f2842db9
7
- data.tar.gz: b2e836747a841b870b27d13cb096484d2c812fb74f6a3ac44316a923686bcff6589ec9bca90086254394bbf365ed6ba6cc3a9b4520f48bd216cfea0928573f57
6
+ metadata.gz: 849447e5f3655f10f3bff047db0dc3ce5bbc2d6190a8db030ffe7b7a1d9619eebb9ac907c30f87e059d5e1c0b1dfce81f80b8f1768346f1fcee7b62f678b0243
7
+ data.tar.gz: bacfee7008aae385333d3b4e9139b0eeb89cdb1332259f19ec34491db71a1cb824004b968f54a699b7b0e35b94eca5ff4c134bb5ff04a7ffa095ab9d533408e3
data/CHANGELOG.md CHANGED
@@ -1,4 +1,30 @@
1
1
  # Changelog
2
+ ## [3.1.0] - 2026-02-18
3
+
4
+ ### 🌟 New Features: Observability & Tracing
5
+ * **Distributed Tracing Stack:** Implemented a native distributed tracing system that ensures full visibility from the Producer to the Consumer/Worker.
6
+ * **Producer:** Messages now automatically carry a `correlation_id`. Added support for custom Middlewares to inject IDs from the application context (e.g., Rails `Current.request_id` or Sidekiq IDs).
7
+ * **Consumer:** Automatically extracts the `correlation_id` from AMQP headers and wraps the entire execution in a **Tagged Logger** block (e.g., `[d41d8cd9...] [API] Processing...`).
8
+ * **Controller:** Introduced `self.log_tags` to allow injecting rich business context into logs (e.g., `[Tenant-123]`) using the native `around_action` hook.
9
+
10
+ ### 🛡 Security
11
+ * **Router Hardening:** Added a strict inheritance check in the `Consumer`.
12
+ * **Prevention:** The router now verifies that the instantiated class inherits from `BugBunny::Controller` before execution.
13
+ * **Impact:** Prevents potential **Remote Code Execution (RCE)** vulnerabilities where an attacker could try to instantiate arbitrary system classes (like `::Kernel`) via the `type` header.
14
+
15
+ ### 🐛 Bug Fixes
16
+ * **RPC Type Consistency:** Fixed a critical issue where RPC responses were ignored if the `correlation_id` was an Integer.
17
+ * **Fix:** The Producer now strictly normalizes all correlation IDs to Strings (`.to_s`) during both storage (pending requests) and retrieval (reply listener), ensuring reliable matching regardless of the ID format.
18
+
19
+ ## [3.0.6] - 2026-02-17
20
+
21
+ ### ♻️ Refactor & Standards
22
+ * **Architectural Cleanup:** Removed the `BugBunny::Rabbit` intermediate class. Connection management logic (`disconnect`) has been moved directly to the main `BugBunny` module for simplicity.
23
+ * **Rails Standardization:** Renamed `lib/bug_bunny/config.rb` to `lib/bug_bunny/configuration.rb` and the class from `Config` to `Configuration`. This ensures full compliance with Zeitwerk autoloading standards.
24
+
25
+ ### 🛡 Stability
26
+ * **Fork Safety:** Enhanced `Railtie` to robustly handle process forking. Added support for `ActiveSupport::ForkTracker` (Rails 7.1+) and guarded Puma event hooks to prevent `NoMethodError` on newer Puma versions.
27
+
2
28
  ## [3.0.5] - 2026-02-17
3
29
 
4
30
  ### 🐛 Bug Fixes
data/README.md CHANGED
@@ -54,12 +54,24 @@ BugBunny.configure do |config|
54
54
 
55
55
  # --- Logging (Niveles recomendados) ---
56
56
  # Logger de BugBunny: Muestra tus requests (INFO)
57
- config.logger = Logger.new(STDOUT)
58
- config.logger.level = Logger::INFO
57
+ rails_logger = Rails.logger
58
+
59
+ if defined?(ActiveSupport::TaggedLogging) && !rails_logger.respond_to?(:tagged)
60
+ config.logger = ActiveSupport::TaggedLogging.new(rails_logger)
61
+ else
62
+ config.logger = rails_logger
63
+ end
59
64
 
60
65
  # Logger de Bunny (Driver): Silencia el ruido de bajo nivel (WARN)
61
- config.bunny_logger = Logger.new(STDOUT)
66
+ if defined?(ActiveSupport::TaggedLogging) && !rails_logger.respond_to?(:tagged)
67
+ config.bunny_logger = ActiveSupport::TaggedLogging.new(rails_logger)
68
+ else
69
+ config.bunny_logger = rails_logger
70
+ end
62
71
  config.bunny_logger.level = Logger::WARN
72
+
73
+ # Controller Namaspeace
74
+ config.controller_namespace = 'MyApp::AsyncHandlers' # Default: 'Rabbit::Controllers'
63
75
  end
64
76
  ```
65
77
 
@@ -149,6 +161,80 @@ svc.save
149
161
  # Log: [BugBunny] [POST] '/services' | Routing Key: 'urgent'
150
162
  ```
151
163
 
164
+ ### Soporte de Parámetros Anidados (Nested Queries)
165
+ En la versión 3.0.3 arreglaste la serialización usando `Rack::Utils`. Esto es una "feature" poderosa que permite filtrar por hashes complejos, algo muy común en APIs modernas.
166
+
167
+ **Sugerencia:** Agregar un ejemplo en la sección **CRUD RESTful > LEER (GET)**:
168
+
169
+ ```ruby
170
+ # --- LEER CON FILTROS AVANZADOS ---
171
+ # Soporta hashes anidados (gracias a Rack::Utils)
172
+ # Envia: GET services?q[status]=active&q[tags][]=web
173
+ Manager::Service.where(q: { status: 'active', tags: ['web'] })
174
+ ```
175
+
176
+ ### 🔌 Manipulación de Headers (Middleware)
177
+
178
+ BugBunny permite interceptar y modificar las peticiones antes de que se envíen a RabbitMQ utilizando `client_middleware`. Esto es ideal para inyectar trazas, autenticación o metadatos de contexto.
179
+
180
+ Existen 3 formas principales de usarlo:
181
+
182
+ #### 1. Definición Inline (Rápida)
183
+ Ideal para inyectar headers estáticos específicos de un recurso.
184
+ ```ruby
185
+ class Payment < BugBunny::Resource
186
+ client_middleware do |stack|
187
+ stack.use(Class.new(BugBunny::Middleware::Base) do
188
+ def on_request(env)
189
+ env.headers['X-Service-Version'] = 'v2'
190
+ env.headers['Content-Type'] = 'application/json'
191
+ end
192
+ end)
193
+ end
194
+ end
195
+ ```
196
+
197
+ #### 2. Clase Reutilizable (Recomendada)
198
+ Si tienes lógica compartida (ej: Autenticación), define una clase y úsala en múltiples recursos.
199
+
200
+ ```ruby
201
+ # app/middleware/auth_middleware.rb
202
+ class AuthMiddleware < BugBunny::Middleware::Base
203
+ def on_request(env)
204
+ env.headers['Authorization'] = "Bearer #{ENV['API_KEY']}"
205
+ end
206
+ end
207
+
208
+ # app/models/user.rb
209
+ class User < BugBunny::Resource
210
+ client_middleware do |stack|
211
+ stack.use AuthMiddleware
212
+ end
213
+ end
214
+ ```
215
+
216
+ #### 3. Contexto Dinámico (Pro)
217
+ Permite inyectar valores que cambian en cada petición (como el Usuario actual o Tenant), leyendo de variables globales thread-safe (como CurrentAttributes en Rails).
218
+
219
+ ```ruby
220
+ # Middleware que lee el Tenant actual
221
+ # app/middleware/tenant_middleware.rb
222
+ class TenantMiddleware < BugBunny::Middleware::Base
223
+ def on_request(env)
224
+ # Ejemplo usando Rails CurrentAttributes
225
+ if Current.tenant_id
226
+ env.headers['X-Tenant-ID'] = Current.tenant_id
227
+ end
228
+ end
229
+ end
230
+
231
+ class Order < BugBunny::Resource
232
+ client_middleware do |stack|
233
+ stack.use TenantMiddleware
234
+ end
235
+ end
236
+ ```
237
+
152
238
  ---
153
239
 
154
240
  ## 📡 Modo Servidor (Worker & Router)
@@ -217,7 +303,21 @@ class ApplicationController < BugBunny::Controller
217
303
  end
218
304
  ```
219
305
 
220
- ### 3. Tabla de Ruteo (Convención)
306
+ ### 3. Namespace de Controladores (Opcional)
307
+
308
+ Por defecto, BugBunny busca los controladores dentro del módulo `Rabbit::Controllers`. Esto implica que tus archivos deben estar en `app/rabbit/controllers/`.
309
+
310
+ Si prefieres organizar tus consumidores en otro lugar (ej: dentro de un dominio específico o carpeta existente), puedes cambiar el namespace.
311
+
312
+ **Configuración:**
313
+ ```ruby
314
+ # config/initializers/bug_bunny.rb
315
+ BugBunny.configure do |config|
316
+ config.controller_namespace = 'Billing::Events'
317
+ end
318
+ ```
319
+
320
+ ### 4. Tabla de Ruteo (Convención)
221
321
 
222
322
  El Router infiere la acción automáticamente:
223
323
 
@@ -230,6 +330,116 @@ El Router infiere la acción automáticamente:
230
330
  | `DELETE` | `services/12` | `ServicesController` | `destroy` |
231
331
  | `POST` | `services/12/restart` | `ServicesController` | `restart` (Custom) |
232
332
 
333
+ ### 🔎 Observabilidad y Logging
334
+
335
+ BugBunny implementa un sistema de **Tracing Distribuido** nativo. Esto permite rastrear una petición desde que se origina en tu aplicación (Producer) hasta que es procesada por el worker (Consumer), manteniendo el mismo ID de traza (`correlation_id`) en todos los logs.
336
+
337
+ #### 1. Productor: Inyectar el Trace ID
338
+
339
+ Para asegurar que los mensajes salgan de tu aplicación con el ID de traza correcto (por ejemplo, el `X-Request-Id` de Rails, Sidekiq o tu propio `Current.request_id`), debes inyectarlo antes de publicar el mensaje.
340
+
341
+ La forma recomendada es crear un Middleware y registrarlo globalmente.
342
+
343
+ **A. Crear el Middleware**
344
+
345
+ ```ruby
346
+ # app/middleware/correlation_injector.rb
347
+ class CorrelationInjector < BugBunny::Middleware::Base
348
+ def on_request(env)
349
+ # Ejemplo: Si usas Rails CurrentAttributes o similar
350
+ if defined?(Current) && Current.request_id
351
+ env.correlation_id = Current.request_id
352
+ end
353
+ end
354
+ end
355
+ ```
356
+
357
+ **B. Registrar el Middleware (Initializer)**
358
+
359
+ ```ruby
360
+ # config/initializers/bug_bunny.rb
361
+ require 'bug_bunny'
362
+ require_relative '../../app/middleware/correlation_injector'
363
+
364
+ # Módulo para interceptar la inicialización de cualquier cliente
365
+ module BugBunnyGlobalMiddleware
366
+ def initialize(pool:)
367
+ super
368
+ @stack.use CorrelationInjector
369
+ end
370
+ end
371
+
372
+ # Aplicamos el parche para que afecte a Resources y Clientes manuales
373
+ BugBunny::Client.prepend(BugBunnyGlobalMiddleware)
374
+ ```
375
+
376
+ ---
377
+
378
+ #### 2. Consumidor: Logging Automático
379
+
380
+ El consumidor de BugBunny está diseñado para garantizar la trazabilidad "out-of-the-box".
381
+
382
+ ##### A. Comportamiento por Defecto
383
+ Al recibir un mensaje, el Consumidor realiza automáticamente los siguientes pasos:
384
+ 1. Extrae el `correlation_id` de las propiedades AMQP (o genera un UUID si no existe).
385
+ 2. Envuelve todo el procesamiento en un bloque de log etiquetado (`tagged logging`).
386
+ 3. Pasa el ID al Controlador.
387
+
388
+ **No necesitas configurar nada.** Tus logs se verán así automáticamente:
389
+
390
+ ```text
391
+ [d41d8cd9-8f00...] [Consumer] Listening on queue...
392
+ [d41d8cd9-8f00...] [API] Procesando usuario 123...
393
+ ```
394
+
395
+ ##### B. Configuración Global (Initializer)
396
+ Si deseas agregar tags estáticos que aparezcan en **todos** los mensajes procesados por este worker (como el nombre del servicio, versión o entorno), agrégalos a `config.log_tags`.
397
+
398
+ > **Nota:** No agregues `:uuid` aquí, ya que el Consumidor lo agrega automáticamente.
399
+
400
+ ```ruby
401
+ BugBunny.configure do |config|
402
+ # ... configuración de conexión ...
403
+
404
+ # Tags globales adicionales
405
+ config.log_tags = [
406
+ 'WORKER',
407
+ ->(_) { ENV['APP_VERSION'] }
408
+ ]
409
+ end
410
+ ```
411
+
412
+ **Resultado en Log:**
413
+ ```text
414
+ [d41d8cd9...] [WORKER] [v1.0.2] [API] Procesando mensaje...
415
+ ```
416
+
417
+ ##### C. Configuración por Controlador (Contexto Rico)
418
+ Para agregar información específica del mensaje o lógica de negocio (como IDs de inquilinos, usuario actual, o headers específicos), utiliza `self.log_tags` en tus controladores.
419
+
420
+ Esto aprovecha el `around_action` nativo de la gema para inyectar contexto.
421
+
422
+ ```ruby
423
+ # app/rabbit/controllers/application_controller.rb
424
+ module Rabbit
425
+ module Controllers
426
+ class ApplicationController < BugBunny::Controller
427
+ # Define tags dinámicos basados en el mensaje actual
428
+ self.log_tags = [
429
+ ->(c) { c.params[:tenant_id] }, # Tag del Tenant (si viene en el body)
430
+ ->(c) { c.headers['X-Source'] } # Tag del origen
431
+ ]
432
+ end
433
+ end
434
+ end
435
+ ```
436
+
437
+ **Resultado Final en Log:**
438
+ (UUID Automático + Tag Global + Tag de Controlador)
439
+ ```text
440
+ [d41d8cd9...] [WORKER] [Tenant-55] [Console] Creando usuario...
441
+ ```
442
+
233
443
  ---
234
444
 
235
445
  ## 🔌 Modo Publisher (Cliente Manual)
@@ -256,6 +466,20 @@ client.publish('audit/events',
256
466
  )
257
467
  ```
258
468
 
469
+ ### ⚠️ Consideraciones sobre RPC (Direct Reply-To)
470
+
471
+ BugBunny utiliza el mecanismo nativo `amq.rabbitmq.reply-to` para las peticiones RPC. Esto maximiza el rendimiento eliminando la necesidad de crear colas temporales por cada petición.
472
+
473
+ **Trade-off:**
474
+ Al usar este mecanismo, las respuestas son efímeras. Si el proceso Cliente (tu aplicación Rails/Sidekiq) se reinicia abruptamente justo después de enviar la petición pero milisegundos antes de procesar la respuesta, **esa respuesta se perderá**.
475
+
476
+ **Recomendación:**
477
+ Diseña tus acciones de Controlador RPC (`POST`, `PUT`) para que sean **idempotentes**.
478
+ * *Mal diseño:* "Crear pago" (si se reintenta, cobra doble).
479
+ * *Buen diseño:* "Crear pago con ID X" (si se reintenta y ya existe, devuelve el recibo existente).
480
+
481
+ Esto permite que, ante un `BugBunny::RequestTimeout` por caída del cliente, puedas reintentar la operación de forma segura.
482
+
259
483
  ---
260
484
 
261
485
  ## 🏗 Arquitectura REST-over-AMQP
data/Rakefile CHANGED
@@ -1,8 +1,12 @@
1
- # frozen_string_literal: true
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
2
3
 
3
- require "bundler/gem_tasks"
4
- require "rubocop/rake_task"
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ t.verbose = true
9
+ t.warning = false
10
+ end
5
11
 
6
- RuboCop::RakeTask.new
7
-
8
- task default: :rubocop
12
+ task default: :test
data/bin_worker.rb CHANGED
@@ -1,20 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # bin_worker.rb
2
- require_relative 'test_helper'
3
- require_relative 'test_controller'
4
+ require 'bundler/setup'
5
+ require 'bug_bunny'
6
+ require_relative 'test_controller' # Cargamos los controladores
7
+
8
+ puts '🐰 WORKER INICIADO (Exchange: Topic)...'
4
9
 
5
- puts "🐰 WORKER INICIADO (Exchange: Topic)..."
10
+ # Configuración básica
11
+ BugBunny.configure do |config|
12
+ config.logger = Logger.new($stdout)
13
+ config.logger.level = Logger::DEBUG
14
+ end
6
15
 
7
- # Creamos la conexión (o usamos una del pool si quisieras)
16
+ # Iniciar el Consumidor
17
+ # Escucha en la cola 'bug_bunny_queue', atada al exchange 'test_exchange' con routing key '#' (todo)
8
18
  connection = BugBunny.create_connection
9
19
 
10
- # Usamos el método de clase directo.
11
- # Al no pasar 'block: false', esto bloqueará la ejecución aquí mismo eternamente.
12
20
  BugBunny::Consumer.subscribe(
13
21
  connection: connection,
14
- queue_name: 'test_users_queue',
15
22
  exchange_name: 'test_exchange',
16
23
  exchange_type: 'topic',
17
- routing_key: 'test_user.#'
24
+ queue_name: 'bug_bunny_test_queue',
25
+ routing_key: '#' # Wildcard para recibir todo en este test
18
26
  )
19
-
20
- # ¡Ya no necesitas el loop! El subscribe mantiene vivo el proceso.
@@ -1,19 +1,17 @@
1
- # lib/bug_bunny/config.rb
1
+ # frozen_string_literal: true
2
+
2
3
  require 'logger'
3
4
 
4
5
  module BugBunny
5
6
  # Clase de configuración global para la gema BugBunny.
6
7
  # Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ.
7
- #
8
- # @example Configuración típica
9
- # BugBunny.configure do |config|
10
- # config.host = 'rabbit.local'
11
- # config.rpc_timeout = 5
12
- # end
13
- class Config
8
+ class Configuration
14
9
  # @return [String] Host o IP del servidor RabbitMQ (ej: 'localhost').
15
10
  attr_accessor :host
16
11
 
12
+ # @return [Integer] Puerto del servidor RabbitMQ (default: 5672).
13
+ attr_accessor :port
14
+
17
15
  # @return [String] Usuario para la autenticación (default: 'guest').
18
16
  attr_accessor :username
19
17
 
@@ -26,48 +24,57 @@ module BugBunny
26
24
  # @return [Logger] Instancia del logger para depuración (default: Logger a STDOUT).
27
25
  attr_accessor :logger
28
26
 
29
- # @return [Logger] Logger específico para el driver Bunny (Conexión, Heartbeats, Frames).
30
- # Se recomienda nivel WARN para evitar ruido.
27
+ # @return [Logger] Logger específico para el driver Bunny.
31
28
  attr_accessor :bunny_logger
32
29
 
33
- # @return [Boolean] Si `true`, Bunny intentará reconectar automáticamente ante fallos de red (default: true).
30
+ # @return [Boolean] Si `true`, Bunny intentará reconectar automáticamente.
34
31
  attr_accessor :automatically_recover
35
32
 
36
- # @return [Integer] Tiempo en segundos a esperar antes de intentar reconectar (default: 5).
33
+ # @return [Integer] Tiempo en segundos a esperar antes de intentar reconectar.
37
34
  attr_accessor :network_recovery_interval
38
35
 
39
- # @return [Integer] Timeout en segundos para establecer la conexión TCP inicial (default: 10).
36
+ # @return [Integer] Timeout en segundos para establecer la conexión TCP inicial.
40
37
  attr_accessor :connection_timeout
41
38
 
42
- # @return [Integer] Timeout en segundos para leer datos del socket TCP (default: 30).
39
+ # @return [Integer] Timeout en segundos para leer datos del socket TCP.
43
40
  attr_accessor :read_timeout
44
41
 
45
- # @return [Integer] Timeout en segundos para escribir datos en el socket TCP (default: 30).
42
+ # @return [Integer] Timeout en segundos para escribir datos en el socket TCP.
46
43
  attr_accessor :write_timeout
47
44
 
48
- # @return [Integer] Intervalo en segundos para enviar latidos (heartbeats) y mantener la conexión viva (default: 15).
45
+ # @return [Integer] Intervalo en segundos para enviar latidos (heartbeats).
49
46
  attr_accessor :heartbeat
50
47
 
51
- # @return [Integer] Timeout en milisegundos para operaciones de continuación RPC internas de Bunny (default: 15_000).
48
+ # @return [Integer] Timeout en milisegundos para operaciones de continuación RPC internas.
52
49
  attr_accessor :continuation_timeout
53
50
 
54
- # @return [Integer] Cantidad de mensajes que el consumidor pre-cargará antes de procesarlos (QoS) (default: 1).
51
+ # @return [Integer] Cantidad de mensajes que el consumidor pre-cargará (QoS).
55
52
  attr_accessor :channel_prefetch
56
53
 
57
- # @return [Integer] Tiempo máximo en segundos que el cliente esperará una respuesta RPC antes de lanzar {BugBunny::RequestTimeout} (default: 10).
54
+ # @return [Integer] Tiempo máximo en segundos que el cliente esperará una respuesta RPC.
58
55
  attr_accessor :rpc_timeout
59
56
 
60
- # @return [Integer] Intervalo en segundos para verificar que la cola del consumidor sigue existiendo (default: 60).
57
+ # @return [Integer] Intervalo en segundos para verificar la salud de la cola.
61
58
  attr_accessor :health_check_interval
62
59
 
60
+ # @return [String] Namespace base donde se buscarán los controladores (default: 'Rabbit::Controllers').
61
+ attr_accessor :controller_namespace
62
+
63
+ # @return [Array<Symbol, Proc, String>]
64
+ attr_accessor :log_tags
65
+
63
66
  # Inicializa la configuración con valores por defecto seguros.
64
67
  def initialize
65
- # Logger de la Aplicación (BugBunny) -> INFO (Ves tus requests)
66
- @logger = Logger.new(STDOUT)
68
+ @host = '127.0.0.1'
69
+ @port = 5672
70
+ @username = 'guest'
71
+ @password = 'guest'
72
+ @vhost = '/'
73
+
74
+ @logger = Logger.new($stdout)
67
75
  @logger.level = Logger::INFO
68
76
 
69
- # Logger del Driver (Bunny) -> WARN (Oculta el ruido de "handle_frame")
70
- @bunny_logger = Logger.new(STDOUT)
77
+ @bunny_logger = Logger.new($stdout)
71
78
  @bunny_logger.level = Logger::WARN
72
79
  @automatically_recover = true
73
80
  @network_recovery_interval = 5
@@ -79,14 +86,16 @@ module BugBunny
79
86
  @channel_prefetch = 1
80
87
  @rpc_timeout = 10
81
88
  @health_check_interval = 60
89
+
90
+ # Configuración por defecto para mantener compatibilidad
91
+ @controller_namespace = 'Rabbit::Controllers'
92
+
93
+ @log_tags = [:uuid]
82
94
  end
83
95
 
84
96
  # Construye la URL de conexión AMQP basada en los atributos configurados.
85
- # Útil para logs o herramientas externas.
86
- #
87
- # @return [String] La cadena de conexión en formato `amqp://user:pass@host/vhost`.
88
97
  def url
89
- "amqp://#{username}:#{password}@#{host}/#{vhost}"
98
+ "amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"
90
99
  end
91
100
  end
92
101
  end
@@ -65,7 +65,17 @@ module BugBunny
65
65
  start_health_check(queue_name)
66
66
 
67
67
  q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
68
- process_message(delivery_info, properties, body)
68
+ trace_id = properties.correlation_id
69
+
70
+ logger = BugBunny.configuration.logger
71
+
72
+ if logger.respond_to?(:tagged)
73
+ logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
74
+ elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
75
+ Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
76
+ else
77
+ process_message(delivery_info, properties, body)
78
+ end
69
79
  end
70
80
  rescue StandardError => e
71
81
  BugBunny.configuration.logger.error("[Consumer] Connection Error: #{e.message}. Retrying...")
@@ -84,20 +94,28 @@ module BugBunny
84
94
  # @param body [String] El payload crudo del mensaje.
85
95
  # @return [void]
86
96
  def process_message(delivery_info, properties, body)
87
- if properties.type.nil? || properties.type.empty?
88
- BugBunny.configuration.logger.error("[Consumer] Missing 'type' header. Message rejected.")
97
+ BugBunny.configuration.logger.debug("delivery_info: #{delivery_info}, properties: #{properties}, body: #{body}")
98
+ # 1. Recuperación Robusta del Path (Ruta)
99
+ path = properties.type
100
+ if path.nil? || path.empty?
101
+ path = properties.headers ? properties.headers['path'] : nil
102
+ end
103
+
104
+ if path.nil? || path.empty?
105
+ BugBunny.configuration.logger.error("[Consumer] Missing 'type' or 'path' header. Message rejected.")
89
106
  session.channel.reject(delivery_info.delivery_tag, false)
90
107
  return
91
108
  end
92
109
 
93
- # 1. Determinar Verbo HTTP (Default: GET)
94
- http_method = properties.headers ? (properties.headers['x-http-method'] || 'GET') : 'GET'
110
+ # 2. Recuperación Robusta del Verbo HTTP
111
+ headers_hash = properties.headers || {}
112
+ http_method = headers_hash['x-http-method'] || headers_hash['method'] || 'GET'
95
113
 
96
- # 2. Router: Inferencia de Controlador y Acción
97
- route_info = router_dispatch(http_method, properties.type)
114
+ # 3. Router: Inferencia de Controlador y Acción
115
+ route_info = router_dispatch(http_method, path)
98
116
 
99
- headers = {
100
- type: properties.type,
117
+ request_metadata = {
118
+ type: path,
101
119
  http_method: http_method,
102
120
  controller: route_info[:controller],
103
121
  action: route_info[:action],
@@ -106,32 +124,41 @@ module BugBunny
106
124
  content_type: properties.content_type,
107
125
  correlation_id: properties.correlation_id,
108
126
  reply_to: properties.reply_to
109
- }
127
+ }.merge(properties.headers)
128
+
129
+ # 4. Instanciación Dinámica del Controlador
130
+ # Utilizamos el namespace configurado en lugar de hardcodear "Rabbit::Controllers"
131
+ begin
132
+ namespace = BugBunny.configuration.controller_namespace
133
+ controller_name = route_info[:controller].camelize
110
134
 
111
- # 3. Instanciación Dinámica del Controlador
112
- # Ej: "users" -> Rabbit::Controllers::UsersController
113
- controller_class_name = "rabbit/controllers/#{route_info[:controller]}".camelize
114
- controller_class = controller_class_name.constantize
135
+ # Construcción: "Messaging::Handlers" + "::" + "Users"
136
+ controller_class_name = "#{namespace}::#{controller_name}"
115
137
 
116
- # 4. Ejecución del Pipeline (Filtros -> Acción)
117
- response_payload = controller_class.call(headers: headers, body: body)
138
+ controller_class = controller_class_name.constantize
118
139
 
119
- # 5. Respuesta RPC (Si se solicita respuesta)
140
+ unless controller_class < BugBunny::Controller
141
+ raise BugBunny::SecurityError, "Class #{controller_class} is not a valid BugBunny Controller"
142
+ end
143
+ rescue NameError => _e
144
+ BugBunny.configuration.logger.error("[Consumer] Controller not found: #{controller_class_name}")
145
+ handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
146
+ session.channel.reject(delivery_info.delivery_tag, false)
147
+ return
148
+ end
149
+
150
+ # 5. Ejecución del Pipeline (Middleware + Acción)
151
+ response_payload = controller_class.call(headers: request_metadata, body: body)
152
+
153
+ # 6. Respuesta RPC
120
154
  if properties.reply_to
121
155
  reply(response_payload, properties.reply_to, properties.correlation_id)
122
156
  end
123
157
 
124
- # 6. Acknowledge (Confirmación de procesado)
158
+ # 7. Acknowledge
125
159
  session.channel.ack(delivery_info.delivery_tag)
126
160
 
127
- rescue NameError => e
128
- # Error 501/404: El controlador o la acción no existen.
129
- BugBunny.configuration.logger.error("[Consumer] Routing Error: #{e.message}")
130
- handle_fatal_error(properties, 501, "Routing Error", e.message)
131
- session.channel.reject(delivery_info.delivery_tag, false)
132
-
133
161
  rescue StandardError => e
134
- # Error 500: Crash interno de la aplicación.
135
162
  BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
136
163
  handle_fatal_error(properties, 500, "Internal Server Error", e.message)
137
164
  session.channel.reject(delivery_info.delivery_tag, false)
@@ -189,6 +216,7 @@ module BugBunny
189
216
  # @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
190
217
  # @return [void]
191
218
  def reply(payload, reply_to, correlation_id)
219
+ BugBunny.configuration.logger.debug("[Consumer] 📤 Enviando REPLY a: #{reply_to} | ID: #{correlation_id}")
192
220
  session.channel.default_exchange.publish(
193
221
  payload.to_json,
194
222
  routing_key: reply_to,