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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +116 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/sentry/SKILL.md +135 -0
  6. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  7. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  8. data/.agents/skills/skill-builder/SKILL.md +293 -0
  9. data/.agents/skills/skill-manager/SKILL.md +225 -0
  10. data/.agents/skills/skill-manager/scripts/sync.rb +356 -0
  11. data/.agents/skills/yard/SKILL.md +311 -0
  12. data/.agents/skills/yard/references/tipos.md +144 -0
  13. data/CHANGELOG.md +14 -0
  14. data/CLAUDE.md +28 -225
  15. data/README.md +5 -3
  16. data/lib/bug_bunny/consumer.rb +21 -5
  17. data/lib/bug_bunny/otel.rb +47 -0
  18. data/lib/bug_bunny/producer.rb +13 -4
  19. data/lib/bug_bunny/request.rb +14 -2
  20. data/lib/bug_bunny/version.rb +1 -1
  21. data/lib/bug_bunny.rb +1 -0
  22. data/skill/SKILL.md +253 -0
  23. data/skill/references/client-middleware.md +161 -0
  24. data/skill/references/consumer.md +122 -0
  25. data/skill/references/controller.md +105 -0
  26. data/skill/references/errores.md +97 -0
  27. data/skill/references/resource.md +116 -0
  28. data/skill/references/routing.md +82 -0
  29. data/skill/references/testing.md +138 -0
  30. data/skills.lock +30 -0
  31. data/skills.yml +40 -0
  32. data/spec/integration/consumer_middleware_spec.rb +23 -2
  33. data/spec/unit/consumer_spec.rb +138 -6
  34. data/spec/unit/otel_spec.rb +54 -0
  35. data/spec/unit/producer_spec.rb +187 -0
  36. data/spec/unit/request_spec.rb +51 -0
  37. metadata +28 -29
  38. data/.agents/skills/rabbitmq-expert/SKILL.md +0 -1555
  39. data/.claude/commands/gem-ai-setup.md +0 -174
  40. data/.claude/commands/pr.md +0 -53
  41. data/.claude/commands/release.md +0 -52
  42. data/.claude/commands/rubocop.md +0 -22
  43. data/.claude/commands/service-ai-setup.md +0 -168
  44. data/.claude/commands/test.md +0 -28
  45. data/.claude/commands/yard.md +0 -46
  46. data/docs/_index.md +0 -50
  47. data/docs/ai/_index.md +0 -56
  48. data/docs/ai/antipatterns.md +0 -166
  49. data/docs/ai/api.md +0 -251
  50. data/docs/ai/architecture.md +0 -92
  51. data/docs/ai/errors.md +0 -158
  52. data/docs/ai/faq_external.md +0 -133
  53. data/docs/ai/faq_internal.md +0 -86
  54. data/docs/ai/glossary.md +0 -45
  55. data/docs/concepts.md +0 -140
  56. data/docs/howto/controller.md +0 -194
  57. data/docs/howto/middleware_client.md +0 -119
  58. data/docs/howto/middleware_consumer.md +0 -127
  59. data/docs/howto/rails.md +0 -214
  60. data/docs/howto/resource.md +0 -200
  61. data/docs/howto/routing.md +0 -133
  62. data/docs/howto/testing.md +0 -259
  63. 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