bug_bunny 3.1.0 → 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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +182 -350
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +28 -2
- data/lib/bug_bunny/consumer.rb +13 -14
- data/lib/bug_bunny/controller.rb +2 -2
- data/lib/bug_bunny/producer.rb +41 -32
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +135 -16
- data/lib/bug_bunny/session.rb +47 -18
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test/integration/infrastructure_test.rb +61 -0
- data/test/integration/manual_client_test.rb +203 -0
- data/test/test_helper.rb +96 -11
- metadata +18 -16
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test/integration/fire_and_forget_test.rb +0 -76
- data/test/integration/rpc_flow_test.rb +0 -78
- data/test/unit/configuration_test.rb +0 -40
- data/test/unit/consumer_test.rb +0 -44
- data/test/unit/controller_headers_test.rb +0 -38
- data/test/unit/hybrid_resource_test.rb +0 -60
- data/test/unit/middleware_test.rb +0 -61
- data/test/unit/resource_test.rb +0 -49
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
data/README.md
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
# 🐰 BugBunny
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/bug_bunny)
|
|
4
4
|
|
|
5
|
-
|
|
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,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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
52
|
-
config.rpc_timeout = 10 #
|
|
53
|
-
config.network_recovery_interval = 5 #
|
|
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
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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:
|
|
98
|
+
|
|
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.
|
|
79
103
|
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
119
|
+
## 🚀 Modo Cliente: Recursos (ORM)
|
|
97
120
|
|
|
98
|
-
|
|
121
|
+
Los recursos son proxies de servicios remotos. Heredan de `BugBunny::Resource`.
|
|
99
122
|
|
|
100
|
-
### Definición
|
|
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
|
-
#
|
|
106
|
-
self.exchange = '
|
|
107
|
-
self.exchange_type = '
|
|
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 }
|
|
108
136
|
|
|
109
|
-
#
|
|
110
|
-
# Define la URL base y la routing key por defecto.
|
|
137
|
+
# Configuración de Ruteo (La "URL" base)
|
|
111
138
|
self.resource_name = 'services'
|
|
112
139
|
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
135
|
-
#
|
|
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
|
-
#
|
|
141
|
-
|
|
142
|
-
svc.
|
|
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
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
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
|
-
###
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
**Sugerencia:** Agregar un ejemplo en la sección **CRUD RESTful > LEER (GET)**:
|
|
187
|
+
### 4. Client Middleware (Interceptores)
|
|
188
|
+
Intercepta peticiones antes de salir hacia RabbitMQ. Ideal para inyectar Auth o Headers.
|
|
168
189
|
|
|
169
190
|
```ruby
|
|
170
|
-
|
|
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
|
|
191
|
+
class Manager::Service < BugBunny::Resource
|
|
186
192
|
client_middleware do |stack|
|
|
187
193
|
stack.use(Class.new(BugBunny::Middleware::Base) do
|
|
188
194
|
def on_request(env)
|
|
189
|
-
env.headers['
|
|
190
|
-
env.headers['
|
|
195
|
+
env.headers['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
|
|
196
|
+
env.headers['X-App-Version'] = '1.0.0'
|
|
191
197
|
end
|
|
192
198
|
end)
|
|
193
199
|
end
|
|
194
200
|
end
|
|
195
201
|
```
|
|
196
202
|
|
|
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
203
|
---
|
|
239
204
|
|
|
240
|
-
## 📡 Modo Servidor
|
|
205
|
+
## 📡 Modo Servidor: Controladores
|
|
206
|
+
|
|
207
|
+
BugBunny implementa un **Router** que despacha mensajes a controladores basándose en el header `type` (URL) y `x-http-method`.
|
|
241
208
|
|
|
242
|
-
|
|
209
|
+
### 1. Ruteo Inteligente
|
|
210
|
+
El consumidor infiere automáticamente la acción:
|
|
243
211
|
|
|
244
|
-
|
|
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) |
|
|
245
220
|
|
|
246
|
-
|
|
221
|
+
### 2. El Controlador
|
|
222
|
+
Ubicación: `app/rabbit/controllers/`.
|
|
247
223
|
|
|
248
224
|
```ruby
|
|
249
|
-
# app/rabbit/controllers/services_controller.rb
|
|
250
225
|
class ServicesController < BugBunny::Controller
|
|
251
|
-
# Callbacks
|
|
252
|
-
before_action :set_service, only:
|
|
226
|
+
# Callbacks estándar
|
|
227
|
+
before_action :set_service, only: [:show, :update]
|
|
253
228
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
render status: 200, json:
|
|
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' }
|
|
257
232
|
end
|
|
258
233
|
|
|
259
|
-
# POST services
|
|
260
234
|
def create
|
|
261
|
-
# BugBunny
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
267
242
|
end
|
|
268
243
|
|
|
269
244
|
private
|
|
270
245
|
|
|
271
246
|
def set_service
|
|
272
|
-
# params[:id] se extrae
|
|
273
|
-
@service =
|
|
274
|
-
|
|
275
|
-
unless @service
|
|
276
|
-
render status: 404, json: { error: "Service not found" }
|
|
277
|
-
end
|
|
247
|
+
# params[:id] se extrae del Path
|
|
248
|
+
@service = Service.find(params[:id])
|
|
278
249
|
end
|
|
279
250
|
end
|
|
280
251
|
```
|
|
281
252
|
|
|
282
|
-
###
|
|
283
|
-
|
|
284
|
-
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.
|
|
285
255
|
|
|
286
256
|
```ruby
|
|
287
|
-
# app/rabbit/controllers/application.rb
|
|
288
257
|
class ApplicationController < BugBunny::Controller
|
|
289
|
-
|
|
290
|
-
rescue_from ActiveRecord::RecordNotFound do
|
|
258
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
291
259
|
render status: :not_found, json: { error: "Resource missing" }
|
|
292
260
|
end
|
|
293
261
|
|
|
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
262
|
rescue_from StandardError do |e|
|
|
300
263
|
BugBunny.configuration.logger.error(e)
|
|
301
|
-
render status: :internal_server_error, json: { 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
|
|
264
|
+
render status: :internal_server_error, json: { error: "Crash" }
|
|
353
265
|
end
|
|
354
266
|
end
|
|
355
267
|
```
|
|
356
268
|
|
|
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
269
|
---
|
|
377
270
|
|
|
378
|
-
|
|
271
|
+
## 🔎 Observabilidad y Tracing
|
|
379
272
|
|
|
380
|
-
|
|
273
|
+
> **Novedad v3.1:** BugBunny implementa Distributed Tracing nativo.
|
|
381
274
|
|
|
382
|
-
|
|
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.
|
|
275
|
+
El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
|
|
387
276
|
|
|
388
|
-
|
|
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.
|
|
389
279
|
|
|
390
280
|
```text
|
|
391
|
-
[d41d8cd9
|
|
392
|
-
[d41d8cd9
|
|
281
|
+
[d41d8cd9...] [Consumer] Listening on queue...
|
|
282
|
+
[d41d8cd9...] [API] Processing ServicesController#create...
|
|
393
283
|
```
|
|
394
284
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
> **Nota:** No agregues `:uuid` aquí, ya que el Consumidor lo agrega automáticamente.
|
|
285
|
+
### 2. Logs de Negocio (Controller)
|
|
286
|
+
Inyecta contexto rico (Tenant, Usuario, IP) en los logs usando `log_tags`.
|
|
399
287
|
|
|
400
288
|
```ruby
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
'WORKER',
|
|
407
|
-
->(_) { ENV['APP_VERSION'] }
|
|
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]
|
|
408
294
|
]
|
|
409
295
|
end
|
|
410
296
|
```
|
|
411
297
|
|
|
412
|
-
|
|
413
|
-
|
|
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.
|
|
298
|
+
### 3. Inyección en el Productor
|
|
299
|
+
Para que tus logs de Rails y Rabbit coincidan, usa un middleware global:
|
|
421
300
|
|
|
422
301
|
```ruby
|
|
423
|
-
#
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
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)
|
|
433
307
|
end
|
|
434
308
|
end
|
|
435
|
-
```
|
|
436
309
|
|
|
437
|
-
|
|
438
|
-
(
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
)
|
|
310
|
+
BugBunny::Client.prepend(Module.new {
|
|
311
|
+
def initialize(pool:)
|
|
312
|
+
super
|
|
313
|
+
@stack.use CorrelationInjector
|
|
314
|
+
end
|
|
315
|
+
})
|
|
467
316
|
```
|
|
468
317
|
|
|
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
318
|
---
|
|
484
319
|
|
|
485
|
-
##
|
|
486
|
-
|
|
487
|
-
BugBunny desacopla el transporte de la lógica usando headers estándar.
|
|
320
|
+
## 🧵 Guía de Producción
|
|
488
321
|
|
|
489
|
-
|
|
490
|
-
|
|
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.
|
|
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.
|
|
495
324
|
|
|
496
|
-
###
|
|
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.
|
|
497
327
|
|
|
498
|
-
|
|
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).
|
|
499
332
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```
|
|
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`.
|
|
503
335
|
|
|
504
336
|
---
|
|
505
337
|
|