bug_bunny 4.8.0 → 4.9.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/.agents/skills/documentation-writer/SKILL.md +45 -0
- data/.agents/skills/gem-release/SKILL.md +116 -0
- data/.agents/skills/quality-code/SKILL.md +51 -0
- data/.agents/skills/sentry/SKILL.md +135 -0
- data/.agents/skills/sentry/references/api-endpoints.md +147 -0
- data/.agents/skills/sentry/scripts/sentry.rb +194 -0
- data/.agents/skills/skill-builder/SKILL.md +293 -0
- data/.agents/skills/skill-manager/SKILL.md +225 -0
- data/.agents/skills/skill-manager/scripts/sync.rb +356 -0
- data/.agents/skills/yard/SKILL.md +311 -0
- data/.agents/skills/yard/references/tipos.md +144 -0
- data/CHANGELOG.md +14 -0
- data/CLAUDE.md +28 -225
- data/README.md +5 -3
- data/lib/bug_bunny/consumer.rb +21 -5
- data/lib/bug_bunny/otel.rb +47 -0
- data/lib/bug_bunny/producer.rb +13 -4
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -0
- data/skill/SKILL.md +253 -0
- data/skill/references/client-middleware.md +161 -0
- data/skill/references/consumer.md +122 -0
- data/skill/references/controller.md +105 -0
- data/skill/references/errores.md +97 -0
- data/skill/references/resource.md +116 -0
- data/skill/references/routing.md +82 -0
- data/skill/references/testing.md +138 -0
- data/skills.lock +30 -0
- data/skills.yml +40 -0
- data/spec/integration/consumer_middleware_spec.rb +23 -2
- data/spec/unit/consumer_spec.rb +138 -6
- data/spec/unit/otel_spec.rb +54 -0
- data/spec/unit/producer_spec.rb +187 -0
- data/spec/unit/request_spec.rb +51 -0
- metadata +28 -29
- data/.agents/skills/rabbitmq-expert/SKILL.md +0 -1555
- data/.claude/commands/gem-ai-setup.md +0 -174
- data/.claude/commands/pr.md +0 -53
- data/.claude/commands/release.md +0 -52
- data/.claude/commands/rubocop.md +0 -22
- data/.claude/commands/service-ai-setup.md +0 -168
- data/.claude/commands/test.md +0 -28
- data/.claude/commands/yard.md +0 -46
- data/docs/_index.md +0 -50
- data/docs/ai/_index.md +0 -56
- data/docs/ai/antipatterns.md +0 -166
- data/docs/ai/api.md +0 -251
- data/docs/ai/architecture.md +0 -92
- data/docs/ai/errors.md +0 -158
- data/docs/ai/faq_external.md +0 -133
- data/docs/ai/faq_internal.md +0 -86
- data/docs/ai/glossary.md +0 -45
- data/docs/concepts.md +0 -140
- data/docs/howto/controller.md +0 -194
- data/docs/howto/middleware_client.md +0 -119
- data/docs/howto/middleware_consumer.md +0 -127
- data/docs/howto/rails.md +0 -214
- data/docs/howto/resource.md +0 -200
- data/docs/howto/routing.md +0 -133
- data/docs/howto/testing.md +0 -259
- data/docs/howto/tracing.md +0 -119
data/skill/SKILL.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# BugBunny Expert
|
|
2
|
+
|
|
3
|
+
Skill de conocimiento completo sobre BugBunny. Consultame para cualquier pregunta sobre integración, arquitectura, API, errores y antipatrones.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Glosario
|
|
8
|
+
|
|
9
|
+
**AMQP** — Advanced Message Queuing Protocol. Protocolo binario que implementa RabbitMQ.
|
|
10
|
+
**Bunny** — Cliente Ruby para AMQP. BugBunny lo usa internamente para conexiones, canales y publicación.
|
|
11
|
+
**Exchange** — Recibe mensajes del producer y los enruta a queues según reglas. Tipos: `direct` (match exacto), `topic` (wildcards), `fanout` (broadcast).
|
|
12
|
+
**Queue** — Almacena mensajes hasta que un consumer los consume. Las queues durables sobreviven reinicios del broker.
|
|
13
|
+
**Routing Key** — String que el producer adjunta al mensaje. El exchange lo usa para decidir a qué queues enrutar.
|
|
14
|
+
**Binding** — Enlace entre un exchange y una queue, opcionalmente con un patrón de routing key.
|
|
15
|
+
**Session** — `BugBunny::Session` envuelve canales de Bunny con thread-safety y double-checked locking.
|
|
16
|
+
**RPC** — Patrón síncrono que usa la pseudo-cola `amq.rabbitmq.reply-to` para respuestas sin crear queues temporales.
|
|
17
|
+
**Fire-and-Forget** — Patrón asíncrono donde el producer publica y continúa sin esperar respuesta. Retorna `{ 'status' => 202 }`.
|
|
18
|
+
**Resource** — ORM tipo ActiveRecord que mapea operaciones CRUD a llamadas AMQP.
|
|
19
|
+
**Consumer** — Worker bloqueante que despacha mensajes a controladores mediante un Router.
|
|
20
|
+
**Connection Pool** — Pool de conexiones (`connection_pool` gem) que comparte sessions entre threads. Cada slot cachea su `Session` y `Producer`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Arquitectura: Flujo RPC
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
28
|
+
│ Resource │────>│ Client │────>│ Middleware│────>│ Producer │
|
|
29
|
+
└──────────┘ └──────────┘ │ Stack │ └────┬─────┘
|
|
30
|
+
└──────────┘ │
|
|
31
|
+
▼
|
|
32
|
+
┌──────────┐
|
|
33
|
+
│ Exchange │
|
|
34
|
+
└────┬─────┘
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
┌──────────┐
|
|
38
|
+
│ Queue │
|
|
39
|
+
└────┬─────┘
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
43
|
+
│ Reply │<────│Controller│<────│ Router │<────│ Consumer │
|
|
44
|
+
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
1. El `Client` pasa la petición por una pila de middlewares client-side.
|
|
48
|
+
2. El `Producer` publica en el exchange con `correlation_id`, `reply_to` y el path en el header `type`.
|
|
49
|
+
3. El hilo emisor se bloquea en un `Concurrent::IVar` esperando la respuesta.
|
|
50
|
+
4. El `Consumer` recibe, ejecuta consumer middlewares, rutea al controller.
|
|
51
|
+
5. El controller ejecuta callbacks y la acción, luego responde via `reply_to`.
|
|
52
|
+
6. Se aplican ganchos de traza (`on_rpc_reply`) y se devuelve el objeto hidratado.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Arquitectura: Componentes Clave
|
|
57
|
+
|
|
58
|
+
| Clase | Responsabilidad |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `BugBunny::Configuration` | Configuración global. Valida campos requeridos en `BugBunny.configure`. |
|
|
61
|
+
| `BugBunny::Session` | Wrapper de canal Bunny. Declara exchanges y queues. Thread-safe con double-checked locking. |
|
|
62
|
+
| `BugBunny::Producer` | Publica mensajes. Implementa RPC con `Concurrent::IVar` y direct reply-to. |
|
|
63
|
+
| `BugBunny::Client` | API de alto nivel. Pool de conexiones y middleware stack (onion architecture). |
|
|
64
|
+
| `BugBunny::Consumer` | Subscribe loop con health check. Rutea mensajes via `BugBunny.routes`. |
|
|
65
|
+
| `BugBunny::ConsumerMiddleware::Stack` | Pipeline de middlewares antes de `process_message`. Thread-safe. |
|
|
66
|
+
| `BugBunny::Controller` | Base class tipo Rails. `before_action`, `around_action`, `after_action`, `rescue_from`, `render`. |
|
|
67
|
+
| `BugBunny::Resource` | ORM sobre AMQP. `find`, `where`, `create`, `save`, `destroy`. ActiveModel validations y callbacks. |
|
|
68
|
+
| `BugBunny::Routing::RouteSet` | DSL de rutas: `resources`, `namespace`, `member`, `collection`. |
|
|
69
|
+
| `BugBunny::Observability` | Mixin de logging estructurado. `safe_log` nunca lanza excepciones. Filtra keys sensibles. |
|
|
70
|
+
| `BugBunny::Middleware::Stack` | Builder de middlewares client-side (onion architecture tipo Faraday). |
|
|
71
|
+
| BugBunny::Request | Value object del mensaje saliente con metadata AMQP completa. |
|
|
72
|
+
| BugBunny::OTel | Helpers para emitir campos siguiendo las OTel semantic conventions for messaging. |
|
|
73
|
+
| BugBunny::Railtie | Integración Rails: autoload de `app/rabbit`, fork safety (Puma, Spring). |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Observability: OpenTelemetry
|
|
78
|
+
|
|
79
|
+
BugBunny implementa las [OpenTelemetry semantic conventions for messaging](https://opentelemetry.io/docs/specs/otel/trace/semantic-conventions/messaging/) de forma nativa para garantizar la trazabilidad entre servicios en entornos distribuidos.
|
|
80
|
+
|
|
81
|
+
### Campos Estándar (Flat-naming)
|
|
82
|
+
|
|
83
|
+
| Campo | Valor / Origen | Propósito |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `messaging_system` | `"rabbitmq"` | Identifica el broker. |
|
|
86
|
+
| `messaging_operation` | `"publish"`, `"receive"`, `"process"` | Tipo de interacción. |
|
|
87
|
+
| `messaging_destination_name` | `exchange_name` | Exchange destino (o `""` para default). |
|
|
88
|
+
| `messaging_routing_key` | `routing_key` | Clave de ruteo final. |
|
|
89
|
+
| `messaging_message_id` | `correlation_id` | ID único para correlación y traza. |
|
|
90
|
+
|
|
91
|
+
### Inyección y Extracción
|
|
92
|
+
|
|
93
|
+
- **Publisher:** Inyecta estos campos en los headers AMQP bajo el prefijo `messaging_`. El usuario puede sobrescribirlos como *escape hatch* desde `headers`.
|
|
94
|
+
- **Consumer:** Extrae los campos de los logs estructurados sin mutar los headers originales. Los eventos `consumer.message_received` y `consumer.message_processed` incluyen estos campos automáticamente.
|
|
95
|
+
- **RPC Reply:** El consumer inyecta los mismos campos en el reply para cerrar el ciclo de traza del lado del cliente.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## API: Configuración Global
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
BugBunny.configure do |config|
|
|
103
|
+
# Conexión
|
|
104
|
+
config.host = 'localhost' # default: '127.0.0.1'
|
|
105
|
+
config.port = 5672 # default: 5672
|
|
106
|
+
config.username = 'guest' # default: 'guest'
|
|
107
|
+
config.password = 'guest' # default: 'guest'
|
|
108
|
+
config.vhost = '/' # default: '/'
|
|
109
|
+
|
|
110
|
+
# Resiliencia
|
|
111
|
+
config.automatically_recover = true
|
|
112
|
+
config.network_recovery_interval = 5
|
|
113
|
+
config.max_reconnect_attempts = nil # nil = infinito
|
|
114
|
+
config.connection_timeout = 10
|
|
115
|
+
config.heartbeat = 15
|
|
116
|
+
|
|
117
|
+
# Performance
|
|
118
|
+
config.channel_prefetch = 1
|
|
119
|
+
config.rpc_timeout = 10
|
|
120
|
+
|
|
121
|
+
# Logging
|
|
122
|
+
config.logger = Rails.logger
|
|
123
|
+
config.log_tags = [:uuid]
|
|
124
|
+
|
|
125
|
+
# Propagación de trazas
|
|
126
|
+
config.rpc_reply_headers = -> { { 'X-Trace-Id' => Tracer.id } }
|
|
127
|
+
config.on_rpc_reply = ->(h) { Tracer.hydrate(h['X-Trace-Id']) }
|
|
128
|
+
|
|
129
|
+
# Infraestructura (cascade level 2)
|
|
130
|
+
config.exchange_options = { durable: true }
|
|
131
|
+
config.queue_options = { auto_delete: false }
|
|
132
|
+
|
|
133
|
+
# Health check
|
|
134
|
+
config.health_check_interval = 60
|
|
135
|
+
config.health_check_file = 'tmp/bb_health'
|
|
136
|
+
|
|
137
|
+
# Routing
|
|
138
|
+
config.controller_namespace = 'BugBunny::Controllers'
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
La validación es automática tras el bloque; lanza `ConfigurationError` si faltan campos requeridos o los valores están fuera de rango.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## API: Routing DSL
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
BugBunny.routes.draw do
|
|
150
|
+
resources :users
|
|
151
|
+
resources :orders, only: [:index, :show]
|
|
152
|
+
resources :products, except: [:destroy]
|
|
153
|
+
|
|
154
|
+
namespace :admin do
|
|
155
|
+
resources :reports
|
|
156
|
+
resources :nodes do
|
|
157
|
+
member do
|
|
158
|
+
put :drain # PUT nodes/:id/drain
|
|
159
|
+
end
|
|
160
|
+
collection do
|
|
161
|
+
get :stats # GET nodes/stats
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Genera rutas REST estándar (index, show, create, update, destroy) mapeadas a controladores. El `namespace` añade prefijo al path y busca controladores dentro del módulo correspondiente.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## API: RPC vs Fire-and-Forget
|
|
173
|
+
|
|
174
|
+
**RPC síncrono** — Bloquea hasta respuesta. Usa `amq.rabbitmq.reply-to`. Timeout configurable.
|
|
175
|
+
```ruby
|
|
176
|
+
response = client.request('users/42', method: :get)
|
|
177
|
+
# → { 'status' => 200, 'body' => { 'id' => 42, 'name' => 'John' } }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Fire-and-Forget** — Publica y continúa. Sin confirmación.
|
|
181
|
+
```ruby
|
|
182
|
+
client.publish('events', method: :post, body: { type: 'order.placed' })
|
|
183
|
+
# → { 'status' => 202, 'body' => nil }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## FAQ
|
|
189
|
+
|
|
190
|
+
### ¿Cómo se integra con Rails?
|
|
191
|
+
`rails generate bug_bunny:install` genera el inicializador, crea `app/bug_bunny/controllers/` y actualiza `CLAUDE.md`. El pool se define en el inicializador y se asigna a `BugBunny::Resource.connection_pool`.
|
|
192
|
+
|
|
193
|
+
### ¿Cómo funciona el Health Check?
|
|
194
|
+
El consumer ejecuta un check periódico (default 60s) que verifica la conexión AMQP con un `queue.declare(passive: true)`. Si `health_check_file` está configurado, actualiza su mtime. En Kubernetes, usar un `livenessProbe` tipo `exec` que verifique recencia del archivo.
|
|
195
|
+
|
|
196
|
+
### ¿Cómo funciona el Connection Pool?
|
|
197
|
+
Cada slot del pool cachea su `Session` y `Producer` durante su vida útil. Esto evita recrear canales AMQP (costoso) y previene el error de doble `basic_consume`. Thread-safety garantizada por `ConnectionPool`.
|
|
198
|
+
|
|
199
|
+
### ¿Cómo funciona la cascada de configuración?
|
|
200
|
+
3 niveles: Gem defaults → Global config (`BugBunny.configure`) → Per-request (args en `client.request` o `Resource.with`). Se mergean con `merge`.
|
|
201
|
+
|
|
202
|
+
### ¿Cómo funciona fork safety?
|
|
203
|
+
`BugBunny::Railtie` registra hooks en `ActiveSupport::ForkTracker` (Rails 7.1+), `Puma.events.on_worker_boot` y `Spring.after_fork` para llamar `BugBunny.disconnect` y evitar sockets TCP heredados.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Antipatrones
|
|
208
|
+
|
|
209
|
+
### Consumer en Puma
|
|
210
|
+
No ejecutar el `Consumer` dentro de hilos de Puma. Es un bucle bloqueante que saturará el servidor web. Usar un proceso worker dedicado o una tarea Rake separada.
|
|
211
|
+
|
|
212
|
+
### Reasignación de Pool en Runtime
|
|
213
|
+
No asignar `Resource.connection_pool` dentro de controllers o models durante una petición. Es un ajuste global que causa condiciones de carrera y fugas de conexiones.
|
|
214
|
+
|
|
215
|
+
### Abuso de .with persistente
|
|
216
|
+
No guardar el resultado de `Order.with(...)` en una variable para múltiples llamadas. Lanzará error tras la primera ejecución. Para múltiples llamadas, usar siempre la forma de bloque.
|
|
217
|
+
|
|
218
|
+
### Registrar middleware durante call()
|
|
219
|
+
No registrar consumer middlewares durante la ejecución de `call()`. El stack toma un snapshot al inicio; los registros concurrentes no afectan la ejecución actual.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Errores Comunes
|
|
224
|
+
|
|
225
|
+
### BugBunny::RequestTimeout (408)
|
|
226
|
+
**Causa:** No hubo respuesta en `config.rpc_timeout` segundos.
|
|
227
|
+
**Resolución:** Verificar que el worker esté activo y que el controlador remoto no lance excepciones silenciosas.
|
|
228
|
+
|
|
229
|
+
### BugBunny::SecurityError
|
|
230
|
+
**Causa:** El mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
|
|
231
|
+
**Resolución:** Verificar la jerarquía de controladores y que `config.controller_namespace` coincida.
|
|
232
|
+
|
|
233
|
+
### BugBunny::UnprocessableEntity (422)
|
|
234
|
+
**Causa:** Fallo de validación en el servicio remoto.
|
|
235
|
+
**Resolución:** `resource.save` devuelve `false`. Acceder a `resource.errors` o `rescue` con `e.error_messages`.
|
|
236
|
+
|
|
237
|
+
### BugBunny::CommunicationError
|
|
238
|
+
**Causa:** Fallo de conexión o reconexión agotada.
|
|
239
|
+
**Resolución:** Verificar conectividad a RabbitMQ. Revisar `max_reconnect_attempts` y logs de reconexión.
|
|
240
|
+
|
|
241
|
+
Ver catálogo completo en [Errores](references/errores.md).
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Referencias
|
|
246
|
+
|
|
247
|
+
- [Routing](references/routing.md) — DSL de rutas, bindings, namespaces, member y collection
|
|
248
|
+
- [Controllers](references/controller.md) — Acciones, callbacks, render, rescue_from y log tags
|
|
249
|
+
- [Resources](references/resource.md) — CRUD sobre AMQP, .with, callbacks y change tracking
|
|
250
|
+
- [Client y Middleware](references/client-middleware.md) — Client, Producer, middleware stack onion
|
|
251
|
+
- [Consumer](references/consumer.md) — Subscribe loop, consumer middleware, health check
|
|
252
|
+
- [Catálogo de Errores](references/errores.md) — Jerarquía completa de excepciones con resolución
|
|
253
|
+
- [Testing](references/testing.md) — RSpec helpers, mocks, patrones de integración
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Client y Middleware
|
|
2
|
+
|
|
3
|
+
## Client
|
|
4
|
+
|
|
5
|
+
API de alto nivel para publicar mensajes. Usa un pool de conexiones y una pila de middlewares.
|
|
6
|
+
|
|
7
|
+
### Métodos Principales
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# RPC síncrono — bloquea hasta respuesta
|
|
11
|
+
response = client.request('users/123', method: :get, timeout: 30)
|
|
12
|
+
response = client.request('users', method: :post, body: { name: 'John' })
|
|
13
|
+
# → { 'status' => 200, 'body' => {...} }
|
|
14
|
+
|
|
15
|
+
# Fire-and-Forget — no bloquea
|
|
16
|
+
client.publish('events/user_created', method: :post, body: { user_id: 42 })
|
|
17
|
+
# → { 'status' => 202, 'body' => nil }
|
|
18
|
+
|
|
19
|
+
# General con bloque
|
|
20
|
+
client.send(url) { |req| req.delivery_mode = :publish }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Argumentos de Request
|
|
24
|
+
|
|
25
|
+
| Argumento | Tipo | Default | Propósito |
|
|
26
|
+
|-----------|------|---------|-----------|
|
|
27
|
+
| `method` | Symbol | `:get` | Verbo HTTP |
|
|
28
|
+
| `body` | Hash/String | nil | Payload del mensaje |
|
|
29
|
+
| `headers` | Hash | {} | Headers AMQP custom |
|
|
30
|
+
| `params` | Hash | {} | Query string params |
|
|
31
|
+
| `timeout` | Integer | config | Override de RPC timeout |
|
|
32
|
+
| `exchange` | String | config | Exchange destino |
|
|
33
|
+
| `exchange_type` | String | 'direct' | Tipo de exchange |
|
|
34
|
+
| `routing_key` | String | path | Override de routing key |
|
|
35
|
+
| `exchange_options` | Hash | {} | Cascade level 3 |
|
|
36
|
+
| `queue_options` | Hash | {} | Cascade level 3 |
|
|
37
|
+
|
|
38
|
+
## Producer (bajo nivel)
|
|
39
|
+
|
|
40
|
+
El `Producer` es usado internamente por el `Client`. Implementa los dos patrones de entrega.
|
|
41
|
+
|
|
42
|
+
### RPC
|
|
43
|
+
|
|
44
|
+
- Usa `amq.rabbitmq.reply-to` (direct reply-to pattern).
|
|
45
|
+
- Tracking de `correlation_id` en `Concurrent::Map` (thread-safe).
|
|
46
|
+
- Reply listener (`basic_consume`) auto-iniciado en el primer RPC.
|
|
47
|
+
- Double-checked locking mutex para seguridad del listener.
|
|
48
|
+
- Timeout lanza `BugBunny::RequestTimeout`.
|
|
49
|
+
|
|
50
|
+
### Fire-and-Forget
|
|
51
|
+
|
|
52
|
+
- Publica en el exchange y retorna `{ 'status' => 202 }` inmediatamente.
|
|
53
|
+
- Sin confirmación de procesamiento.
|
|
54
|
+
|
|
55
|
+
## Middleware Stack (Client-side, Onion Architecture)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Request ─→ RaiseError ─→ JsonResponse ─→ Custom ─→ Producer
|
|
59
|
+
Response ←─ RaiseError ←─ JsonResponse ←─ Custom ←─
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Registrar Middlewares
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class Order < BugBunny::Resource
|
|
66
|
+
client_middleware do |stack|
|
|
67
|
+
stack.use BugBunny::Middleware::RaiseError
|
|
68
|
+
stack.use BugBunny::Middleware::JsonResponse
|
|
69
|
+
stack.use MyCustomMiddleware
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Crear un Middleware Custom
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class MyMiddleware < BugBunny::Middleware::Base
|
|
78
|
+
def on_request(env)
|
|
79
|
+
# Modificar request antes de enviar
|
|
80
|
+
env.headers['X-Custom'] = 'value'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def on_complete(response)
|
|
84
|
+
# Modificar response después de recibir
|
|
85
|
+
response['body'] = transform(response['body'])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
El método `call` del `Base` invoca `on_request`, delega a `@app.call`, y luego `on_complete`.
|
|
91
|
+
|
|
92
|
+
### Middlewares Incluidos
|
|
93
|
+
|
|
94
|
+
**RaiseError** — Mapea status HTTP a excepciones:
|
|
95
|
+
|
|
96
|
+
| Status | Excepción |
|
|
97
|
+
|--------|-----------|
|
|
98
|
+
| 200-299 | (ninguna) |
|
|
99
|
+
| 400 | `BadRequest` |
|
|
100
|
+
| 404 | `NotFound` |
|
|
101
|
+
| 406 | `NotAcceptable` |
|
|
102
|
+
| 408 | `RequestTimeout` |
|
|
103
|
+
| 409 | `Conflict` |
|
|
104
|
+
| 422 | `UnprocessableEntity` (con smart extraction de errors) |
|
|
105
|
+
| 500+ | `InternalServerError` / `ServerError` |
|
|
106
|
+
| 4xx otros | `ClientError` |
|
|
107
|
+
|
|
108
|
+
**JsonResponse** — Auto-parsea `response['body']` de String a Hash/Array. Aplica `HashWithIndifferentAccess` si disponible.
|
|
109
|
+
|
|
110
|
+
## OpenTelemetry: Publisher Injection
|
|
111
|
+
|
|
112
|
+
El `Producer` (vía `Request#amqp_options`) inyecta automáticamente los campos de OTel semantic conventions en los headers AMQP del mensaje saliente.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# Headers inyectados automáticamente
|
|
116
|
+
{
|
|
117
|
+
'messaging_system' => 'rabbitmq',
|
|
118
|
+
'messaging_operation' => 'publish',
|
|
119
|
+
'messaging_destination_name' => 'exchange_name',
|
|
120
|
+
'messaging_routing_key' => 'rk',
|
|
121
|
+
'messaging_message_id' => 'uuid'
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
El orden de merge es: **OTel base** → **headers del usuario** → **x-http-method**. Esto permite al desarrollador sobrescribir valores de OTel si es necesario, pero garantiza que el ruteo interno (`x-http-method`) se mantenga íntegro.
|
|
126
|
+
|
|
127
|
+
## Request Object
|
|
128
|
+
|
|
129
|
+
Value object con toda la metadata AMQP:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
req.path # 'users/123'
|
|
133
|
+
req.method # :get
|
|
134
|
+
req.body # Hash, Array, String o nil
|
|
135
|
+
req.headers # Hash custom
|
|
136
|
+
req.params # Hash query string
|
|
137
|
+
req.full_path # path + query string
|
|
138
|
+
req.delivery_mode # :rpc o :publish
|
|
139
|
+
req.exchange # String destino
|
|
140
|
+
req.exchange_type # 'direct', 'topic', 'fanout'
|
|
141
|
+
req.correlation_id # UUID auto-generado
|
|
142
|
+
req.reply_to # 'amq.rabbitmq.reply-to' (auto para RPC)
|
|
143
|
+
req.timestamp # Time.now.to_i
|
|
144
|
+
req.content_type # 'application/json'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Cascada de Configuración (3 niveles)
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Level 1: Gem defaults
|
|
151
|
+
{ durable: false, auto_delete: false } # exchanges
|
|
152
|
+
{ exclusive: false, durable: false, auto_delete: true } # queues
|
|
153
|
+
|
|
154
|
+
# Level 2: Global config
|
|
155
|
+
BugBunny.configure { |c| c.exchange_options = { durable: true } }
|
|
156
|
+
|
|
157
|
+
# Level 3: Per-request
|
|
158
|
+
client.request('users', exchange_options: { durable: true })
|
|
159
|
+
|
|
160
|
+
# Merge final: Level1.merge(Level2).merge(Level3)
|
|
161
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Consumer
|
|
2
|
+
|
|
3
|
+
## Subscribe
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
consumer = BugBunny::Consumer.subscribe(
|
|
7
|
+
connection: bunny_session,
|
|
8
|
+
queue_name: 'my_app_queue',
|
|
9
|
+
exchange_name: 'my_exchange',
|
|
10
|
+
routing_key: 'users.*',
|
|
11
|
+
exchange_type: 'topic',
|
|
12
|
+
exchange_opts: { durable: true },
|
|
13
|
+
queue_opts: { auto_delete: false },
|
|
14
|
+
block: true # Si false, retorna inmediatamente
|
|
15
|
+
)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Flujo de Procesamiento
|
|
19
|
+
|
|
20
|
+
1. Escucha en la queue con `manual_ack: true`.
|
|
21
|
+
2. Extrae campos **OTel messaging** del mensaje para logs estructurados (sin mutar headers).
|
|
22
|
+
3. Valida que el mensaje tenga header `type` (path).
|
|
23
|
+
4. Parsea el método HTTP de headers (`x-http-method` o `method`).
|
|
24
|
+
5. Emite log `consumer.message_received` con campos OTel (`messaging_operation: 'process'`).
|
|
25
|
+
6. Reconoce la ruta con `BugBunny.routes.recognize(method, path)`.
|
|
26
|
+
7. Resuelve el controlador validando herencia de `BugBunny::Controller`.
|
|
27
|
+
8. Ejecuta consumer middlewares → controller callbacks → acción.
|
|
28
|
+
9. Responde via `reply_to` si está presente (RPC), inyectando campos OTel (`messaging_operation: 'publish'`).
|
|
29
|
+
10. Emite log `consumer.message_processed` con campos OTel y duraciones.
|
|
30
|
+
11. Hace `ack` del mensaje. En caso de error, `reject`.
|
|
31
|
+
|
|
32
|
+
## Observability: OTel Fields
|
|
33
|
+
|
|
34
|
+
El consumer construye automáticamente el hash de campos OTel al inicio de `process_message`:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
otel_fields = BugBunny::OTel.messaging_headers(
|
|
38
|
+
operation: 'process',
|
|
39
|
+
destination: delivery_info.exchange,
|
|
40
|
+
routing_key: delivery_info.routing_key,
|
|
41
|
+
message_id: properties.correlation_id
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Estos campos se mergean en todos los eventos de log del ciclo de vida del mensaje, permitiendo que ExisRay los rastree sin necesidad de propagarlos manualmente en los headers del usuario.
|
|
46
|
+
|
|
47
|
+
## Lifecycle
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
consumer.shutdown # Cierra canal, detiene health check
|
|
51
|
+
consumer.session # Accede al Session subyacente
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Consumer Middleware
|
|
55
|
+
|
|
56
|
+
### Registrar
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
BugBunny.configuration.consumer_middlewares.use MyTracing::Middleware
|
|
60
|
+
BugBunny.configuration.consumer_middlewares.use MyAuth::Middleware
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Crear Middleware
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class MyMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
67
|
+
def call(delivery_info, properties, body)
|
|
68
|
+
# Pre-procesamiento (ej: hidratar trace context)
|
|
69
|
+
@app.call(delivery_info, properties, body)
|
|
70
|
+
# Post-procesamiento (ej: cleanup)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Comportamiento del Stack
|
|
76
|
+
|
|
77
|
+
- El stack toma un **snapshot** al inicio de `call()`.
|
|
78
|
+
- Registros concurrentes durante la ejecución NO afectan la cadena actual.
|
|
79
|
+
- Thread-safe para registros con `use()`.
|
|
80
|
+
- Orden FIFO: el primero registrado es el primero en ejecutar.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
stack.use(A) # A.call → B.call → core
|
|
84
|
+
stack.use(B)
|
|
85
|
+
stack.empty? # false
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Health Check
|
|
89
|
+
|
|
90
|
+
- **Intervalo:** Configurable (default 60s).
|
|
91
|
+
- **Verificación:** `queue.declare(passive: true)` para confirmar conexión.
|
|
92
|
+
- **Touchfile:** Si `config.health_check_file` está configurado, actualiza mtime.
|
|
93
|
+
- **Fallo:** Cierra canal, dispara loop de reconexión.
|
|
94
|
+
|
|
95
|
+
### Kubernetes Integration
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
livenessProbe:
|
|
99
|
+
exec:
|
|
100
|
+
command:
|
|
101
|
+
- test
|
|
102
|
+
- -f
|
|
103
|
+
- /app/tmp/bb_health
|
|
104
|
+
initialDelaySeconds: 30
|
|
105
|
+
periodSeconds: 60
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Reconexión
|
|
109
|
+
|
|
110
|
+
- Exponential backoff desde `network_recovery_interval` hasta `max_reconnect_interval`.
|
|
111
|
+
- Intentos limitados por `max_reconnect_attempts` (nil = infinito).
|
|
112
|
+
- Logs estructurados en cada intento: `event=session.reconnect_attempt`.
|
|
113
|
+
- Si se agotan intentos: `event=consumer.reconnect_exhausted`, lanza `CommunicationError`.
|
|
114
|
+
|
|
115
|
+
## Manejo de Errores
|
|
116
|
+
|
|
117
|
+
| Situación | Respuesta |
|
|
118
|
+
|-----------|-----------|
|
|
119
|
+
| Ruta no encontrada | 404 + log `event=consumer.route_not_found` |
|
|
120
|
+
| Controller no encontrado (namespace) | 404 + log `event=consumer.controller_not_found` |
|
|
121
|
+
| Controller no hereda de BugBunny::Controller | `SecurityError` |
|
|
122
|
+
| Excepción no capturada en controller | 500 + log `event=controller.unhandled_exception` con backtrace |
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Controllers
|
|
2
|
+
|
|
3
|
+
## Estructura Base
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class UsersController < BugBunny::Controller
|
|
7
|
+
before_action :authenticate, only: [:create, :update, :destroy]
|
|
8
|
+
around_action :with_tracing
|
|
9
|
+
after_action :log_response, only: [:index, :show]
|
|
10
|
+
rescue_from BugBunny::NotFound, with: :render_not_found
|
|
11
|
+
|
|
12
|
+
def index
|
|
13
|
+
users = UserService.list(params[:filter])
|
|
14
|
+
render status: :ok, json: { users: users }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
user = UserService.find(params[:id])
|
|
19
|
+
render status: :ok, json: user
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def authenticate
|
|
25
|
+
render status: :forbidden, json: { error: 'Unauthorized' } unless valid_token?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def with_tracing
|
|
29
|
+
Tracer.start
|
|
30
|
+
yield
|
|
31
|
+
ensure
|
|
32
|
+
Tracer.finish
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def log_response
|
|
36
|
+
logger.info("Response: #{rendered_response[:status]}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_not_found(exception)
|
|
40
|
+
render status: :not_found, json: { error: exception.message }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Callbacks — Orden de Ejecución
|
|
46
|
+
|
|
47
|
+
1. `around_action` blocks (capa externa, envuelve todo con `yield`)
|
|
48
|
+
2. `before_action` callbacks (se detiene si se llama `render`)
|
|
49
|
+
3. Acción del controlador
|
|
50
|
+
4. `after_action` callbacks (NO se ejecuta si before_action haltó o si hubo excepción)
|
|
51
|
+
|
|
52
|
+
## Filtros: only / except
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
before_action :authenticate, only: [:create, :update]
|
|
56
|
+
after_action :audit, except: [:index]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## rescue_from
|
|
60
|
+
|
|
61
|
+
Captura excepciones con handler method o bloque:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
rescue_from BugBunny::UnprocessableEntity, with: :handle_validation
|
|
65
|
+
rescue_from StandardError do |e|
|
|
66
|
+
render status: :internal_server_error, json: { error: e.message }
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Atributos Disponibles en el Controller
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
@headers # Hash — metadata del mensaje AMQP (method, routing_key, id, etc.)
|
|
74
|
+
@params # HashWithIndifferentAccess — body JSON + query params unificados
|
|
75
|
+
@raw_string # String — body crudo si no es JSON
|
|
76
|
+
@response_headers # Hash — headers para el reply RPC
|
|
77
|
+
@rendered_response # Hash o nil — respuesta renderizada
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Render
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
render(status: :ok, json: { users: [...] })
|
|
84
|
+
render(status: 201, json: @user)
|
|
85
|
+
render(status: :unprocessable_entity, json: { errors: @resource.errors }, headers: { 'X-Custom' => 'val' })
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Si no se llama `render`, el response default es `{ status: 204, body: nil }`.
|
|
89
|
+
|
|
90
|
+
## Log Tags
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Global
|
|
94
|
+
BugBunny.configuration.log_tags = [:uuid, :user_id, ->(c) { c.current_user }]
|
|
95
|
+
|
|
96
|
+
# Por controller
|
|
97
|
+
class UsersController < BugBunny::Controller
|
|
98
|
+
self.log_tags = [:uuid]
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Tipos soportados:
|
|
103
|
+
- **Symbol:** Llama al método del controller (ej: `:uuid` → `self.uuid`)
|
|
104
|
+
- **Proc:** Ejecuta con el controller como argumento
|
|
105
|
+
- **String:** Valor literal
|