bug_bunny 3.1.0 → 3.1.2

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.
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,495 +42,316 @@ 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
36
-
37
- ### 1. Inicializador y Logging
61
+ ## ⚙️ Configuración Inicial
38
62
 
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
54
-
55
- # --- Logging (Niveles recomendados) ---
56
- # Logger de BugBunny: Muestra tus requests (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
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
64
78
 
65
- # Logger de Bunny (Driver): Silencia el ruido de bajo nivel (WARN)
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
71
- config.bunny_logger.level = Logger::WARN
79
+ # 3. Logging
80
+ config.logger = Rails.logger
81
+ end
72
82
 
73
- # Controller Namaspeace
74
- config.controller_namespace = 'MyApp::AsyncHandlers' # Default: 'Rabbit::Controllers'
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
75
87
  end
88
+
89
+ # Inyecta el pool en los recursos
90
+ BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
76
91
  ```
77
92
 
78
- ### 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:
79
98
 
80
- 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.
81
106
 
82
107
  ```ruby
83
108
  # config/initializers/bug_bunny.rb
84
-
85
- # Define el pool global (ajusta el tamaño según tus hilos de Puma/Sidekiq)
86
- BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
87
- 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
88
114
  end
89
-
90
- # Inyecta el pool a los recursos para que lo usen automáticamente
91
- BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
92
115
  ```
93
116
 
94
117
  ---
95
118
 
96
- ## 🚀 Modo Resource (ORM / Cliente)
119
+ ## 🚀 Modo Cliente: Recursos (ORM)
97
120
 
98
- 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`.
99
122
 
100
- ### 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.
101
125
 
102
126
  ```ruby
103
127
  # app/models/manager/service.rb
104
128
  class Manager::Service < BugBunny::Resource
105
- # 1. Configuración de Transporte
106
- self.exchange = 'box_cluster_manager'
107
- self.exchange_type = 'direct'
129
+ # Configuración de Transporte
130
+ self.exchange = 'cluster_events'
131
+ self.exchange_type = 'topic'
108
132
 
109
- # 2. Configuración Lógica (Routing)
110
- # Define la URL base y la routing key por defecto.
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 }
136
+
137
+ # Configuración de Ruteo (La "URL" base)
111
138
  self.resource_name = 'services'
112
139
 
113
- # 3. Wrapping de Parámetros (Opcional)
114
- # Por defecto usa el nombre del modelo sin módulo (Manager::Service -> 'service').
115
- # Puedes forzarlo con:
116
- # 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
117
146
  end
118
147
  ```
119
148
 
120
- ### CRUD RESTful
121
-
122
- Las operaciones de Ruby se traducen a verbos HTTP sobre AMQP.
149
+ ### 2. CRUD y Consultas RESTful
123
150
 
124
151
  ```ruby
125
152
  # --- LEER (GET) ---
126
- # Envia: GET services
127
- # Routing Key: "services"
128
- services = Manager::Service.all
129
-
153
+ # RPC: Espera respuesta del worker.
130
154
  # Envia: GET services/123
131
155
  service = Manager::Service.find('123')
132
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
+
133
162
  # --- CREAR (POST) ---
134
- # Envia: POST services
135
- # Body: { "service": { "name": "nginx", "replicas": 3 } }
136
- # 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 } }
137
165
  svc = Manager::Service.create(name: 'nginx', replicas: 3)
138
166
 
139
167
  # --- ACTUALIZAR (PUT) ---
140
- # Envia: PUT services/123
141
- # Body: { "service": { "replicas": 5 } }
142
- svc.update(replicas: 5)
168
+ # Dirty Tracking: Solo envía los campos que cambiaron.
169
+ svc.name = 'nginx-pro'
170
+ svc.save
143
171
 
144
172
  # --- ELIMINAR (DELETE) ---
145
- # Envia: DELETE services/123
146
173
  svc.destroy
147
174
  ```
148
175
 
149
- ### Contexto Dinámico (`.with`)
150
-
151
- 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).
152
178
 
153
179
  ```ruby
154
- # La instancia nace sabiendo que pertenece a la routing_key 'urgent'
155
- svc = Manager::Service.with(routing_key: 'urgent').new(name: 'redis')
156
-
157
- # ... lógica de negocio ...
158
-
159
- # Al guardar, BugBunny recuerda el contexto y envía a 'urgent'
160
- svc.save
161
- # Log: [BugBunny] [POST] '/services' | Routing Key: 'urgent'
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')
162
185
  ```
