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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +204 -148
- data/Rakefile +10 -6
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +40 -3
- data/lib/bug_bunny/consumer.rb +56 -29
- data/lib/bug_bunny/controller.rb +137 -93
- data/lib/bug_bunny/exception.rb +4 -0
- data/lib/bug_bunny/producer.rb +45 -29
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +176 -138
- data/lib/bug_bunny/session.rb +97 -47
- 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 +109 -0
- metadata +47 -8
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc4ecbe75edaf8811acfe2929b74fb4aa98cb65c61d74aca4f5a1320b40a94b8
|
|
4
|
+
data.tar.gz: c3ed5b1e5a773ab8d80fbecc1b96d4a04a62dd1f0b1b5414a2275f497fcde9ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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,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
|
|
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
|
-
|
|
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
|
-
#
|
|
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 #
|
|
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
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
config.logger.level = Logger::INFO
|
|
79
|
+
# 3. Logging
|
|
80
|
+
config.logger = Rails.logger
|
|
81
|
+
end
|
|
59
82
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
119
|
+
## 🚀 Modo Cliente: Recursos (ORM)
|
|
85
120
|
|
|
86
|
-
|
|
121
|
+
Los recursos son proxies de servicios remotos. Heredan de `BugBunny::Resource`.
|
|
87
122
|
|
|
88
|
-
### 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.
|
|
89
125
|
|
|
90
126
|
```ruby
|
|
91
127
|
# app/models/manager/service.rb
|
|
92
128
|
class Manager::Service < BugBunny::Resource
|
|
93
|
-
#
|
|
94
|
-
self.exchange = '
|
|
95
|
-
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 }
|
|
96
136
|
|
|
97
|
-
#
|
|
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
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
123
|
-
#
|
|
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
|
-
#
|
|
129
|
-
|
|
130
|
-
svc.
|
|
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
|
-
#
|
|
143
|
-
|
|
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
|
-
|
|
187
|
+
### 4. Client Middleware (Interceptores)
|
|
188
|
+
Intercepta peticiones antes de salir hacia RabbitMQ. Ideal para inyectar Auth o Headers.
|
|
146
189
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
205
|
+
## 📡 Modo Servidor: Controladores
|
|
155
206
|
|
|
156
|
-
BugBunny
|
|
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.
|
|
209
|
+
### 1. Ruteo Inteligente
|
|
210
|
+
El consumidor infiere automáticamente la acción:
|
|
159
211
|
|
|
160
|
-
|
|
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:
|
|
226
|
+
# Callbacks estándar
|
|
227
|
+
before_action :set_service, only: [:show, :update]
|
|
167
228
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
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' }
|
|
171
232
|
end
|
|
172
233
|
|
|
173
|
-
# POST services
|
|
174
234
|
def create
|
|
175
|
-
# BugBunny
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
187
|
-
@service =
|
|
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
|
-
###
|
|
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
|
-
|
|
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: "
|
|
264
|
+
render status: :internal_server_error, json: { error: "Crash" }
|
|
216
265
|
end
|
|
217
266
|
end
|
|
218
267
|
```
|
|
219
268
|
|
|
220
|
-
|
|
269
|
+
---
|
|
221
270
|
|
|
222
|
-
|
|
271
|
+
## 🔎 Observabilidad y Tracing
|
|
223
272
|
|
|
224
|
-
|
|
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
|
-
|
|
280
|
+
```text
|
|
281
|
+
[d41d8cd9...] [Consumer] Listening on queue...
|
|
282
|
+
[d41d8cd9...] [API] Processing ServicesController#create...
|
|
283
|
+
```
|
|
236
284
|
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
310
|
+
BugBunny::Client.prepend(Module.new {
|
|
311
|
+
def initialize(pool:)
|
|
312
|
+
super
|
|
313
|
+
@stack.use CorrelationInjector
|
|
314
|
+
end
|
|
315
|
+
})
|
|
316
|
+
```
|
|
264
317
|
|
|
265
|
-
|
|
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
|
-
|
|
320
|
+
## 🧵 Guía de Producción
|
|
273
321
|
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rake/testtask'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
task default: :rubocop
|
|
12
|
+
task default: :test
|
data/lib/bug_bunny/client.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
#
|
|
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
|
|
74
|
-
req.body
|
|
75
|
-
req.exchange
|
|
76
|
-
req.exchange_type
|
|
77
|
-
req.routing_key
|
|
78
|
-
req.timeout
|
|
79
|
-
|
|
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
|