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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67cd206fe5bbd998fd71e143d5b22972d841439115209a007223b776630ebf46
4
- data.tar.gz: 6647fc999934cc49b636f32e50f8cdad8c21effd4005a8ff403b233498ee75af
3
+ metadata.gz: 3b9386a291d7b4a4b674279d75e3a10b9b89b184daa8992150fab59a873dd055
4
+ data.tar.gz: e5e0db15d748fc4fee5deb06fcabc7083555b5a90803b7b7e1e916fc4efd99d2
5
5
  SHA512:
6
- metadata.gz: 1cc705a67bef35d982a2c2ed1117c70fc7dc14f79a6a4b9b70e8e7549f0d6db860070e0c6b85e859ef7239f7a322988d8d5054a0ef88e6a363faf911a87dd9d7
7
- data.tar.gz: 7ba4187d186d391fb9e8b42da9d1e7c0a1656f059421a6249c8c3c2c45f1fcdeddf5070a0845245d0371e17250df0a83f674cf77eb188794e95d95e79103a9e3
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 **RESTful familiar** para desarrolladores Rails. Envía mensajes como si estuvieras usando Active Record y procésalos como si fueran Controladores de Rails, apoyado por un potente motor de enrutamiento declarativo.
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
- ## 📖 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)
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 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
- ## 💡 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, evalúa tu mapa de rutas y ejecuta `UsersController#create`.
37
-
38
- ---
20
+ ## 🚀 Instalación
39
21
 
40
- ## 📦 Instalación
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', '~> 4.0'
25
+ gem 'bug_bunny'
46
26
  ```
47
27
 
48
- Ejecuta el bundle e instala los archivos base:
28
+ Y luego ejecuta:
29
+ ```bash
30
+ $ bundle install
31
+ ```
49
32
 
33
+ O instálalo manualmente:
50
34
  ```bash
51
- bundle install
52
- rails g bug_bunny:install
35
+ $ gem install bug_bunny
53
36
  ```
54
37
 
55
- Esto genera:
56
- 1. `config/initializers/bug_bunny.rb`
57
- 2. `app/rabbit/controllers/`
38
+ Luego, genera el inicializador (en Rails):
39
+ ```bash
40
+ $ rails generate bug_bunny:install
41
+ ```
58
42
 
59
43
  ---
60
44
 
61
- ## ⚙️ Configuración Inicial
45
+ ## 🛠️ Configuración
62
46
 
63
- Para entornos productivos (Puma/Sidekiq), es **obligatorio** configurar un Pool de conexiones.
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
- # 1. Credenciales
70
- config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
71
- config.username = ENV.fetch('RABBITMQ_USER', 'guest')
72
- config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
73
- config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
74
-
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 # Base del backoff de reconexión (segundos)
78
- config.max_reconnect_interval = 60 # Techo del backoff exponencial (segundos)
79
- config.max_reconnect_attempts = nil # nil = reintenta infinitamente; Integer = falla hard
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
- ## 🏗️ Configuración de Infraestructura en Cascada
69
+ ## 📦 Uso como Modelo (Consumer + Producer)
101
70
 
102
- BugBunny utiliza 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:
71
+ BugBunny permite definir modelos que representan recursos en otros microservicios.
103
72
 
104
- 1. **Defaults de la Gema:** Rápidos y efímeros (`durable: false`).
105
- 2. **Configuración Global:** Definida en el inicializador para todo el entorno.
106
- 3. **Configuración de Recurso:** Atributos de clase en modelos específicos.
107
- 4. **Configuración al Vuelo:** Parámetros pasados en la llamada `.with` o en el Cliente manual.
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
- **Ejemplo de Configuración Global (Nivel 2):**
110
- Útil para hacer que todos los recursos en el entorno de pruebas sean auto-borrables.
79
+ # Atributos (ActiveRecord style)
80
+ attribute :name, :string
81
+ attribute :status, :string
82
+ attribute :cpu_cores, :integer
111
83
 
112
- ```ruby
113
- # config/initializers/bug_bunny.rb
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
- ## 🚀 Modo Cliente: Recursos (ORM)
96
+ ## 🛣️ Enrutamiento y Controladores (Server side)
125
97
 
126
- Los recursos son proxies de servicios remotos. Heredan de `BugBunny::Resource`.
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
- # app/models/manager/service.rb
133
- class Manager::Service < BugBunny::Resource
134
- # Configuración de Transporte
135
- self.exchange = 'cluster_events'
136
- self.exchange_type = 'topic'
137
-
138
- # Configuración de Infraestructura Específica (Nivel 3)
139
- # Este recurso crítico sobrevivirá a reinicios del servidor RabbitMQ
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
- # A. Atributos Tipados (Opcional, para casting)
146
- attribute :created_at, :datetime
147
- attribute :replicas, :integer, default: 1
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
- # B. Validaciones (Funcionan en ambos tipos)
150
- validates :name, presence: true
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
- ### 2. CRUD y Consultas RESTful
129
+ ---
155
130
 
