bug_bunny 3.0.6 → 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a8a31414070c751adbf073b7588e31d866ed7f1047abfbadad6de2ae75e411e
4
- data.tar.gz: 438a1f9d2a2d3cd6f012f16a5c1349c42f20c87be1cb76212a841c528004bdfd
3
+ metadata.gz: fc4ecbe75edaf8811acfe2929b74fb4aa98cb65c61d74aca4f5a1320b40a94b8
4
+ data.tar.gz: c3ed5b1e5a773ab8d80fbecc1b96d4a04a62dd1f0b1b5414a2275f497fcde9ee
5
5
  SHA512:
6
- metadata.gz: 130ae01d89317ad8ecf2c9bb832d4ce235f6eb23df903e256ea47a5669bc2b300239b41beabcbb6a2e8dc44a674ac3eaa90cb8fab0bc44037c377e1503317022
7
- data.tar.gz: 603106d7e627b086624d88914601b6cdeb465162116cb6821ca519b5a75c20b7700a0a7ee578e44ef3b311784f69bc17efea1e73a7d32403e5c19efb6b21836a
6
+ metadata.gz: ee79317a849f3b62e25791f8c3e2bc73e55e6531c697f04ef7896ff438bfac817c5efef15f170769fc3c441a212776f6cec90d0a02b7a52764208512862df1af
7
+ data.tar.gz: a4e7c0a8a5661e285978376b12c13977fcb9c522a4a19b8c15177d81917084dde7b595baa30c8ef142547d2e35a5983abec7ccaf2a8b6a52e044e51b8196889f
data/CHANGELOG.md CHANGED
@@ -1,4 +1,32 @@
1
1
  # Changelog
2
+ ## [3.1.1] - 2026-02-19
3
+
4
+ ### 🚀 Features
5
+ * **Infrastructure Configuration Cascade:** Added support for dynamic configuration of RabbitMQ Exchanges and Queues (e.g., `durable`, `auto_delete`). Configurations can now be applied across 3 levels:
6
+ 1. **Global Default:** Via `BugBunny.configure { |c| c.exchange_options = {...} }`.
7
+ 2. **Resource Level:** Via class attributes `self.exchange_options = {...}` on `BugBunny::Resource`.
8
+ 3. **On-the-fly:** Via `BugBunny::Client` request kwargs or `Resource.with(exchange_options: {...})`.
9
+
10
+ ### 🛠 Improvements
11
+ * **Test Suite Resilience:** Updated internal test helpers to use global cascade configurations, resolving `PRECONDITION_FAILED` conflicts during rapid test execution.
12
+
13
+ ## [3.1.0] - 2026-02-18
14
+
15
+ ### 🌟 New Features: Observability & Tracing
16
+ * **Distributed Tracing Stack:** Implemented a native distributed tracing system that ensures full visibility from the Producer to the Consumer/Worker.
17
+ * **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).
18
+ * **Consumer:** Automatically extracts the `correlation_id` from AMQP headers and wraps the entire execution in a **Tagged Logger** block (e.g., `[d41d8cd9...] [API] Processing...`).
19
+ * **Controller:** Introduced `self.log_tags` to allow injecting rich business context into logs (e.g., `[Tenant-123]`) using the native `around_action` hook.
20
+
21
+ ### 🛡 Security
22
+ * **Router Hardening:** Added a strict inheritance check in the `Consumer`.
23
+ * **Prevention:** The router now verifies that the instantiated class inherits from `BugBunny::Controller` before execution.
24
+ * **Impact:** Prevents potential **Remote Code Execution (RCE)** vulnerabilities where an attacker could try to instantiate arbitrary system classes (like `::Kernel`) via the `type` header.
25
+
26
+ ### 🐛 Bug Fixes
27
+ * **RPC Type Consistency:** Fixed a critical issue where RPC responses were ignored if the `correlation_id` was an Integer.
28
+ * **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.
29
+
2
30
  ## [3.0.6] - 2026-02-17
3
31
 
4
32
  ### ♻️ Refactor & Standards