163
186
 
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.
187
+ ### 4. Client Middleware (Interceptores)
188
+ Intercepta peticiones de ida y respuestas de vuelta en la arquitectura del cliente.
166
189
 
167
- **Sugerencia:** Agregar un ejemplo en la sección **CRUD RESTful > LEER (GET)**:
190
+ **Middlewares Incluidos (Built-ins)**
191
+ Si usas `BugBunny::Resource` el manejo de JSON y errores ya está integrado. Pero si utilizas el cliente manual (`BugBunny::Client`), puedes inyectar los middlewares incluidos para no tener que parsear respuestas manualmente:
168
192
 
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
- ```
193
+ * `BugBunny::Middleware::JsonResponse`: Parsea automáticamente el cuerpo de la respuesta de JSON a un Hash de Ruby.
194
+ * `BugBunny::Middleware::RaiseError`: Evalúa el código de estado (`status`) de la respuesta y lanza excepciones nativas (`BugBunny::NotFound`, `BugBunny::UnprocessableEntity`, `BugBunny::InternalServerError`, etc.).
175
195
 
176
- ### 🔌 Manipulación de Headers (Middleware)
196
+ ```ruby
197
+ # Uso con el cliente manual
198
+ client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |stack|
199
+ stack.use BugBunny::Middleware::RaiseError
200
+ stack.use BugBunny::Middleware::JsonResponse
201
+ end
177
202
 
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.
203
+ # Ahora el cliente devolverá Hashes y lanzará errores si el worker falla
204
+ response = client.request('users/1', method: :get)
205
+ ```
179
206
 
180
- Existen 3 formas principales de usarlo:
207
+ **Middlewares Personalizados**
208
+ Ideales para inyectar Auth o Headers de trazabilidad en todos los requests de un Recurso.
181
209
 
182
- #### 1. Definición Inline (Rápida)
183
- Ideal para inyectar headers estáticos específicos de un recurso.
184
210
  ```ruby
185
- class Payment < BugBunny::Resource
211
+ class Manager::Service < BugBunny::Resource
186
212
  client_middleware do |stack|