156
- ```ruby
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
- ### 3. Contexto Dinámico (`.with`)
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
- # Nivel 4: Configuración al vuelo. Inyectamos opciones solo para esta llamada.
186
- Manager::Service.with(
187
- routing_key: 'high_priority',
188
- exchange_options: { durable: false } # Ignora el durable: true de la clase
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
- # config/initializers/bug_bunny_custom_errors.rb
253
- module CustomBugBunnyErrors
254
- def on_complete(response)
255
- status = response['status'].to_i
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
- ## 📡 Modo Servidor: Controladores
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
- ```ruby
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
- BugBunny.routes.draw do
285
- # 1. Colecciones Básicas y Filtrado
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
- # 2. Rutas Anidadas (Member y Collection)
290
- resources :nodes, except: [:create, :destroy] do
291
- # Member inyecta el parámetro :id (ej. PUT nodes/:id/drain)
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
- # Collection opera sobre el conjunto (ej. GET nodes/stats)
298
- collection do
299
- get :stats
300
- end
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
- # 3. Rutas estáticas (Colecciones o Acciones Custom)
304
- get 'health_checks/up', to: 'health_checks#up'
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
- # 4. Extracción automática de variables dinámicas profundas
307
- get 'api/v1/clusters/:cluster_id/nodes/:node_id/metrics', to: 'api/v1/metrics#show'
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
- ### 2. El Controlador
312
- Ubicación: `app/rabbit/controllers/`.
313
- Los parámetros declarados en el archivo de rutas (como `:id` o `:cluster_id`) estarán disponibles automáticamente dentro del hash `params` de tu controlador.
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
- def set_service
338
- # params[:id] es extraído e inyectado por el BugBunny.routes
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
- BugBunny.configuration.logger.error(e)
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).
@@ -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
- BugBunny.configuration.logger.info("component=bug_bunny event=consumer_start queue=#{queue_name} queue_opts=#{final_q_opts}")
81
- BugBunny.configuration.logger.info("component=bug_bunny event=consumer_bound exchange=#{exchange_name} exchange_type=#{exchange_type} routing_key=#{routing_key} exchange_opts=#{final_x_opts}")
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
- BugBunny.configuration.logger.error { "component=bug_bunny event=reconnect_exhausted max_attempts_count=#{max_attempts} error_message=#{e.message.inspect}" }
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
- BugBunny.configuration.logger.error { "component=bug_bunny event=connection_error error_message=#{e.message.inspect} attempt_count=#{attempt} max_attempts_count=#{max_attempts || 'infinity'} retry_in_s=#{wait}" }
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
- BugBunny.configuration.logger.error('component=bug_bunny event=message_rejected reason=missing_type_header')
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
- BugBunny.configuration.logger.info("component=bug_bunny event=message_received method=#{http_method} path=#{path} routing_key=#{delivery_info.routing_key}")
145
- BugBunny.configuration.logger.debug { "component=bug_bunny event=message_received_body body=#{body.truncate(200).inspect}" }
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
- BugBunny.configuration.logger.warn("component=bug_bunny event=route_not_found method=#{http_method} path=#{uri.path}")
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
- BugBunny.configuration.logger.warn("component=bug_bunny event=controller_not_found controller=#{controller_class_name}")
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
- BugBunny.configuration.logger.error("component=bug_bunny event=security_violation reason=invalid_controller controller=#{controller_class}")
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
- BugBunny.configuration.logger.debug { "component=bug_bunny event=route_matched controller=#{controller_class_name} action=#{route_info[:action]}" }
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
- duration_s = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(6)
221
- BugBunny.configuration.logger.info("component=bug_bunny event=message_processed status=#{response_payload[:status]} duration_s=#{duration_s} controller=#{controller_class_name} action=#{route_info[:action]}")
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
- duration_s = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(6)
225
- BugBunny.configuration.logger.error { "component=bug_bunny event=execution_error error_class=#{e.class} error_message=#{e.message.inspect} duration_s=#{duration_s}" }
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
- BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_reply reply_to=#{reply_to} correlation_id=#{correlation_id}" }
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
- BugBunny.configuration.logger.warn("component=bug_bunny event=health_check_failed queue=#{q_name} error_message=#{e.message.inspect}")
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
- BugBunny.configuration.logger.error("component=bug_bunny event=health_check_file_error path=#{file_path} error_message=#{e.message.inspect}")
307
+ safe_log(:error, "consumer.health_check_file_error", path: file_path, **exception_metadata(e))
303
308
  end
304
309
  end
305
310
  end
@@ -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
- BugBunny.configuration.logger.error { "component=bug_bunny event=unhandled_exception error_class=#{exception.class} error_message=#{exception.message.inspect}" }
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
@@ -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
- BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_waiting correlation_id=#{cid} timeout_s=#{wait_timeout}" }
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
- BugBunny.configuration.logger.info("component=bug_bunny event=publish method=#{verb} path=#{target} routing_key=#{rk} correlation_id=#{id}")
110
- BugBunny.configuration.logger.debug { "component=bug_bunny event=publish_detail exchange=#{request.exchange} exchange_opts=#{final_x_opts}" }
111
- BugBunny.configuration.logger.debug { "component=bug_bunny event=publish_payload payload=#{payload.truncate(300).inspect}" } if payload.is_a?(String)
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
- BugBunny.configuration.logger.debug { 'component=bug_bunny event=reply_listener_start queue=amq.rabbitmq.reply-to' }
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
- BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_response_received correlation_id=#{cid}" }
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
- BugBunny.configuration.logger.warn("component=bug_bunny event=rpc_response_orphaned correlation_id=#{cid}")
157
+ safe_log(:warn, "producer.rpc_response_orphaned", correlation_id: cid)
155
158
  end
156
159
  end
157
160
  @reply_listener_started = true
@@ -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
- BugBunny.configuration.logger.warn('component=bug_bunny event=reconnect_attempt')
130
+ safe_log(:warn, "session.reconnect_attempt")
129
131
  @connection.start
130
132
  rescue StandardError => e
131
- BugBunny.configuration.logger.error { "component=bug_bunny event=reconnect_failed error_message=#{e.message.inspect}" }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.3.0"
4
+ VERSION = "4.4.0"
5
5
  end
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
- configuration.logger.info('component=bug_bunny event=disconnect')
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.3.0
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