bug_bunny 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +110 -358
- data/lib/bug_bunny/consumer.rb +24 -19
- data/lib/bug_bunny/controller.rb +3 -2
- data/lib/bug_bunny/observability.rb +71 -0
- data/lib/bug_bunny/producer.rb +10 -7
- data/lib/bug_bunny/session.rb +4 -2
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +7 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b9386a291d7b4a4b674279d75e3a10b9b89b184daa8992150fab59a873dd055
|
|
4
|
+
data.tar.gz: e5e0db15d748fc4fee5deb06fcabc7083555b5a90803b7b7e1e916fc4efd99d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57835a7bd1b1b437c655754c70cb5e3ddd38466f64a527317b82ddec47c524c67ad9c821758698b5b553d03f484dcbff5608faea4eb2dadc96975387044236d9
|
|
7
|
+
data.tar.gz: b0a435d8c309f8bbd7f9ac748d26c6961f718955ce09d06753c13e2b23ce77877510f6aa84702df315f4b012fc52f4e7910a7905695c2438c7fae396821324da
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.4.0] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### 📈 Standard Observability Pattern
|
|
6
|
+
* **Unified Observability Module:** Adopción del patrón de observabilidad estándar para todas las gemas.
|
|
7
|
+
* **Semantic Event Naming:** Todos los eventos ahora siguen el formato `clase.evento` (ej: `consumer.message_processed`, `producer.publish`) para una mejor categorización.
|
|
8
|
+
* **Resilient Logging:** Implementación de `@logger` en cada componente con el método `safe_log` para garantizar que la telemetría nunca interrumpa la ejecución principal.
|
|
9
|
+
* **Full Documentation Sync:** Actualización del README con los nuevos ejemplos de uso del patrón de observabilidad.
|
|
10
|
+
|
|
3
11
|
## [4.3.0] - 2026-03-24
|
|
4
12
|
|
|
5
13
|
### 📈 Observability Alignment (ExisRay Standards)
|
data/README.md
CHANGED
|
@@ -4,201 +4,140 @@
|
|
|
4
4
|
|
|
5
5
|
**Active Record over AMQP.**
|
|
6
6
|
|
|
7
|
-
BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en una arquitectura
|
|
7
|
+
BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en una arquitectura familiar RESTful. Permite que tus microservicios se comuniquen como si fueran APIs locales, utilizando controladores, rutas y modelos con una interfaz idéntica a Active Record.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## ✨ Características
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 Declarativo (Rutas)](#1-ruteo-declarativo-rutas)
|
|
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)
|
|
11
|
+
* **RESTful Routing:** Define tus endpoints AMQP con un DSL estilo Rails (`get`, `post`, `resources`).
|
|
12
|
+
* **Active Record Pattern:** Modela tus recursos remotos con validaciones, callbacks y tracking de cambios.
|
|
13
|
+
* **Middleware Stack:** Arquitectura de cebolla (Onion) para interceptar peticiones, manejar errores y transformar payloads.
|
|
14
|
+
* **RPC & Pub/Sub:** Soporta nativamente tanto peticiones síncronas (Request-Response) como publicaciones asíncronas.
|
|
15
|
+
* **Observabilidad de Clase Mundial:** Integración nativa con **ExisRay** (Tracing distribuido y Logs estructurados KV).
|
|
16
|
+
* **Resiliencia Enterprise:** Reconexión automática con Backoff Exponencial y Health Checks automáticos.
|
|
27
17
|
|
|
28
18
|
---
|
|
29
19
|
|
|
30
|
-
##
|
|
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, evalúa tu mapa de rutas y ejecuta `UsersController#create`.
|
|
37
|
-
|
|
38
|
-
---
|
|
20
|
+
## 🚀 Instalación
|
|
39
21
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Agrega la gema a tu `Gemfile`:
|
|
22
|
+
Añade esta línea al `Gemfile` de tu aplicación:
|
|
43
23
|
|
|
44
24
|
```ruby
|
|
45
|
-
gem 'bug_bunny'
|
|
25
|
+
gem 'bug_bunny'
|
|
46
26
|
```
|
|
47
27
|
|
|
48
|
-
|
|
28
|
+
Y luego ejecuta:
|
|
29
|
+
```bash
|
|
30
|
+
$ bundle install
|
|
31
|
+
```
|
|
49
32
|
|
|
33
|
+
O instálalo manualmente:
|
|
50
34
|
```bash
|
|
51
|
-
|
|
52
|
-
rails g bug_bunny:install
|
|
35
|
+
$ gem install bug_bunny
|
|
53
36
|
```
|
|
54
37
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
38
|
+
Luego, genera el inicializador (en Rails):
|
|
39
|
+
```bash
|
|
40
|
+
$ rails generate bug_bunny:install
|
|
41
|
+
```
|
|
58
42
|
|
|
59
43
|
---
|
|
60
44
|
|
|
61
|
-
##
|
|
45
|
+
## 🛠️ Configuración
|
|
62
46
|
|
|
63
|
-
|
|
47
|
+
Configura la conexión a RabbitMQ y las opciones globales:
|
|
64
48
|
|
|
65
49
|
```ruby
|
|
66
50
|
# config/initializers/bug_bunny.rb
|
|
67
|
-
|
|
68
51
|
BugBunny.configure do |config|
|
|
69
|
-
|
|
70
|
-
config.
|
|
71
|
-
config.username =
|
|
72
|
-
config.password =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
config.
|
|
77
|
-
config.network_recovery_interval = 5
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# 3. Health Checks (Opcional, para Docker Swarm / K8s)
|
|
82
|
-
config.health_check_file = '/tmp/bug_bunny_health'
|
|
83
|
-
|
|
84
|
-
# 4. Logging
|
|
85
|
-
config.logger = Rails.logger
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# 5. Connection Pool (CRÍTICO para concurrencia)
|
|
89
|
-
# Define un pool global para compartir conexiones entre hilos
|
|
90
|
-
BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
|
|
91
|
-
BugBunny.create_connection
|
|
52
|
+
config.host = ENV['RABBITMQ_HOST'] || '127.0.0.1'
|
|
53
|
+
config.port = 5672
|
|
54
|
+
config.username = 'guest'
|
|
55
|
+
config.password = 'guest'
|
|
56
|
+
|
|
57
|
+
# Resiliencia
|
|
58
|
+
config.max_reconnect_attempts = 10 # Falla tras 10 intentos (útil en K8s)
|
|
59
|
+
config.max_reconnect_interval = 60 # Máximo 60s entre reintentos
|
|
60
|
+
config.network_recovery_interval = 5 # Intervalo base para backoff
|
|
61
|
+
|
|
62
|
+
# Infraestructura por defecto (Nivel 2)
|
|
63
|
+
config.exchange_options = { durable: true }
|
|
92
64
|
end
|
|
93
|
-
|
|
94
|
-
# Inyecta el pool en los recursos
|
|
95
|
-
BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
96
65
|
```
|
|
97
66
|
|
|
98
67
|
---
|
|
99
68
|
|
|
100
|
-
##
|
|
69
|
+
## 📦 Uso como Modelo (Consumer + Producer)
|
|
101
70
|
|
|
102
|
-
BugBunny
|
|
71
|
+
BugBunny permite definir modelos que representan recursos en otros microservicios.
|
|
103
72
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
73
|
+
```ruby
|
|
74
|
+
class RemoteNode < BugBunny::Resource
|
|
75
|
+
# Configuración del canal
|
|
76
|
+
self.exchange = 'inventory_exchange'
|
|
77
|
+
self.resource_name = 'nodes' # Equivale al path de la URL
|
|
108
78
|
|
|
109
|
-
|
|
110
|
-
|
|
79
|
+
# Atributos (ActiveRecord style)
|
|
80
|
+
attribute :name, :string
|
|
81
|
+
attribute :status, :string
|
|
82
|
+
attribute :cpu_cores, :integer
|
|
111
83
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
BugBunny.configure do |config|
|
|
115
|
-
if Rails.env.test?
|
|
116
|
-
config.exchange_options = { auto_delete: true }
|
|
117
|
-
config.queue_options = { auto_delete: true }
|
|
118
|
-
end
|
|
84
|
+
# Validaciones
|
|
85
|
+
validates :name, presence: true
|
|
119
86
|
end
|
|
87
|
+
|
|
88
|
+
# Uso:
|
|
89
|
+
node = RemoteNode.find('node-123')
|
|
90
|
+
node.status = 'active'
|
|
91
|
+
node.save # Realiza un PUT a inventory_exchange con routing_key 'nodes.node-123'
|
|
120
92
|
```
|
|
121
93
|
|
|
122
94
|
---
|
|
123
95
|
|
|
124
|
-
##
|
|
96
|
+
## 🛣️ Enrutamiento y Controladores (Server side)
|
|
125
97
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
### 1. Definición y Atributos Híbridos
|
|
129
|
-
BugBunny es **Schema-less**. Soporta atributos tipados (ActiveModel) y dinámicos simultáneamente, además de definir su propia infraestructura.
|
|
98
|
+
Define cómo debe responder tu aplicación a los mensajes entrantes:
|
|
130
99
|
|
|
131
100
|
```ruby
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self.exchange_options = { durable: true, auto_delete: false }
|
|
141
|
-
|
|
142
|
-
# Configuración de Ruteo (La "URL" base)
|
|
143
|
-
self.resource_name = 'services'
|
|
101
|
+
# config/rabbit_routes.rb
|
|
102
|
+
BugBunny.routes.draw do
|
|
103
|
+
resources :nodes do
|
|
104
|
+
member do
|
|
105
|
+
put :drain
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
144
109
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
110
|
+
# app/rabbit/controllers/nodes_controller.rb
|
|
111
|
+
module Rabbit
|
|
112
|
+
module Controllers
|
|
113
|
+
class NodesController < BugBunny::Controller
|
|
114
|
+
def index
|
|
115
|
+
nodes = Node.all # Lógica local de tu app
|
|
116
|
+
render status: :ok, json: nodes
|
|
117
|
+
end
|
|
148
118
|
|
|
149
|
-
|
|
150
|
-
|
|
119
|
+
def drain
|
|
120
|
+
# El ID viene automáticamente en params[:id]
|
|
121
|
+
Node.find(params[:id]).start_drain_process!
|
|
122
|
+
render status: :accepted, json: { message: "Draining started" }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
151
126
|
end
|
|
152
127
|
```
|
|
153
128
|
|
|
154
|
-
|
|
129
|
+
---
|
|
155
130
|
|
|
156
|
-
|
|
157
|
-
# --- LEER (GET) ---
|
|
158
|
-
# RPC: Espera respuesta del worker.
|
|
159
|
-
# Envia: GET services/123
|
|
160
|
-
service = Manager::Service.find('123')
|
|
161
|
-
|
|
162
|
-
# --- BÚSQUEDAS AVANZADAS ---
|
|
163
|
-
# Soporta Hashes anidados para filtros complejos.
|
|
164
|
-
# Envia: GET services?q[status]=active&q[tags][]=web
|
|
165
|
-
Manager::Service.where(q: { status: 'active', tags: ['web'] })
|
|
166
|
-
|
|
167
|
-
# --- CREAR (POST) ---
|
|
168
|
-
# RPC: Envía payload y espera el objeto persistido.
|
|
169
|
-
# Payload: { "service": { "name": "nginx", "replicas": 3 } }
|
|
170
|
-
svc = Manager::Service.create(name: 'nginx', replicas: 3)
|
|
171
|
-
|
|
172
|
-
# --- ACTUALIZAR (PUT) ---
|
|
173
|
-
# Dirty Tracking: Solo envía los campos que cambiaron.
|
|
174
|
-
svc.name = 'nginx-pro'
|
|
175
|
-
svc.save
|
|
176
|
-
|
|
177
|
-
# --- ELIMINAR (DELETE) ---
|
|
178
|
-
svc.destroy
|
|
179
|
-
```
|
|
131
|
+
## 🔌 Middlewares
|
|
180
132
|
|
|
181
|
-
|
|
182
|
-
Puedes sobrescribir la configuración de enrutamiento o infraestructura para una ejecución específica sin afectar al modelo global (Thread-Safe).
|
|
133
|
+
Puedes extender el comportamiento del cliente globalmente o por recurso:
|
|
183
134
|
|
|
184
135
|
```ruby
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
).create(name: 'redis_temp')
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### 4. Client Middleware (Interceptores)
|
|
193
|
-
Intercepta peticiones de ida y respuestas de vuelta en la arquitectura del cliente.
|
|
194
|
-
|
|
195
|
-
**Middlewares Incluidos (Built-ins)**
|
|
196
|
-
Si usas `BugBunny::Resource`, el manejo de JSON y de errores ya está integrado automáticamente. Pero si utilizas el cliente manual (`BugBunny::Client`), puedes inyectar los middlewares incluidos para no tener que parsear respuestas manualmente:
|
|
197
|
-
|
|
198
|
-
* `BugBunny::Middleware::JsonResponse`: Parsea automáticamente el cuerpo de la respuesta de JSON a un Hash de Ruby.
|
|
199
|
-
* `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.).
|
|
136
|
+
# Globalmente en el inicializador
|
|
137
|
+
BugBunny.configure do |config|
|
|
138
|
+
# ...
|
|
139
|
+
end
|
|
200
140
|
|
|
201
|
-
```ruby
|
|
202
141
|
# Uso con el cliente manual
|
|
203
142
|
client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |stack|
|
|
204
143
|
stack.use BugBunny::Middleware::RaiseError
|
|
@@ -230,116 +169,47 @@ response = client.request('users/1', method: :get)
|
|
|
230
169
|
```
|
|
231
170
|
|
|
232
171
|
**Middlewares Personalizados**
|
|
233
|
-
Ideales para inyectar Auth o Headers de trazabilidad en todos los requests de un Recurso.
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
class Manager::Service < BugBunny::Resource
|
|
237
|
-
client_middleware do |stack|
|
|
238
|
-
stack.use(Class.new(BugBunny::Middleware::Base) do
|
|
239
|
-
def on_request(env)
|
|
240
|
-
env.headers['Authorization'] = "Bearer #{ENV['API_TOKEN']}"
|
|
241
|
-
env.headers['X-App-Version'] = '1.0.0'
|
|
242
|
-
end
|
|
243
|
-
end)
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Personalización Avanzada de Errores**
|
|
249
|
-
Si en tu aplicación necesitas mapear códigos HTTP de negocio (ej. `402 Payment Required`) a excepciones personalizadas, la forma más limpia es usar `Module#prepend` sobre el middleware nativo en un inicializador. De esta forma inyectas tus reglas sin perder el comportamiento por defecto para los demás errores:
|
|
250
172
|
|
|
251
173
|
```ruby
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# 1. Reglas específicas de tu negocio
|
|
258
|
-
if status == 402
|
|
259
|
-
raise MyApp::PaymentRequiredError, response['body']['message']
|
|
260
|
-
elsif status == 403 && response['body']['reason'] == 'ip_blocked'
|
|
261
|
-
raise MyApp::IpBlockedError, response['body']['detail']
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# 2. Delegar el resto de los errores (404, 422, 500) al middleware original
|
|
265
|
-
super(response)
|
|
174
|
+
class MyCustomMiddleware < BugBunny::Middleware::Base
|
|
175
|
+
def call(request)
|
|
176
|
+
puts "Enviando mensaje a: #{request.path}"
|
|
177
|
+
app.call(request)
|
|
266
178
|
end
|
|
267
179
|
end
|
|
268
|
-
|
|
269
|
-
BugBunny::Middleware::RaiseError.prepend(CustomBugBunnyErrors)
|
|
270
180
|
```
|
|
271
181
|
|
|
272
182
|
---
|
|
273
183
|
|
|
274
|
-
##
|
|
275
|
-
|
|
276
|
-
A partir de BugBunny v4, el enrutamiento es **declarativo** y explícito, al igual que en Rails. Se utiliza un archivo de rutas centralizado para mapear los mensajes AMQP entrantes a los Controladores adecuados.
|
|
277
|
-
|
|
278
|
-
### 1. Ruteo Declarativo (Rutas)
|
|
279
|
-
Crea un inicializador en tu aplicación (ej. `config/initializers/bug_bunny_routes.rb`) para definir tu mapa de rutas. BugBunny usará este DSL para extraer automáticamente parámetros dinámicos de las URLs.
|
|
184
|
+
## 🔎 Observabilidad y Tracing
|
|
280
185
|
|
|
281
|
-
|
|
282
|
-
# config/initializers/bug_bunny_routes.rb
|
|
186
|
+
BugBunny implementa Distributed Tracing nativo y sigue los estándares de observabilidad de **ExisRay** para logs estructurados.
|
|
283
187
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# Genera rutas para index, show y update únicamente
|
|
287
|
-
resources :services, only: [:index, :show, :update]
|
|
188
|
+
### 1. Logs Estructurados (Key-Value)
|
|
189
|
+
A partir de la v4.3.0, todos los logs internos de la gema utilizan un formato `key=value` optimizado para motores de logs (CloudWatch, Datadog, ELK).
|
|
288
190
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
member do
|
|
293
|
-
put :drain
|
|
294
|
-
post :restart
|
|
295
|
-
end
|
|
191
|
+
* **Data First:** Las unidades están en la llave (`_s`, `_ms`, `_count`), permitiendo que los valores sean números puros para agregaciones automáticas.
|
|
192
|
+
* **Reloj Monotónico:** Las duraciones (`duration_s`) se calculan con precisión de microsegundos usando el reloj monotónico del sistema.
|
|
193
|
+
* **Campos de Identidad:** Todos los logs incluyen `component=bug_bunny` y un `event` semántico.
|
|
296
194
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
end
|
|
195
|
+
**Ejemplos de Logs:**
|
|
196
|
+
```text
|
|
197
|
+
# Mensaje procesado con éxito (incluye duración y status numérico)
|
|
198
|
+
component=bug_bunny event=consumer.message_processed status=200 duration_s=0.015432 controller=UsersController action=show
|
|
302
199
|
|
|
303
|
-
|
|
304
|
-
|
|
200
|
+
# Error de ejecución (campos estandarizados)
|
|
201
|
+
component=bug_bunny event=consumer.execution_error error_class=NoMethodError error_message="undefined method..." duration_s=0.008123
|
|
305
202
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
end
|
|
203
|
+
# Reintento de conexión con backoff (sufijos de unidad)
|
|
204
|
+
component=bug_bunny event=consumer.connection_error error_message="..." attempt_count=3 retry_in_s=20
|
|
309
205
|
```
|
|
310
206
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
```ruby
|
|
316
|
-
class ServicesController < BugBunny::Controller
|
|
317
|
-
# Callbacks estándar
|
|
318
|
-
before_action :set_service, only: [:show, :update]
|
|
319
|
-
|
|
320
|
-
def show
|
|
321
|
-
# Renderiza JSON que viajará de vuelta por la cola reply-to
|
|
322
|
-
render status: 200, json: { id: @service.id, state: 'running' }
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def create
|
|
326
|
-
# BugBunny envuelve los params automáticamente basándose en el resource_name
|
|
327
|
-
# params[:service] => { name: '...', replicas: ... }
|
|
328
|
-
if Service.create(params[:service])
|
|
329
|
-
render status: 201, json: { status: 'created' }
|
|
330
|
-
else
|
|
331
|
-
render status: 422, json: { errors: 'Invalid' }
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
private
|
|
207
|
+
**Ventaja en Cloud:**
|
|
208
|
+
Al usar `duration_s` como un float puro, puedes realizar consultas analíticas directamente en tu motor de logs sin parsear strings:
|
|
209
|
+
`stats avg(duration_s), max(duration_s) by controller, action`
|
|
336
210
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
@service = Service.find(params[:id])
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
```
|
|
211
|
+
### 2. Distributed Tracing
|
|
212
|
+
El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
|
|
343
213
|
|
|
344
214
|
### 3. Manejo de Errores Declarativo
|
|
345
215
|
Captura excepciones y devuélvelas como códigos de estado AMQP/HTTP.
|
|
@@ -351,7 +221,7 @@ class ApplicationController < BugBunny::Controller
|
|
|
351
221
|
end
|
|
352
222
|
|
|
353
223
|
rescue_from StandardError do |e|
|
|
354
|
-
|
|
224
|
+
safe_log(:error, "application.crash", **exception_metadata(e))
|
|
355
225
|
render status: :internal_server_error, json: { error: "Crash" }
|
|
356
226
|
end
|
|
357
227
|
end
|
|
@@ -359,124 +229,6 @@ end
|
|
|
359
229
|
|
|
360
230
|
---
|
|
361
231
|
|
|
362
|
-
## 🔎 Observabilidad y Tracing
|
|
363
|
-
|
|
364
|
-
BugBunny implementa Distributed Tracing nativo. El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
|
|
365
|
-
|
|
366
|
-
### 1. Logs Automáticos (Consumer)
|
|
367
|
-
No requiere configuración. El worker envuelve la ejecución en bloques de log etiquetados con el UUID.
|
|
368
|
-
|
|
369
|
-
```text
|
|
370
|
-
[d41d8cd9...] [BugBunny::Consumer] 📥 Received PUT "/nodes/4bv445vgc158hk" | RK: 'dbu55...'
|
|
371
|
-
[d41d8cd9...] [BugBunny::Consumer] 🎯 Routed to Rabbit::Controllers::NodesController#drain
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### 2. Logs de Negocio (Controller)
|
|
375
|
-
Inyecta contexto rico (Tenant, Usuario, IP) en los logs usando `log_tags`.
|
|
376
|
-
|
|
377
|
-
```ruby
|
|
378
|
-
# app/rabbit/controllers/application_controller.rb
|
|
379
|
-
class ApplicationController < BugBunny::Controller
|
|
380
|
-
self.log_tags = [
|
|
381
|
-
->(c) { c.params[:tenant_id] }, # Agrega [Tenant-55]
|
|
382
|
-
->(c) { c.headers['X-Source'] } # Agrega [Console]
|
|
383
|
-
]
|
|
384
|
-
end
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### 3. Inyección en el Productor
|
|
388
|
-
Para que tus logs de Rails y Rabbit coincidan, usa un middleware global:
|
|
389
|
-
|
|
390
|
-
```ruby
|
|
391
|
-
# config/initializers/bug_bunny.rb
|
|
392
|
-
# Middleware para inyectar Current.request_id de Rails al mensaje Rabbit
|
|
393
|
-
class CorrelationInjector < BugBunny::Middleware::Base
|
|
394
|
-
def on_request(env)
|
|
395
|
-
env.correlation_id = Current.request_id if defined?(Current)
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
BugBunny::Client.prepend(Module.new {
|
|
400
|
-
def initialize(pool:)
|
|
401
|
-
super
|
|
402
|
-
@stack.use CorrelationInjector
|
|
403
|
-
end
|
|
404
|
-
})
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
---
|
|
408
|
-
|
|
409
|
-
## 🧵 Guía de Producción
|
|
410
|
-
|
|
411
|
-
### Connection Pooling
|
|
412
|
-
Es vital usar `ConnectionPool` si usas servidores web multi-hilo (Puma) o workers (Sidekiq). BugBunny no gestiona hilos internamente; se apoya en el pool.
|
|
413
|
-
|
|
414
|
-
### Fork Safety
|
|
415
|
-
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.
|
|
416
|
-
|
|
417
|
-
### RPC y "Direct Reply-To"
|
|
418
|
-
Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
|
|
419
|
-
* **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.
|
|
420
|
-
* **Recomendación:** Diseña tus acciones RPC (`POST`, `PUT`) para que sean **idempotentes** (seguras de reintentar ante un timeout).
|
|
421
|
-
|
|
422
|
-
### Seguridad
|
|
423
|
-
El Router incluye protecciones contra **Remote Code Execution (RCE)**. El Consumer verifica estrictamente que el Controlador resuelto a través del archivo de rutas herede de `BugBunny::Controller` antes de ejecutarla, impidiendo la inyección de clases arbitrarias. Además, las llamadas a rutas no registradas fallan rápido con un `404 Not Found`.
|
|
424
|
-
|
|
425
|
-
### Reconexión con Backoff Exponencial
|
|
426
|
-
|
|
427
|
-
Cuando el Consumer pierde la conexión a RabbitMQ, reintenta automáticamente usando un backoff exponencial basado en `network_recovery_interval`:
|
|
428
|
-
|
|
429
|
-
| Intento | Espera (base = 5s, techo = 60s) |
|
|
430
|
-
|---------|----------------------------------|
|
|
431
|
-
| 1 | 5s |
|
|
432
|
-
| 2 | 10s |
|
|
433
|
-
| 3 | 20s |
|
|
434
|
-
| 4 | 40s |
|
|
435
|
-
| 5+ | 60s (cap) |
|
|
436
|
-
|
|
437
|
-
Por defecto reintenta indefinidamente (`max_reconnect_attempts: nil`). En entornos orquestados (Kubernetes, Docker Swarm), es preferible dejar que el orquestador reinicie el contenedor cuando la infraestructura no está disponible:
|
|
438
|
-
|
|
439
|
-
```ruby
|
|
440
|
-
BugBunny.configure do |config|
|
|
441
|
-
config.network_recovery_interval = 5 # Base del backoff
|
|
442
|
-
config.max_reconnect_interval = 60 # Techo máximo de espera
|
|
443
|
-
config.max_reconnect_attempts = 10 # Falla hard después de 10 intentos
|
|
444
|
-
end
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
Con esta configuración, si RabbitMQ no vuelve en ~10 reintentos el proceso levanta la excepción y el orquestador lo reinicia con su propia política de restart.
|
|
448
|
-
|
|
449
|
-
### Health Checks en Docker Swarm / Kubernetes
|
|
450
|
-
Dado que un Worker se ejecuta en segundo plano sin exponer un servidor web tradicional, orquestadores como Docker Swarm o Kubernetes no pueden usar un endpoint HTTP para verificar si el proceso está saludable.
|
|
451
|
-
|
|
452
|
-
BugBunny implementa el patrón **Touchfile**. Puedes configurar la gema para que actualice la fecha de modificación de un archivo temporal en cada latido exitoso (heartbeat) hacia RabbitMQ.
|
|
453
|
-
|
|
454
|
-
**1. Configurar la gema:**
|
|
455
|
-
```ruby
|
|
456
|
-
# config/initializers/bug_bunny.rb
|
|
457
|
-
BugBunny.configure do |config|
|
|
458
|
-
# Actualizará la fecha de este archivo si la conexión a la cola está sana
|
|
459
|
-
config.health_check_file = '/tmp/bug_bunny_health'
|
|
460
|
-
end
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
**2. Configurar el Orquestador (Ejemplo docker-compose.yml):**
|
|
464
|
-
Con esta configuración, Docker Swarm verificará que el archivo haya sido modificado (tocado) en los últimos 15 segundos. Si el worker se bloquea o pierde la conexión de manera irrecuperable, Docker reiniciará el contenedor automáticamente.
|
|
465
|
-
|
|
466
|
-
```yaml
|
|
467
|
-
services:
|
|
468
|
-
worker:
|
|
469
|
-
image: my_rails_app
|
|
470
|
-
command: bundle exec rake bug_bunny:work
|
|
471
|
-
healthcheck:
|
|
472
|
-
test: ["CMD-SHELL", "test $$(expr $$(date +%s) - $$(stat -c %Y /tmp/bug_bunny_health)) -lt 15 || exit 1"]
|
|
473
|
-
interval: 10s
|
|
474
|
-
timeout: 5s
|
|
475
|
-
retries: 3
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
232
|
## 📄 Licencia
|
|
481
233
|
|
|
482
234
|
Código abierto bajo [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -27,6 +27,8 @@ module BugBunny
|
|
|
27
27
|
# routing_key: 'users.#'
|
|
28
28
|
# )
|
|
29
29
|
class Consumer
|
|
30
|
+
include BugBunny::Observability
|
|
31
|
+
|
|
30
32
|
# @return [BugBunny::Session] La sesión wrapper de RabbitMQ que gestiona el canal.
|
|
31
33
|
attr_reader :session
|
|
32
34
|
|
|
@@ -45,6 +47,7 @@ module BugBunny
|
|
|
45
47
|
def initialize(connection)
|
|
46
48
|
@session = BugBunny::Session.new(connection, publisher_confirms: false)
|
|
47
49
|
@health_timer = nil
|
|
50
|
+
@logger = BugBunny.configuration.logger
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
# Inicia la suscripción a la cola y comienza el bucle de procesamiento.
|
|
@@ -77,8 +80,8 @@ module BugBunny
|
|
|
77
80
|
.merge(BugBunny.configuration.queue_options || {})
|
|
78
81
|
.merge(queue_opts || {})
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
safe_log(:info, "consumer.start", queue: queue_name, queue_opts: final_q_opts)
|
|
84
|
+
safe_log(:info, "consumer.bound", exchange: exchange_name, exchange_type: exchange_type, routing_key: routing_key, exchange_opts: final_x_opts)
|
|
82
85
|
|
|
83
86
|
start_health_check(queue_name)
|
|
84
87
|
|
|
@@ -100,7 +103,7 @@ module BugBunny
|
|
|
100
103
|
max_attempts = BugBunny.configuration.max_reconnect_attempts
|
|
101
104
|
|
|
102
105
|
if max_attempts && attempt >= max_attempts
|
|
103
|
-
|
|
106
|
+
safe_log(:error, "consumer.reconnect_exhausted", max_attempts_count: max_attempts, **exception_metadata(e))
|
|
104
107
|
raise
|
|
105
108
|
end
|
|
106
109
|
|
|
@@ -109,7 +112,7 @@ module BugBunny
|
|
|
109
112
|
BugBunny.configuration.max_reconnect_interval
|
|
110
113
|
].min
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
safe_log(:error, "consumer.connection_error", attempt_count: attempt, max_attempts_count: max_attempts || 'infinity', retry_in_s: wait, **exception_metadata(e))
|
|
113
116
|
sleep wait
|
|
114
117
|
retry
|
|
115
118
|
end
|
|
@@ -132,7 +135,7 @@ module BugBunny
|
|
|
132
135
|
path = properties.type || (properties.headers && properties.headers['path'])
|
|
133
136
|
|
|
134
137
|
if path.nil? || path.empty?
|
|
135
|
-
|
|
138
|
+
safe_log(:error, "consumer.message_rejected", reason: :missing_type_header)
|
|
136
139
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
137
140
|
return
|
|
138
141
|
end
|
|
@@ -141,8 +144,8 @@ module BugBunny
|
|
|
141
144
|
headers_hash = properties.headers || {}
|
|
142
145
|
http_method = (headers_hash['x-http-method'] || headers_hash['method'] || 'GET').to_s.upcase
|
|
143
146
|
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
safe_log(:info, "consumer.message_received", method: http_method, path: path, routing_key: delivery_info.routing_key)
|
|
148
|
+
safe_log(:debug, "consumer.message_received_body", body: body.truncate(200))
|
|
146
149
|
|
|
147
150
|
# ===================================================================
|
|
148
151
|
# 3. Ruteo Declarativo
|
|
@@ -159,7 +162,7 @@ module BugBunny
|
|
|
159
162
|
route_info = BugBunny.routes.recognize(http_method, uri.path)
|
|
160
163
|
|
|
161
164
|
if route_info.nil?
|
|
162
|
-
|
|
165
|
+
safe_log(:warn, "consumer.route_not_found", method: http_method, path: uri.path)
|
|
163
166
|
handle_fatal_error(properties, 404, "Not Found", "No route matches [#{http_method}] \"/#{uri.path}\"")
|
|
164
167
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
165
168
|
return
|
|
@@ -178,7 +181,7 @@ module BugBunny
|
|
|
178
181
|
begin
|
|
179
182
|
controller_class = controller_class_name.constantize
|
|
180
183
|
rescue NameError
|
|
181
|
-
|
|
184
|
+
safe_log(:warn, "consumer.controller_not_found", controller: controller_class_name)
|
|
182
185
|
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
183
186
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
184
187
|
return
|
|
@@ -186,13 +189,13 @@ module BugBunny
|
|
|
186
189
|
|
|
187
190
|
# Verificación estricta de Seguridad (RCE Prevention)
|
|
188
191
|
unless controller_class < BugBunny::Controller
|
|
189
|
-
|
|
192
|
+
safe_log(:error, "consumer.security_violation", reason: :invalid_controller, controller: controller_class)
|
|
190
193
|
handle_fatal_error(properties, 403, "Forbidden", "Invalid Controller Class")
|
|
191
194
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
192
195
|
return
|
|
193
196
|
end
|
|
194
197
|
|
|
195
|
-
|
|
198
|
+
safe_log(:debug, "consumer.route_matched", controller: controller_class_name, action: route_info[:action])
|
|
196
199
|
|
|
197
200
|
request_metadata = {
|
|
198
201
|
type: path,
|
|
@@ -217,13 +220,15 @@ module BugBunny
|
|
|
217
220
|
|
|
218
221
|
session.channel.ack(delivery_info.delivery_tag)
|
|
219
222
|
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
safe_log(:info, "consumer.message_processed",
|
|
224
|
+
status: response_payload[:status],
|
|
225
|
+
duration_s: duration_s(start_time),
|
|
226
|
+
controller: controller_class_name,
|
|
227
|
+
action: route_info[:action])
|
|
222
228
|
|
|
223
229
|
rescue StandardError => e
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
BugBunny.configuration.logger.debug { "component=bug_bunny event=execution_error backtrace=#{e.backtrace.first(5).join(' | ').inspect}" }
|
|
230
|
+
safe_log(:error, "consumer.execution_error", duration_s: duration_s(start_time), **exception_metadata(e))
|
|
231
|
+
safe_log(:debug, "consumer.execution_error_backtrace", backtrace: e.backtrace.first(5).join(' | '))
|
|
227
232
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
228
233
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
229
234
|
end
|
|
@@ -235,7 +240,7 @@ module BugBunny
|
|
|
235
240
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
236
241
|
# @return [void]
|
|
237
242
|
def reply(payload, reply_to, correlation_id)
|
|
238
|
-
|
|
243
|
+
safe_log(:debug, "consumer.rpc_reply", reply_to: reply_to, correlation_id: correlation_id)
|
|
239
244
|
session.channel.default_exchange.publish(
|
|
240
245
|
payload.to_json,
|
|
241
246
|
routing_key: reply_to,
|
|
@@ -284,7 +289,7 @@ module BugBunny
|
|
|
284
289
|
# 2. Si llegamos aquí, RabbitMQ y la cola están vivos. Avisamos al orquestador actualizando el archivo.
|
|
285
290
|
touch_health_file(file_path) if file_path
|
|
286
291
|
rescue StandardError => e
|
|
287
|
-
|
|
292
|
+
safe_log(:warn, "consumer.health_check_failed", queue: q_name, **exception_metadata(e))
|
|
288
293
|
session.close
|
|
289
294
|
end
|
|
290
295
|
@health_timer.execute
|
|
@@ -299,7 +304,7 @@ module BugBunny
|
|
|
299
304
|
def touch_health_file(file_path)
|
|
300
305
|
FileUtils.touch(file_path)
|
|
301
306
|
rescue StandardError => e
|
|
302
|
-
|
|
307
|
+
safe_log(:error, "consumer.health_check_file_error", path: file_path, **exception_metadata(e))
|
|
303
308
|
end
|
|
304
309
|
end
|
|
305
310
|
end
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -19,6 +19,7 @@ module BugBunny
|
|
|
19
19
|
class Controller
|
|
20
20
|
include ActiveModel::Model
|
|
21
21
|
include ActiveModel::Attributes
|
|
22
|
+
include BugBunny::Observability
|
|
22
23
|
|
|
23
24
|
# @!group Atributos de Instancia
|
|
24
25
|
|
|
@@ -128,6 +129,7 @@ module BugBunny
|
|
|
128
129
|
def initialize(attributes = {})
|
|
129
130
|
super
|
|
130
131
|
@response_headers = {}
|
|
132
|
+
@logger = BugBunny.configuration.logger
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
# Punto de entrada principal estático llamado por el Router (`BugBunny::Consumer`).
|
|
@@ -204,8 +206,7 @@ module BugBunny
|
|
|
204
206
|
end
|
|
205
207
|
|
|
206
208
|
# Fallback genérico si la excepción no fue mapeada
|
|
207
|
-
|
|
208
|
-
BugBunny.configuration.logger.error { "component=bug_bunny event=unhandled_exception backtrace=#{exception.backtrace.first(5).join(' | ').inspect}" }
|
|
209
|
+
safe_log(:error, "controller.unhandled_exception", backtrace: exception.backtrace.first(5).join(" | "), **exception_metadata(exception))
|
|
209
210
|
|
|
210
211
|
{
|
|
211
212
|
status: 500,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BugBunny
|
|
4
|
+
# @api private
|
|
5
|
+
module Observability
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Registra un evento estructurado. Nunca eleva excepciones.
|
|
9
|
+
#
|
|
10
|
+
# @param level [Symbol] Nivel de log (:debug, :info, :warn, :error)
|
|
11
|
+
# @param event [String] Nombre del evento en formato "clase.evento"
|
|
12
|
+
# @param metadata [Hash] Pares clave-valor adicionales
|
|
13
|
+
def safe_log(level, event, metadata = {})
|
|
14
|
+
return unless @logger
|
|
15
|
+
|
|
16
|
+
fields = { component: observability_name, event: event }.merge(metadata)
|
|
17
|
+
|
|
18
|
+
log_line = fields.map do |k, v|
|
|
19
|
+
val = %i[password token secret api_key auth].include?(k.to_sym) ? "[FILTERED]" : v
|
|
20
|
+
next if val.nil?
|
|
21
|
+
|
|
22
|
+
formatted = case val
|
|
23
|
+
when Numeric then val
|
|
24
|
+
when String then val.include?(" ") ? val.inspect : val
|
|
25
|
+
else val.to_s.include?(" ") ? val.to_s.inspect : val
|
|
26
|
+
end
|
|
27
|
+
"#{k}=#{formatted}"
|
|
28
|
+
end.compact.join(" ")
|
|
29
|
+
|
|
30
|
+
@logger.send(level) { log_line }
|
|
31
|
+
rescue StandardError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Genera metadatos estándar para una excepción.
|
|
35
|
+
#
|
|
36
|
+
# @param error [Exception] El objeto de error capturado.
|
|
37
|
+
# @return [Hash] Hash con error_class y error_message truncado.
|
|
38
|
+
def exception_metadata(error)
|
|
39
|
+
{
|
|
40
|
+
error_class: error.class.name,
|
|
41
|
+
error_message: error.message.gsub('"', "'")[0, 200]
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Timestamp del reloj monotónico para calcular duraciones.
|
|
46
|
+
#
|
|
47
|
+
# @return [Float] Tiempo actual del reloj monotónico.
|
|
48
|
+
def monotonic_now
|
|
49
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Duración en segundos desde un tiempo de inicio.
|
|
53
|
+
#
|
|
54
|
+
# @param start [Float] Valor devuelto por monotonic_now
|
|
55
|
+
# @return [Float] Duración en segundos redondeada a 6 decimales.
|
|
56
|
+
def duration_s(start)
|
|
57
|
+
(monotonic_now - start).round(6)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Infiere el nombre del componente desde el namespace de la clase.
|
|
61
|
+
# Ejemplo: BugBunny::Consumer → "bug_bunny"
|
|
62
|
+
#
|
|
63
|
+
# @return [String] Nombre del componente en snake_case.
|
|
64
|
+
def observability_name
|
|
65
|
+
klass = is_a?(Class) ? self : self.class
|
|
66
|
+
klass.name.split("::").first.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
67
|
+
rescue StandardError
|
|
68
|
+
"unknown"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -13,6 +13,8 @@ module BugBunny
|
|
|
13
13
|
# 3. Implementar el patrón RPC síncrono utilizando futuros (`Concurrent::IVar`).
|
|
14
14
|
# 4. Gestionar la escucha de respuestas en la cola especial de RabbitMQ.
|
|
15
15
|
class Producer
|
|
16
|
+
include BugBunny::Observability
|
|
17
|
+
|
|
16
18
|
# Inicializa el productor.
|
|
17
19
|
#
|
|
18
20
|
# Prepara las estructuras de concurrencia necesarias para manejar múltiples
|
|
@@ -21,6 +23,7 @@ module BugBunny
|
|
|
21
23
|
# @param session [BugBunny::Session] Sesión activa de Bunny (wrapper).
|
|
22
24
|
def initialize(session)
|
|
23
25
|
@session = session
|
|
26
|
+
@logger = BugBunny.configuration.logger
|
|
24
27
|
# Mapa thread-safe para correlacionar IDs de petición con sus futuros (IVars)
|
|
25
28
|
@pending_requests = Concurrent::Map.new
|
|
26
29
|
@reply_listener_mutex = Mutex.new
|
|
@@ -73,7 +76,7 @@ module BugBunny
|
|
|
73
76
|
begin
|
|
74
77
|
fire(request)
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
safe_log(:debug, "producer.rpc_waiting", correlation_id: cid, timeout_s: wait_timeout)
|
|
77
80
|
|
|
78
81
|
# Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
|
|
79
82
|
response_payload = future.value(wait_timeout)
|
|
@@ -106,9 +109,9 @@ module BugBunny
|
|
|
106
109
|
.merge(BugBunny.configuration.exchange_options || {})
|
|
107
110
|
.merge(request.exchange_options || {})
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
safe_log(:info, "producer.publish", method: verb, path: target, routing_key: rk, correlation_id: id)
|
|
113
|
+
safe_log(:debug, "producer.publish_detail", exchange: request.exchange, exchange_opts: final_x_opts)
|
|
114
|
+
safe_log(:debug, "producer.publish_payload", payload: payload.truncate(300)) if payload.is_a?(String)
|
|
112
115
|
end
|
|
113
116
|
|
|
114
117
|
# Serializa el mensaje para su transporte.
|
|
@@ -142,16 +145,16 @@ module BugBunny
|
|
|
142
145
|
@reply_listener_mutex.synchronize do
|
|
143
146
|
return if @reply_listener_started
|
|
144
147
|
|
|
145
|
-
|
|
148
|
+
safe_log(:debug, "producer.reply_listener_start")
|
|
146
149
|
|
|
147
150
|
# Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
|
|
148
151
|
@session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
|
|
149
152
|
cid = props.correlation_id.to_s
|
|
150
|
-
|
|
153
|
+
safe_log(:debug, "producer.rpc_response_received", correlation_id: cid)
|
|
151
154
|
if (future = @pending_requests[cid])
|
|
152
155
|
future.set(body)
|
|
153
156
|
else
|
|
154
|
-
|
|
157
|
+
safe_log(:warn, "producer.rpc_response_orphaned", correlation_id: cid)
|
|
155
158
|
end
|
|
156
159
|
end
|
|
157
160
|
@reply_listener_started = true
|
data/lib/bug_bunny/session.rb
CHANGED
|
@@ -8,6 +8,7 @@ module BugBunny
|
|
|
8
8
|
#
|
|
9
9
|
# @api private
|
|
10
10
|
class Session
|
|
11
|
+
include BugBunny::Observability
|
|
11
12
|
# @!group Opciones por Defecto (Nivel 1: Gema)
|
|
12
13
|
|
|
13
14
|
# Opciones predeterminadas de la gema para Exchanges.
|
|
@@ -31,6 +32,7 @@ module BugBunny
|
|
|
31
32
|
@connection = connection
|
|
32
33
|
@publisher_confirms = publisher_confirms
|
|
33
34
|
@channel = nil
|
|
35
|
+
@logger = BugBunny.configuration.logger
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
# Obtiene el canal actual o crea uno nuevo si es necesario.
|
|
@@ -125,10 +127,10 @@ module BugBunny
|
|
|
125
127
|
def ensure_connection!
|
|
126
128
|
return if @connection.open?
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
safe_log(:warn, "session.reconnect_attempt")
|
|
129
131
|
@connection.start
|
|
130
132
|
rescue StandardError => e
|
|
131
|
-
|
|
133
|
+
safe_log(:error, "session.reconnect_failed", **exception_metadata(e))
|
|
132
134
|
raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
|
|
133
135
|
end
|
|
134
136
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'logger'
|
|
|
5
5
|
require_relative 'bug_bunny/version'
|
|
6
6
|
require_relative 'bug_bunny/exception'
|
|
7
7
|
require_relative 'bug_bunny/configuration'
|
|
8
|
+
require_relative 'bug_bunny/observability'
|
|
8
9
|
require_relative 'bug_bunny/routing/route_set'
|
|
9
10
|
require_relative 'bug_bunny/middleware/base'
|
|
10
11
|
require_relative 'bug_bunny/middleware/stack'
|
|
@@ -22,6 +23,9 @@ require_relative 'bug_bunny/railtie' if defined?(Rails)
|
|
|
22
23
|
# Módulo principal de la gema BugBunny.
|
|
23
24
|
# Actúa como espacio de nombres y punto de configuración global.
|
|
24
25
|
module BugBunny
|
|
26
|
+
extend BugBunny::Observability
|
|
27
|
+
private_class_method :safe_log, :exception_metadata, :observability_name
|
|
28
|
+
|
|
25
29
|
class << self
|
|
26
30
|
# @return [BugBunny::Configuration] La configuración global actual.
|
|
27
31
|
attr_accessor :configuration
|
|
@@ -83,7 +87,9 @@ module BugBunny
|
|
|
83
87
|
|
|
84
88
|
@global_connection.close if @global_connection.open?
|
|
85
89
|
@global_connection = nil
|
|
86
|
-
|
|
90
|
+
|
|
91
|
+
@logger = configuration.logger
|
|
92
|
+
safe_log(:info, "bug_bunny.disconnect")
|
|
87
93
|
end
|
|
88
94
|
|
|
89
95
|
# @api private
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
@@ -243,6 +243,7 @@ files:
|
|
|
243
243
|
- lib/bug_bunny/middleware/json_response.rb
|
|
244
244
|
- lib/bug_bunny/middleware/raise_error.rb
|
|
245
245
|
- lib/bug_bunny/middleware/stack.rb
|
|
246
|
+
- lib/bug_bunny/observability.rb
|
|
246
247
|
- lib/bug_bunny/producer.rb
|
|
247
248
|
- lib/bug_bunny/railtie.rb
|
|
248
249
|
- lib/bug_bunny/request.rb
|