187
213
  stack.use(Class.new(BugBunny::Middleware::Base) do
188
214
  def on_request(env)
189
- env.headers['X-Service-Version'] = 'v2'
190
- env.headers['Content-Type'] = 'application/json'
215
+ env.headers['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
216
+ env.headers['X-App-Version'] = '1.0.0'
191
217
  end
192
218
  end)
193
219
  end
194
220
  end
195
221
  ```
196
222
 
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
-
238
223
  ---
239
224
 
240
- ## 📡 Modo Servidor (Worker & Router)
225
+ ## 📡 Modo Servidor: Controladores
241
226
 
242
- BugBunny incluye un **Router Inteligente** que despacha mensajes a controladores basándose en el Verbo y el Path, imitando a Rails.
227
+ BugBunny implementa un **Router** que despacha mensajes a controladores basándose en el header `type` (URL) y `x-http-method`.
243
228
 
244
- ### 1. El Controlador (`app/rabbit/controllers/`)
229
+ ### 1. Ruteo Inteligente
230
+ El consumidor infiere automáticamente la acción:
245
231
 
246
- Hereda de `BugBunny::Controller`. Tienes acceso a `params`, `before_action` y `rescue_from`.
232
+ | Verbo AMQP | Path (Header `type`) | Controlador | Acción |
233
+ | :--- | :--- | :--- | :--- |
234
+ | `GET` | `services` | `ServicesController` | `index` |
235
+ | `GET` | `services/123` | `ServicesController` | `show` |
236
+ | `POST` | `services` | `ServicesController` | `create` |
237
+ | `PUT` | `services/123` | `ServicesController` | `update` |
238
+ | `DELETE` | `services/123` | `ServicesController` | `destroy` |
239
+ | `POST` | `services/123/restart` | `ServicesController` | `restart` (Custom) |
240
+
241
+ ### 2. El Controlador
242
+ Ubicación: `app/rabbit/controllers/`.
247
243
 
248
244
  ```ruby
249
- # app/rabbit/controllers/services_controller.rb
250
245
  class ServicesController < BugBunny::Controller
251
- # Callbacks
252
- before_action :set_service, only: %i[show update destroy]
246
+ # Callbacks estándar
247
+ before_action :set_service, only: [:show, :update]
253
248
 
254
- # GET services
255
- def index
256
- render status: 200, json: DockerService.all
249
+ def show
250
+ # Renderiza JSON que viajará de vuelta por la cola reply-to
251
+ render status: 200, json: { id: @service.id, state: 'running' }
257
252
  end
258
253
 
259
- # POST services
260
254
  def create
261
- # BugBunny wrappea los params automáticamente en el Resource.
262
- # Aquí los consumimos con seguridad usando Strong Parameters simulados o hash access.
263
- # params[:service] estará disponible gracias al param_key del Resource.
264
-
265
- result = DockerService.create(params[:service])
266
- render status: 201, json: result
255
+ # BugBunny envuelve los params automáticamente (param_key)
256
+ # params[:service] => { name: '...', replicas: ... }
257
+ if Service.create(params[:service])
258
+ render status: 201, json: { status: 'created' }
259
+ else
260
+ render status: 422, json: { errors: 'Invalid' }
261
+ end
267
262
  end
268
263
 
269
264
  private
270
265
 
271
266
  def set_service
272
- # params[:id] se extrae automágicamente de la URL (Route Param)
273
- @service = DockerService.find(params[:id])
274
-
275
- unless @service
276
- render status: 404, json: { error: "Service not found" }
277
- end
267
+ # params[:id] se extrae del Path
268
+ @service = Service.find(params[:id])
278
269
  end
279
270
  end
280
271
  ```
281
272
 
282
- ### 2. Manejo de Errores (`rescue_from`)
283
-
284
- Puedes definir un `ApplicationController` base para manejar errores de forma centralizada y declarativa.
273
+ ### 3. Manejo de Errores Declarativo
274
+ Captura excepciones y devuélvelas como códigos de estado AMQP/HTTP.
285
275
 
286
276
  ```ruby
287
- # app/rabbit/controllers/application.rb
288
277
  class ApplicationController < BugBunny::Controller
289
- # Manejo específico
290
- rescue_from ActiveRecord::RecordNotFound do
278
+ rescue_from ActiveRecord::RecordNotFound do |e|
291
279
  render status: :not_found, json: { error: "Resource missing" }
292
280
  end
293
281
 
294
- rescue_from ActiveModel::ValidationError do |e|
295
- render status: :unprocessable_entity, json: e.model.errors
296
- end
297
-
298
- # Catch-all (Red de seguridad)
299
282
  rescue_from StandardError do |e|
300
283
  BugBunny.configuration.logger.error(e)
301
- render status: :internal_server_error, json: { error: "Internal Error" }
302
- end
303
- end
304
- ```
305
-
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)
321
-
322
- El Router infiere la acción automáticamente:
323
-
324
- | Verbo | URL Pattern | Controlador | Acción |
325
- | :--- | :--- | :--- | :--- |
326
- | `GET` | `services` | `ServicesController` | `index` |
327
- | `GET` | `services/12` | `ServicesController` | `show` |
328
- | `POST` | `services` | `ServicesController` | `create` |
329
- | `PUT` | `services/12` | `ServicesController` | `update` |
330
- | `DELETE` | `services/12` | `ServicesController` | `destroy` |
331
- | `POST` | `services/12/restart` | `ServicesController` | `restart` (Custom) |
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
284
+ render status: :internal_server_error, json: { error: "Crash" }
369
285
  end
370
286
  end
371
-
372
- # Aplicamos el parche para que afecte a Resources y Clientes manuales
373
- BugBunny::Client.prepend(BugBunnyGlobalMiddleware)
374
287
  ```
375
288
 
376
289
  ---
377
290
 
378
- #### 2. Consumidor: Logging Automático
291
+ ## 🔎 Observabilidad y Tracing
379
292
 
380
- El consumidor de BugBunny está diseñado para garantizar la trazabilidad "out-of-the-box".
293
+ > **Novedad v3.1:** BugBunny implementa Distributed Tracing nativo.
381
294
 
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.
295
+ El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
387
296
 
388
- **No necesitas configurar nada.** Tus logs se verán así automáticamente:
297
+ ### 1. Logs Automáticos (Consumer)
298
+ No requiere configuración. El worker envuelve la ejecución en bloques de log etiquetados con el UUID.
389
299
 
390
300
  ```text
391
- [d41d8cd9-8f00...] [Consumer] Listening on queue...
392
- [d41d8cd9-8f00...] [API] Procesando usuario 123...
301
+ [d41d8cd9...] [Consumer] Listening on queue...
302
+ [d41d8cd9...] [API] Processing ServicesController#create...
393
303
  ```
394
304
 
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.
305
+ ### 2. Logs de Negocio (Controller)
306
+ Inyecta contexto rico (Tenant, Usuario, IP) en los logs usando `log_tags`.
399
307
 
400
308
  ```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'] }
309
+ # app/rabbit/controllers/application_controller.rb
310
+ class ApplicationController < BugBunny::Controller
311
+ self.log_tags = [
312
+ ->(c) { c.params[:tenant_id] }, # Agrega [Tenant-55]
313
+ ->(c) { c.headers['X-Source'] } # Agrega [Console]
408
314
  ]
409
315
  end
410
316
  ```
411
317
 
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.
318
+ ### 3. Inyección en el Productor
319
+ Para que tus logs de Rails y Rabbit coincidan, usa un middleware global:
421
320
 
422
321
  ```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
322
+ # config/initializers/bug_bunny.rb
323
+ # Middleware para inyectar Current.request_id de Rails al mensaje Rabbit
324
+ class CorrelationInjector < BugBunny::Middleware::Base
325
+ def on_request(env)
326
+ env.correlation_id = Current.request_id if defined?(Current)
433
327
  end
434
328
  end
435
- ```
436
329
 
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
-
443
- ---
444
-
445
- ## 🔌 Modo Publisher (Cliente Manual)
446
-
447
- Si necesitas enviar mensajes crudos fuera de la lógica Resource, usa `BugBunny::Client`.
448
-
449
- ```ruby
450
- client = BugBunny::Client.new(pool: BUG_BUNNY_POOL)
451
-
452
- # --- REQUEST (Síncrono / RPC) ---
453
- # Espera la respuesta. Lanza BugBunny::RequestTimeout si falla.
454
- response = client.request('services/123/logs',
455
- method: :get,
456
- exchange: 'logs_exchange',
457
- timeout: 5
458
- )
459
- puts response['body']
460
-
461
- # --- PUBLISH (Asíncrono / Fire-and-Forget) ---
462
- # No espera respuesta.
463
- client.publish('audit/events',
464
- method: :post,
465
- body: { event: 'login', user_id: 1 }
466
- )
330
+ BugBunny::Client.prepend(Module.new {
331
+ def initialize(pool:)
332
+ super
333
+ @stack.use CorrelationInjector
334
+ end
335
+ })
467
336
  ```
468
337
 
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
-
483
338
  ---
484
339
 
485
- ## 🏗 Arquitectura REST-over-AMQP
486
-
487
- BugBunny desacopla el transporte de la lógica usando headers estándar.
340
+ ## 🧵 Guía de Producción
488
341
 
489
- 1. **Semántica:** El mensaje lleva headers `type` (URL) y `x-http-method` (Verbo).
490
- 2. **Ruteo:** El consumidor lee estos headers y ejecuta el controlador correspondiente.
491
- 3. **Parametros:** `params` unifica:
492
- * **Route Params:** `services/123` -> `params[:id] = 123`
493
- * **Query Params:** `services?force=true` -> `params[:force] = true`
494
- * **Body:** Payload JSON fusionado en el hash.
342
+ ### Connection Pooling
343
+ Es vital usar `ConnectionPool` si usas servidores web multi-hilo (Puma) o workers (Sidekiq). BugBunny no gestiona hilos internamente; se apoya en el pool.
495
344
 
496
- ### Logs Estructurados
345
+ ### Fork Safety
346
+ 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.
497
347
 
498
- Facilita el debugging mostrando claramente qué recurso se está tocando y por dónde viaja.
348
+ ### RPC y "Direct Reply-To"
349
+ Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
350
+ * **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.
351
+ * **Recomendación:** Diseña tus acciones RPC (`POST`, `PUT`) para que sean **idempotentes** (seguras de reintentar ante un timeout).
499
352
 
500
- ```text
501
- [BugBunny] [POST] '/services' | Exchange: 'cluster' (Type: direct) | Routing Key: 'node-1'
502
- ```
353
+ ### Seguridad
354
+ 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`.
503
355
 
504
356
  ---
505
357