data/README.md CHANGED
@@ -1,8 +1,39 @@
1
1
  # 🐰 BugBunny
2
2
 
3
- **BugBunny** es un framework RPC para Ruby on Rails sobre **RabbitMQ**.
3
+ [![Gem Version](https://badge.fury.io/rb/bug_bunny.svg)](https://badge.fury.io/rb/bug_bunny)
4
4
 
5
- Su filosofía es **"Active Record over AMQP"**. Transforma la complejidad de la mensajería asíncrona en una arquitectura **RESTful simulada**. Los mensajes viajan con Verbos HTTP (`GET`, `POST`, `PUT`, `DELETE`) inyectados en los headers AMQP, permitiendo que un **Router Inteligente** despache las peticiones a controladores Rails estándar.
5
+ **Active Record over AMQP.**
6
+
7
+ BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en una arquitectura **RESTful familiar** para desarrolladores Rails. Envía mensajes como si estuvieras usando Active Record y procésalos como si fueran Controladores de Rails.
8
+
9
+ ---
10
+
11
+ ## 📖 Tabla de Contenidos
12
+ - [Introducción: La Filosofía](#-introducción-la-filosofía)
13
+ - [Instalación](#-instalación)
14
+ - [Configuración Inicial](#-configuración-inicial)
15
+ - [Configuración de Infraestructura en Cascada](#-configuración-de-infraestructura-en-cascada-nuevo-v31)
16
+ - [Modo Cliente: Recursos (ORM)](#-modo-cliente-recursos-orm)
17
+ - [Definición y Atributos](#1-definición-y-atributos-híbridos)
18
+ - [CRUD y Consultas](#2-crud-y-consultas-restful)
19
+ - [Contexto Dinámico (.with)](#3-contexto-dinámico-with)
20
+ - [Client Middleware](#4-client-middleware-interceptores)
21
+ - [Modo Servidor: Controladores](#-modo-servidor-controladores)
22
+ - [Ruteo Inteligente](#1-ruteo-inteligente)
23
+ - [El Controlador](#2-el-controlador)
24
+ - [Manejo de Errores](#3-manejo-de-errores-declarativo)
25
+ - [Observabilidad y Tracing](#-observabilidad-y-tracing)
26
+ - [Guía de Producción](#-guía-de-producción)
27
+
28
+ ---
29
+
30
+ ## 💡 Introducción: La Filosofía
31
+
32
+ En lugar de pensar en "Exchanges" y "Queues", BugBunny inyecta verbos HTTP (`GET`, `POST`, `PUT`, `DELETE`) y rutas (`users/1`) en los headers de AMQP.
33
+
34
+ * **Tu código (Cliente):** `User.create(name: 'Gabi')`
35
+ * **Protocolo (BugBunny):** Envía `POST /users` (Header `type: users`) vía RabbitMQ.
36
+ * **Worker (Servidor):** Recibe el mensaje y ejecuta `UsersController#create`.
6
37
 
7
38
  ---
8
39
 
@@ -11,271 +42,296 @@ Su filosofía es **"Active Record over AMQP"**. Transforma la complejidad de la
11
42
  Agrega la gema a tu `Gemfile`:
12
43
 
13
44
  ```ruby
14
- gem 'bug_bunny'
45
+ gem 'bug_bunny', '~> 3.1'
15
46
  ```
16
47
 
17
- Ejecuta el bundle:
48
+ Ejecuta el bundle e instala los archivos base:
18
49
 
19
50
  ```bash
20
51
  bundle install
21
- ```
22
-
23
- Genera los archivos de configuración iniciales:
24
-
25
- ```bash
26
52
  rails g bug_bunny:install
27
53
  ```
28
54
 
29
- Esto creará:
55
+ Esto genera:
30
56
  1. `config/initializers/bug_bunny.rb`
31
57
  2. `app/rabbit/controllers/`
32
58
 
33
59
  ---
34
60
 
35
- ## ⚙️ Configuración
61
+ ## ⚙️ Configuración Inicial
36
62
 
37
- ### 1. Inicializador y Logging
38
-
39
- BugBunny separa los logs de la aplicación (Requests) de los logs del driver (Heartbeats/Frames) para mantener la consola limpia.
63
+ Para entornos productivos (Puma/Sidekiq), es **obligatorio** configurar un Pool de conexiones.
40
64
 
41
65
  ```ruby
42
66
  # config/initializers/bug_bunny.rb
43
67
 
44
68
  BugBunny.configure do |config|
45
- # --- Credenciales ---
69
+ # 1. Credenciales
46
70
  config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
47
71
  config.username = ENV.fetch('RABBITMQ_USER', 'guest')
48
72
  config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
49
73
  config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
50
74
 
51
- # --- Timeouts ---
52
- config.rpc_timeout = 10 # Timeout para esperar respuesta (Síncrono)
53
- config.network_recovery_interval = 5 # Segundos para reintentar conexión
75
+ # 2. Timeouts y Recuperación
76
+ config.rpc_timeout = 10 # Segundos máx para esperar respuesta (Síncrono)
77
+ config.network_recovery_interval = 5 # Reintento de conexión
54
78
 
55
- # --- Logging (Niveles recomendados) ---
56
- # Logger de BugBunny: Muestra tus requests (INFO)
57
- config.logger = Logger.new(STDOUT)
58
- config.logger.level = Logger::INFO
79
+ # 3. Logging
80
+ config.logger = Rails.logger
81
+ end
59
82
 
60
- # Logger de Bunny (Driver): Silencia el ruido de bajo nivel (WARN)
61
- config.bunny_logger = Logger.new(STDOUT)
62
- config.bunny_logger.level = Logger::WARN
83
+ # 4. Connection Pool (CRÍTICO para concurrencia)
84
+ # Define un pool global para compartir conexiones entre hilos
85
+ BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
86
+ BugBunny.create_connection
63
87
  end
88
+
89
+ # Inyecta el pool en los recursos
90
+ BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
64
91
  ```
65
92
 
66
- ### 2. Connection Pool (Crítico) 🧵
93
+ ---
94
+
95
+ ## 🏗️ Configuración de Infraestructura en Cascada (Nuevo v3.1)
96
+
97
+ BugBunny v3.1 introduce un sistema de configuración jerárquico para los parámetros de RabbitMQ (como la durabilidad de Exchanges y Colas). Las opciones se resuelven en el siguiente orden de prioridad:
67
98
 
68
- Para entornos concurrentes como **Puma** o **Sidekiq**, es **obligatorio** definir un Pool de conexiones global. BugBunny no gestiona hilos automáticamente sin esta configuración.
99
+ 1. **Defaults de la Gema:** Rápidos y efímeros (`durable: false`).
100
+ 2. **Configuración Global:** Definida en el inicializador para todo el entorno.
101
+ 3. **Configuración de Recurso:** Atributos de clase en modelos específicos.
102
+ 4. **Configuración al Vuelo:** Parámetros pasados en la llamada `.with` o en el Cliente manual.
103
+
104
+ **Ejemplo de Configuración Global (Nivel 2):**
105
+ Útil para hacer que todos los recursos en el entorno de pruebas sean auto-borrables.
69
106
 
70
107
  ```ruby
71
108
  # config/initializers/bug_bunny.rb
72
-
73
- # Define el pool global (ajusta el tamaño según tus hilos de Puma/Sidekiq)
74
- BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
75
- BugBunny.create_connection
109
+ BugBunny.configure do |config|
110
+ if Rails.env.test?
111
+ config.exchange_options = { auto_delete: true }
112
+ config.queue_options = { auto_delete: true }
113
+ end
76
114
  end
77
-
78
- # Inyecta el pool a los recursos para que lo usen automáticamente
79
- BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
80
115
  ```
81
116
 
82
117
  ---
83
118
 
84
- ## 🚀 Modo Resource (ORM / Cliente)
119
+ ## 🚀 Modo Cliente: Recursos (ORM)
85
120
 
86
- Define modelos que actúan como proxies de recursos remotos. BugBunny se encarga de serializar, "wrappear" parámetros y enviar el verbo correcto.
121
+ Los recursos son proxies de servicios remotos. Heredan de `BugBunny::Resource`.
87
122
 
88
- ### Definición del Modelo
123
+ ### 1. Definición y Atributos Híbridos
124
+ BugBunny v3 es **Schema-less**. Soporta atributos tipados (ActiveModel) y dinámicos simultáneamente, además de definir su propia infraestructura.
89
125
 
90
126
  ```ruby
91
127
  # app/models/manager/service.rb
92
128
  class Manager::Service < BugBunny::Resource
93
- # 1. Configuración de Transporte
94
- self.exchange = 'box_cluster_manager'
95
- self.exchange_type = 'direct'
129
+ # Configuración de Transporte
130
+ self.exchange = 'cluster_events'
131
+ self.exchange_type = 'topic'
132
+
133
+ # Configuración de Infraestructura Específica (Nivel 3)
134
+ # Este recurso crítico sobrevivirá a reinicios del servidor RabbitMQ
135
+ self.exchange_options = { durable: true, auto_delete: false }
96
136
 
97
- # 2. Configuración Lógica (Routing)
98
- # Define la URL base y la routing key por defecto.
137
+ # Configuración de Ruteo (La "URL" base)
99
138
  self.resource_name = 'services'
100
139
 
101
- # 3. Wrapping de Parámetros (Opcional)
102
- # Por defecto usa el nombre del modelo sin módulo (Manager::Service -> 'service').
103
- # Puedes forzarlo con:
104
- # self.param_key = 'docker_service'
140
+ # A. Atributos Tipados (Opcional, para casting)
141
+ attribute :created_at, :datetime
142
+ attribute :replicas, :integer, default: 1
143
+
144
+ # B. Validaciones (Funcionan en ambos tipos)
145
+ validates :name, presence: true
105
146
  end
106
147
  ```
107
148
 
108
- ### CRUD RESTful
109
-
110
- Las operaciones de Ruby se traducen a verbos HTTP sobre AMQP.
149
+ ### 2. CRUD y Consultas RESTful
111
150
 
112
151
  ```ruby
113
152
  # --- LEER (GET) ---
114
- # Envia: GET services
115
- # Routing Key: "services"
116
- services = Manager::Service.all
117
-
153
+ # RPC: Espera respuesta del worker.
118
154
  # Envia: GET services/123
119
155
  service = Manager::Service.find('123')
120
156
 
157
+ # --- BÚSQUEDAS AVANZADAS ---
158
+ # Soporta Hashes anidados para filtros complejos.
159
+ # Envia: GET services?q[status]=active&q[tags][]=web
160
+ Manager::Service.where(q: { status: 'active', tags: ['web'] })
161
+
121
162
  # --- CREAR (POST) ---
122
- # Envia: POST services
123
- # Body: { "service": { "name": "nginx", "replicas": 3 } }
124
- # Nota: Envuelve los params automáticamente en la clave 'service'.
163
+ # RPC: Envía payload y espera el objeto persistido.
164
+ # Payload: { "service": { "name": "nginx", "replicas": 3 } }
125
165
  svc = Manager::Service.create(name: 'nginx', replicas: 3)
126
166
 
127
167
  # --- ACTUALIZAR (PUT) ---
128
- # Envia: PUT services/123
129
- # Body: { "service": { "replicas": 5 } }
130
- svc.update(replicas: 5)
168
+ # Dirty Tracking: Solo envía los campos que cambiaron.
169
+ svc.name = 'nginx-pro'
170
+ svc.save
131
171
 
132
172
  # --- ELIMINAR (DELETE) ---
133
- # Envia: DELETE services/123
134
173
  svc.destroy
135
174
  ```
136
175
 
137
- ### Contexto Dinámico (`.with`)
138
-
139
- Puedes cambiar la configuración (Routing Key, Exchange) para una operación específica sin afectar al modelo global. El contexto se mantiene durante el ciclo de vida del objeto.
176
+ ### 3. Contexto Dinámico (`.with`)
177
+ Puedes sobrescribir la configuración de enrutamiento o infraestructura para una ejecución específica sin afectar al modelo global (Thread-Safe).
140
178
 
141
179
  ```ruby
142
- # La instancia nace sabiendo que pertenece a la routing_key 'urgent'
143
- svc = Manager::Service.with(routing_key: 'urgent').new(name: 'redis')
180
+ # Nivel 4: Configuración al vuelo. Inyectamos opciones solo para esta llamada.
181
+ Manager::Service.with(
182
+ routing_key: 'high_priority',
183
+ exchange_options: { durable: false } # Ignora el durable: true de la clase
184
+ ).create(name: 'redis_temp')
185
+ ```
144
186
 
145
- # ... lógica de negocio ...
187
+ ### 4. Client Middleware (Interceptores)
188
+ Intercepta peticiones antes de salir hacia RabbitMQ. Ideal para inyectar Auth o Headers.
146
189
 
147
- # Al guardar, BugBunny recuerda el contexto y envía a 'urgent'
148
- svc.save
149
- # Log: [BugBunny] [POST] '/services' | Routing Key: 'urgent'
190
+ ```ruby
191
+ class Manager::Service < BugBunny::Resource
192
+ client_middleware do |stack|
193
+ stack.use(Class.new(BugBunny::Middleware::Base) do
194
+ def on_request(env)
195
+ env.headers['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
196
+ env.headers['X-App-Version'] = '1.0.0'
197
+ end
198
+ end)
199
+ end
200
+ end
150
201
  ```
151
202
 
152
203
  ---
153
204
 
154
- ## 📡 Modo Servidor (Worker & Router)
205
+ ## 📡 Modo Servidor: Controladores
155
206
 
156
- BugBunny incluye un **Router Inteligente** que despacha mensajes a controladores basándose en el Verbo y el Path, imitando a Rails.
207
+ BugBunny implementa un **Router** que despacha mensajes a controladores basándose en el header `type` (URL) y `x-http-method`.
157
208
 
158
- ### 1. El Controlador (`app/rabbit/controllers/`)
209
+ ### 1. Ruteo Inteligente
210
+ El consumidor infiere automáticamente la acción:
159
211
 
160
- Hereda de `BugBunny::Controller`. Tienes acceso a `params`, `before_action` y `rescue_from`.
212
+ | Verbo AMQP | Path (Header `type`) | Controlador | Acción |
213
+ | :--- | :--- | :--- | :--- |
214
+ | `GET` | `services` | `ServicesController` | `index` |
215
+ | `GET` | `services/123` | `ServicesController` | `show` |
216
+ | `POST` | `services` | `ServicesController` | `create` |
217
+ | `PUT` | `services/123` | `ServicesController` | `update` |
218
+ | `DELETE` | `services/123` | `ServicesController` | `destroy` |
219
+ | `POST` | `services/123/restart` | `ServicesController` | `restart` (Custom) |
220
+
221
+ ### 2. El Controlador
222
+ Ubicación: `app/rabbit/controllers/`.
161
223
 
162
224
  ```ruby
163
- # app/rabbit/controllers/services_controller.rb
164
225
  class ServicesController < BugBunny::Controller
165
- # Callbacks
166
- before_action :set_service, only: %i[show update destroy]
226
+ # Callbacks estándar
227
+ before_action :set_service, only: [:show, :update]
167
228
 
168
- # GET services
169
- def index
170
- render status: 200, json: DockerService.all
229
+ def show
230
+ # Renderiza JSON que viajará de vuelta por la cola reply-to
231
+ render status: 200, json: { id: @service.id, state: 'running' }
171
232
  end
172
233
 
173
- # POST services
174
234
  def create
175
- # BugBunny wrappea los params automáticamente en el Resource.
176
- # Aquí los consumimos con seguridad usando Strong Parameters simulados o hash access.
177
- # params[:service] estará disponible gracias al param_key del Resource.
178
-
179
- result = DockerService.create(params[:service])
180
- render status: 201, json: result
235
+ # BugBunny envuelve los params automáticamente (param_key)
236
+ # params[:service] => { name: '...', replicas: ... }
237
+ if Service.create(params[:service])
238
+ render status: 201, json: { status: 'created' }
239
+ else
240
+ render status: 422, json: { errors: 'Invalid' }
241
+ end
181
242
  end
182
243
 
183
244
  private
184
245
 
185
246
  def set_service
186
- # params[:id] se extrae automágicamente de la URL (Route Param)
187
- @service = DockerService.find(params[:id])
188
-
189
- unless @service
190
- render status: 404, json: { error: "Service not found" }
191
- end
247
+ # params[:id] se extrae del Path
248
+ @service = Service.find(params[:id])
192
249
  end
193
250
  end
194
251
  ```
195
252
 
196
- ### 2. Manejo de Errores (`rescue_from`)
197
-
198
- Puedes definir un `ApplicationController` base para manejar errores de forma centralizada y declarativa.
253
+ ### 3. Manejo de Errores Declarativo
254
+ Captura excepciones y devuélvelas como códigos de estado AMQP/HTTP.
199
255
 
200
256
  ```ruby
201
- # app/rabbit/controllers/application.rb
202
257
  class ApplicationController < BugBunny::Controller
203
- # Manejo específico
204
- rescue_from ActiveRecord::RecordNotFound do
258
+ rescue_from ActiveRecord::RecordNotFound do |e|
205
259
  render status: :not_found, json: { error: "Resource missing" }
206
260
  end
207
261
 
208
- rescue_from ActiveModel::ValidationError do |e|
209
- render status: :unprocessable_entity, json: e.model.errors
210
- end
211
-
212
- # Catch-all (Red de seguridad)
213
262
  rescue_from StandardError do |e|
214
263
  BugBunny.configuration.logger.error(e)
215
- render status: :internal_server_error, json: { error: "Internal Error" }
264
+ render status: :internal_server_error, json: { error: "Crash" }
216
265
  end
217
266
  end
218
267
  ```
219
268
 
220
- ### 3. Tabla de Ruteo (Convención)
269
+ ---
221
270
 
222
- El Router infiere la acción automáticamente:
271
+ ## 🔎 Observabilidad y Tracing
223
272
 
224
- | Verbo | URL Pattern | Controlador | Acción |
225
- | :--- | :--- | :--- | :--- |
226
- | `GET` | `services` | `ServicesController` | `index` |
227
- | `GET` | `services/12` | `ServicesController` | `show` |
228
- | `POST` | `services` | `ServicesController` | `create` |
229
- | `PUT` | `services/12` | `ServicesController` | `update` |
230
- | `DELETE` | `services/12` | `ServicesController` | `destroy` |
231
- | `POST` | `services/12/restart` | `ServicesController` | `restart` (Custom) |
273
+ > **Novedad v3.1:** BugBunny implementa Distributed Tracing nativo.
232
274
 
233
- ---
275
+ El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
276
+
277
+ ### 1. Logs Automáticos (Consumer)
278
+ No requiere configuración. El worker envuelve la ejecución en bloques de log etiquetados con el UUID.
234
279
 
235
- ## 🔌 Modo Publisher (Cliente Manual)
280
+ ```text
281
+ [d41d8cd9...] [Consumer] Listening on queue...
282
+ [d41d8cd9...] [API] Processing ServicesController#create...
283
+ ```
236
284
 
237
- Si necesitas enviar mensajes crudos fuera de la lógica Resource, usa `BugBunny::Client`.
285
+ ### 2. Logs de Negocio (Controller)
286
+ Inyecta contexto rico (Tenant, Usuario, IP) en los logs usando `log_tags`.
238
287
 
239
288
  ```ruby
240
- client = BugBunny::Client.new(pool: BUG_BUNNY_POOL)
241
-
242
- # --- REQUEST (Síncrono / RPC) ---
243
- # Espera la respuesta. Lanza BugBunny::RequestTimeout si falla.
244
- response = client.request('services/123/logs',
245
- method: :get,
246
- exchange: 'logs_exchange',
247
- timeout: 5
248
- )
249
- puts response['body']
250
-
251
- # --- PUBLISH (Asíncrono / Fire-and-Forget) ---
252
- # No espera respuesta.
253
- client.publish('audit/events',
254
- method: :post,
255
- body: { event: 'login', user_id: 1 }
256
- )
289
+ # app/rabbit/controllers/application_controller.rb
290
+ class ApplicationController < BugBunny::Controller
291
+ self.log_tags = [
292
+ ->(c) { c.params[:tenant_id] }, # Agrega [Tenant-55]
293
+ ->(c) { c.headers['X-Source'] } # Agrega [Console]
294
+ ]
295
+ end
257
296
  ```
258
297
 
259
- ---
298
+ ### 3. Inyección en el Productor
299
+ Para que tus logs de Rails y Rabbit coincidan, usa un middleware global:
260
300
 
261
- ## 🏗 Arquitectura REST-over-AMQP
301
+ ```ruby
302
+ # config/initializers/bug_bunny.rb
303
+ # Middleware para inyectar Current.request_id de Rails al mensaje Rabbit
304
+ class CorrelationInjector < BugBunny::Middleware::Base
305
+ def on_request(env)
306
+ env.correlation_id = Current.request_id if defined?(Current)
307
+ end
308
+ end
262
309
 
263
- BugBunny desacopla el transporte de la lógica usando headers estándar.
310
+ BugBunny::Client.prepend(Module.new {
311
+ def initialize(pool:)
312
+ super
313
+ @stack.use CorrelationInjector
314
+ end
315
+ })
316
+ ```
264
317
 
265
- 1. **Semántica:** El mensaje lleva headers `type` (URL) y `x-http-method` (Verbo).
266
- 2. **Ruteo:** El consumidor lee estos headers y ejecuta el controlador correspondiente.
267
- 3. **Parametros:** `params` unifica:
268
- * **Route Params:** `services/123` -> `params[:id] = 123`
269
- * **Query Params:** `services?force=true` -> `params[:force] = true`
270
- * **Body:** Payload JSON fusionado en el hash.
318
+ ---
271
319
 
272
- ### Logs Estructurados
320
+ ## 🧵 Guía de Producción
273
321
 
274
- Facilita el debugging mostrando claramente qué recurso se está tocando y por dónde viaja.
322
+ ### Connection Pooling
323
+ Es vital usar `ConnectionPool` si usas servidores web multi-hilo (Puma) o workers (Sidekiq). BugBunny no gestiona hilos internamente; se apoya en el pool.
275
324
 
276
- ```text
277
- [BugBunny] [POST] '/services' | Exchange: 'cluster' (Type: direct) | Routing Key: 'node-1'
278
- ```
325
+ ### Fork Safety
326
+ BugBunny incluye un `Railtie` que detecta automáticamente cuando Rails hace un "Fork" (ej: Puma en modo Cluster o Spring). Desconecta automáticamente las conexiones heredadas para evitar corrupción de datos en los sockets TCP.
327
+
328
+ ### RPC y "Direct Reply-To"
329
+ Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
330
+ * **Trade-off:** Si el cliente (Rails) se reinicia justo después de enviar un mensaje RPC pero antes de recibir la respuesta, esa respuesta se pierde.
331
+ * **Recomendación:** Diseña tus acciones RPC (`POST`, `PUT`) para que sean **idempotentes** (seguras de reintentar ante un timeout).
332
+
333
+ ### Seguridad
334
+ El Router incluye protecciones contra **Remote Code Execution (RCE)**. Verifica estrictamente que la clase instanciada herede de `BugBunny::Controller` antes de ejecutarla, impidiendo la inyección de clases arbitrarias de Ruby vía el header `type`.
279
335
 
280
336
  ---
281
337
 
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
@@ -1,14 +1,16 @@
1
- # lib/bug_bunny/client.rb
1
+ # frozen_string_literal: true
2
+
2
3
  require_relative 'middleware/stack'
3
4
 
4
5
  module BugBunny
5
6
  # Cliente principal para realizar peticiones a RabbitMQ.
6
7
  #
7
8
  # Implementa el patrón "Onion Middleware" (Arquitectura de Cebolla) similar a Faraday.
8
- # Mantiene una interfaz flexible donde el verbo HTTP se pasa como opción.
9
+ # Mantiene una interfaz flexible donde el verbo HTTP se pasa como opción y permite
10
+ # configurar la infraestructura AMQP de forma granular por petición.
9
11
  #
10
- # @example Petición RPC (GET)
11
- # client.request('users/123', method: :get)
12
+ # @example Petición RPC (GET) con opciones de infraestructura
13
+ # client.request('users/123', method: :get, exchange_options: { durable: true })
12
14
  #
13
15
  # @example Publicación Fire-and-Forget (POST)
14
16
  # client.publish('logs', method: :post, body: { msg: 'Error' })
@@ -41,6 +43,8 @@ module BugBunny
41
43
  # @option args [Object] :body El cuerpo del mensaje.
42
44
  # @option args [Hash] :headers Headers AMQP adicionales.
43
45
  # @option args [Integer] :timeout Tiempo máximo de espera.
46
+ # @option args [Hash] :exchange_options Opciones específicas para la declaración del Exchange.
47
+ # @option args [Hash] :queue_options Opciones específicas para la declaración de la Cola.
44
48
  # @yield [req] Bloque para configurar el objeto Request directamente.
45
49
  # @return [Hash] La respuesta del servidor.
46
50
  def request(url, **args)
@@ -65,18 +69,28 @@ module BugBunny
65
69
 
66
70
  # Ejecuta la lógica de envío dentro del contexto del Pool.
67
71
  # Mapea los argumentos al objeto Request y ejecuta la cadena de middlewares.
72
+ #
73
+ # @param method_name [Symbol] El método del productor a llamar (:rpc o :fire).
74
+ # @param url [String] La ruta destino.
75
+ # @param args [Hash] Argumentos pasados a los métodos públicos.
76
+ # @yield [req] Bloque para configuración adicional del Request.
68
77
  def run_in_pool(method_name, url, args)
69
78
  # 1. Builder del Request
70
79
  req = BugBunny::Request.new(url)
71
80
 
72
81
  # 2. Syntactic Sugar: Mapeo de argumentos a atributos del Request
73
- req.method = args[:method] if args[:method]
74
- req.body = args[:body] if args[:body]
75
- req.exchange = args[:exchange] if args[:exchange]
76
- req.exchange_type = args[:exchange_type] if args[:exchange_type]
77
- req.routing_key = args[:routing_key] if args[:routing_key]
78
- req.timeout = args[:timeout] if args[:timeout]
79
- req.headers.merge!(args[:headers]) if args[:headers]
82
+ req.method = args[:method] if args[:method]
83
+ req.body = args[:body] if args[:body]
84
+ req.exchange = args[:exchange] if args[:exchange]
85
+ req.exchange_type = args[:exchange_type] if args[:exchange_type]
86
+ req.routing_key = args[:routing_key] if args[:routing_key]
87
+ req.timeout = args[:timeout] if args[:timeout]
88
+
89
+ # Inyección de opciones de infraestructura (Nivel 3 de la cascada)
90
+ req.exchange_options = args[:exchange_options] if args[:exchange_options]
91
+ req.queue_options = args[:queue_options] if args[:queue_options]
92
+
93
+ req.headers.merge!(args[:headers]) if args[:headers]
80
94
 
81
95
  # 3. Configuración del usuario (bloque específico por request)
82
96
  yield req if block_given?
@@ -94,6 +108,7 @@ module BugBunny
94
108
  app = @stack.build(final_action)
95
109
  app.call(req)
96
110
  ensure
111
+ # Aseguramos el cierre del canal pero mantenemos la conexión del pool
97
112
  session.close
98
113
  end
99
114
  end