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/CLAUDE.md
CHANGED
|
@@ -6,235 +6,38 @@ BugBunny es una gema Ruby que implementa una capa de enrutamiento RESTful sobre
|
|
|
6
6
|
|
|
7
7
|
**Problema que resuelve:** Eliminar el acoplamiento directo entre microservicios via HTTP, usando RabbitMQ como bus de mensajes con la misma ergonomía de un framework web.
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
- **Docs AI:** `docs/ai/` — conocimiento estructurado para agentes
|
|
12
|
-
- **Index:** `docs/ai/_index.md` — manifest con versión, audiencias y archivos
|
|
13
|
-
- **Docs humanos:** `docs/_index.md` — manifest de toda la documentación (`howto/`, `concepts.md`)
|
|
14
|
-
|
|
15
|
-
## Skills disponibles
|
|
16
|
-
|
|
17
|
-
- `.agents/skills/rabbitmq-expert/` — arquitectura AMQP, exchanges, quorum queues, DLX, HA, clustering
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Arquitectura
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
Publisher (Client/Resource)
|
|
25
|
-
└─ Producer → Session → Bunny → RabbitMQ Exchange
|
|
26
|
-
│
|
|
27
|
-
RabbitMQ Queue
|
|
28
|
-
│
|
|
29
|
-
Consumer (subscribe loop)
|
|
30
|
-
└─ ConsumerMiddleware::Stack
|
|
31
|
-
└─ process_message
|
|
32
|
-
└─ Router → Controller → Action
|
|
33
|
-
└─ reply (RPC)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Componentes clave
|
|
37
|
-
|
|
38
|
-
| Clase | Responsabilidad |
|
|
39
|
-
|---|---|
|
|
40
|
-
| `BugBunny::Session` | Wrapper de canal Bunny. Declara exchanges y queues. |
|
|
41
|
-
| `BugBunny::Consumer` | Subscribe loop. Rutea mensajes a controladores via `BugBunny.routes`. |
|
|
42
|
-
| `BugBunny::ConsumerMiddleware::Stack` | Pipeline de middlewares antes de `process_message`. |
|
|
43
|
-
| `BugBunny::Producer` | Publica mensajes. Implementa RPC con `Concurrent::IVar`. |
|
|
44
|
-
| `BugBunny::Client` | API de alto nivel para el publicador. Pool de conexiones. |
|
|
45
|
-
| `BugBunny::Controller` | Base class tipo Rails. `around_action`, `before_action`, `render`. |
|
|
46
|
-
| `BugBunny::Resource` | ActiveRecord-like sobre AMQP. `find`, `where`, `create`, etc. |
|
|
47
|
-
| `BugBunny::Request` | Value object del mensaje saliente (path, method, params, headers). |
|
|
48
|
-
| `BugBunny::Observability` | Mixin de logging estructurado. `safe_log`, `exception_metadata`. |
|
|
49
|
-
| `BugBunny::Configuration` | Configuración global. Logger, timeouts, middleware hooks. |
|
|
50
|
-
|
|
51
|
-
### Flujo RPC completo
|
|
52
|
-
|
|
53
|
-
1. `Resource.find(id)` → `Client#request` → `Producer#rpc`
|
|
54
|
-
2. Producer publica en exchange con `reply_to: 'amq.rabbitmq.reply-to'`
|
|
55
|
-
3. `Concurrent::IVar` bloquea el thread principal (`future.value(timeout)`)
|
|
56
|
-
4. Consumer recibe → middleware stack → controller → `reply(response)`
|
|
57
|
-
5. Reply listener thread setea `future.set({ body:, headers: })`
|
|
58
|
-
6. Thread principal: `on_rpc_reply&.call(headers)` → `parse_response(body)`
|
|
59
|
-
|
|
60
|
-
## Hooks de extensión
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
# Middleware antes de process_message (ej: tracing, auth)
|
|
64
|
-
BugBunny.consumer_middlewares.use MyMiddleware
|
|
65
|
-
|
|
66
|
-
# Headers a inyectar en el reply RPC (ej: trace context actualizado)
|
|
67
|
-
config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => Tracer.header } }
|
|
68
|
-
|
|
69
|
-
# Callback en el thread principal al recibir el reply (ej: hidratar tracer)
|
|
70
|
-
config.on_rpc_reply = ->(headers) { Tracer.hydrate(headers['X-Amzn-Trace-Id']) }
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## Dominio y Expertise
|
|
76
|
-
|
|
77
|
-
Al trabajar en esta gema aplicá expertise en:
|
|
78
|
-
|
|
79
|
-
- **Ruby idiomático**: módulos, mixins, metaprogramación, `class_attribute`, `Concurrent::*`
|
|
80
|
-
- **RabbitMQ / AMQP**: exchanges (direct/topic/fanout), queues, bindings, `reply_to`, `correlation_id`, `properties.headers`, publisher confirms, manual ack
|
|
81
|
-
- **Bunny**: la gema Ruby que wrappea AMQP. `channel`, `basic_consume`, `basic_publish`, `IVar`
|
|
82
|
-
- **Rails patterns**: `ActiveModel`, `ActiveSupport`, `class_attribute`, `concerns`, `constantize`
|
|
83
|
-
- **Rack**: `Rack::Utils.parse_nested_query`, `build_nested_query`
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## Observability — Estándar de Logging
|
|
88
|
-
|
|
89
|
-
Esta gema implementa su propio patrón de observability via `BugBunny::Observability`.
|
|
90
|
-
|
|
91
|
-
### Reglas fundamentales
|
|
92
|
-
|
|
93
|
-
- **Formato**: `component=x event=clase.evento [key=value ...]` — todo en una línea
|
|
94
|
-
- **Nunca** llamar al logger directamente. Siempre usar `safe_log`
|
|
95
|
-
- **Nunca** `Kernel#warn`, `$stderr`, `puts`
|
|
96
|
-
- **Niveles**: `ERROR`=excepción, `WARN`=inesperado+continuó, `INFO`=normal, `DEBUG`=detalle
|
|
97
|
-
- `DEBUG` siempre en bloque: `logger.debug { "k=#{v}" }` — `safe_log` lo maneja internamente
|
|
98
|
-
- Duraciones: `Process.clock_gettime(Process::CLOCK_MONOTONIC)`, nunca `Time.now`
|
|
99
|
-
- Logger failures **nunca** interrumpen el flujo — `safe_log` tiene `rescue StandardError`
|
|
100
|
-
|
|
101
|
-
### Uso en clases nuevas
|
|
102
|
-
|
|
103
|
-
```ruby
|
|
104
|
-
class BugBunny::MiClase
|
|
105
|
-
include BugBunny::Observability
|
|
106
|
-
|
|
107
|
-
def initialize
|
|
108
|
-
@logger = BugBunny.configuration.logger
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def mi_metodo
|
|
112
|
-
start = monotonic_now
|
|
113
|
-
# ...
|
|
114
|
-
safe_log(:info, "mi_clase.mi_evento", campo: valor, duration_s: duration_s(start))
|
|
115
|
-
rescue StandardError => e
|
|
116
|
-
safe_log(:error, "mi_clase.error", **exception_metadata(e))
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Naming de eventos
|
|
122
|
-
|
|
123
|
-
Formato estricto: `"clase.evento"` (string, nunca symbol)
|
|
9
|
+
## Documentación
|
|
124
10
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| `consumer.bound` | INFO | Queue bindeada al exchange |
|
|
129
|
-
| `consumer.message_received` | INFO | Mensaje recibido, antes del routing |
|
|
130
|
-
| `consumer.route_matched` | DEBUG | Ruta encontrada |
|
|
131
|
-
| `consumer.message_processed` | INFO | Procesamiento exitoso con duración |
|
|
132
|
-
| `consumer.execution_error` | ERROR | Excepción en el procesamiento |
|
|
133
|
-
| `producer.publish` | INFO | Mensaje publicado |
|
|
134
|
-
| `producer.rpc_waiting` | DEBUG | Bloqueando esperando respuesta |
|
|
135
|
-
| `producer.rpc_response_received` | DEBUG | Reply recibido (thread principal) |
|
|
11
|
+
- **Para humanos**: `docs/` (5 archivos) + `README.md`. Ver README para índice.
|
|
12
|
+
- **Para agentes AI**: `skill/SKILL.md` + `skill/references/`. Es la skill empaquetada que otros proyectos consumen via `skill-manager sync`.
|
|
13
|
+
- **Nunca referenciar `skill/` desde `docs/` o `README.md`** — son audiencias distintas.
|
|
136
14
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# => error_class: "RuntimeError", error_message: "..."
|
|
142
|
-
|
|
143
|
-
safe_log(:info, "clase.evento", duration_s: duration_s(start_time))
|
|
144
|
-
# => duration_s: 0.001234
|
|
145
|
-
|
|
146
|
-
# Valores sensibles se filtran automáticamente:
|
|
147
|
-
# password, token, secret, api_key, auth → [FILTERED]
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
---
|
|
15
|
+
## Knowledge Base
|
|
16
|
+
- Las skills en `.agents/skills/` incluyen conocimiento de dependencias.
|
|
17
|
+
- Leer la skill de una dependencia ANTES de responder sobre ella.
|
|
18
|
+
- Rebuild: `ruby .agents/skills/skill-manager/scripts/sync.rb`
|
|
151
19
|
|
|
152
|
-
|
|
20
|
+
### Entorno
|
|
21
|
+
- Versión de Ruby: leer `.ruby-version`
|
|
22
|
+
- Versión de Rails y gemas: leer `Gemfile.lock`
|
|
23
|
+
- Gestor de Ruby: chruby (no usar rvm ni rbenv)
|
|
24
|
+
- Package manager: Bundler
|
|
153
25
|
|
|
154
26
|
### RuboCop
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
source /opt/homebrew/opt/chruby/share/chruby/chruby.sh && chruby ruby-3.3.8
|
|
160
|
-
bundle exec rubocop
|
|
161
|
-
bundle exec rubocop -a # autocorrect
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**No corregir código existente no tocado.** Solo el código nuevo o modificado en el PR.
|
|
27
|
+
- Usamos rubocop-rails-omakase como base.
|
|
28
|
+
- Correr `bundle exec rubocop -a` antes de commitear.
|
|
29
|
+
- No deshabilitar cops sin justificación en el PR.
|
|
165
30
|
|
|
166
31
|
### YARD
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# resultado = mi_metodo(arg)
|
|
180
|
-
def mi_metodo(name)
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
bundle exec yard doc
|
|
185
|
-
bundle exec yard stats --list-undoc
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### RSpec
|
|
189
|
-
|
|
190
|
-
Tests en `spec/`. Sin mocks de dependencias externas reales (RabbitMQ se mockea con doubles de Bunny).
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
source /opt/homebrew/opt/chruby/share/chruby/chruby.sh && chruby ruby-3.3.8
|
|
194
|
-
bundle exec rspec
|
|
195
|
-
bundle exec rspec spec/bug_bunny/consumer_spec.rb # archivo específico
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## Entorno de Desarrollo
|
|
201
|
-
|
|
202
|
-
### Ruby
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
source /opt/homebrew/opt/chruby/share/chruby/chruby.sh && chruby ruby-3.3.8
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Nunca usar `bundle exec ruby` con el Ruby del sistema (2.6). Siempre sourcear chruby primero.
|
|
209
|
-
|
|
210
|
-
### Worktrees
|
|
211
|
-
|
|
212
|
-
- **Main**: `/Users/gabriel/src/gems/bug_bunny` (rama `main`)
|
|
213
|
-
- **Work**: `/Users/gabriel/src/gems/worktrees/current-5n3` (ramas de feature)
|
|
214
|
-
- `main` está checkeado en otro worktree — no se puede hacer `git checkout main` desde el worktree de trabajo
|
|
215
|
-
|
|
216
|
-
### Push a remoto
|
|
217
|
-
|
|
218
|
-
SSH está roto en este entorno. Para push siempre:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
git remote set-url origin https://github.com/gedera/bug_bunny.git
|
|
222
|
-
git push origin main
|
|
223
|
-
git remote set-url origin git@github.com:gedera/bug_bunny.git # restaurar
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
---
|
|
227
|
-
|
|
228
|
-
## Release Workflow
|
|
229
|
-
|
|
230
|
-
Usá el comando `/release` para el flujo completo. Manualmente:
|
|
231
|
-
|
|
232
|
-
1. Determinar tipo: `patch`=bugfix, `minor`=feature nueva, `major`=breaking change
|
|
233
|
-
2. Actualizar `lib/bug_bunny/version.rb`
|
|
234
|
-
3. Agregar entrada al tope de `CHANGELOG.md`
|
|
235
|
-
4. Commit con mensaje convencional
|
|
236
|
-
5. Desde `/Users/gabriel/src/gems/bug_bunny`: `git merge --ff-only <branch>`
|
|
237
|
-
6. Push via HTTPS + restaurar SSH
|
|
238
|
-
7. `git tag vX.Y.Z && git push origin vX.Y.Z`
|
|
239
|
-
|
|
240
|
-
**Nunca commitear ni pushear sin permiso explícito del usuario.**
|
|
32
|
+
- Documentación incremental: si tocás un método, documentalo con YARD.
|
|
33
|
+
- Consultar la skill `yard` para tags y tipos correctos.
|
|
34
|
+
- Verificar cobertura: `bundle exec yard stats --list-undoc`
|
|
35
|
+
|
|
36
|
+
### Testing
|
|
37
|
+
- Framework: RSpec
|
|
38
|
+
- Correr: `bundle exec rspec`
|
|
39
|
+
- Todo código nuevo debe tener tests.
|
|
40
|
+
|
|
41
|
+
### Releases o Nuevas versiones
|
|
42
|
+
- Usar `/gem-release` para publicar nuevas versiones.
|
|
43
|
+
- El GitHub Action publica a RubyGems automáticamente al pushear un tag `v*`.
|
data/README.md
CHANGED
|
@@ -214,15 +214,17 @@ BugBunny.consumer_middlewares.use TracingMiddleware
|
|
|
214
214
|
|
|
215
215
|
## Observability
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
BugBunny implementa de forma nativa las [OpenTelemetry semantic conventions for messaging](https://opentelemetry.io/docs/specs/otel/trace/semantic-conventions/messaging/), inyectando automáticamente campos como `messaging_system`, `messaging_operation`, `messaging_destination_name` y `messaging_message_id` tanto en los headers AMQP como en los log events estructurados.
|
|
218
|
+
|
|
219
|
+
Todos los eventos internos se emiten como logs `key=value` compatibles con Datadog, CloudWatch, ELK y ExisRay.
|
|
218
220
|
|
|
219
221
|
```
|
|
220
|
-
component=bug_bunny event=consumer.message_processed status=200 duration_s=0.012 controller=NodesController action=show
|
|
222
|
+
component=bug_bunny event=consumer.message_processed status=200 duration_s=0.012 messaging_operation=process controller=NodesController action=show
|
|
221
223
|
component=bug_bunny event=consumer.execution_error error_class=RuntimeError error_message="..." duration_s=0.003
|
|
222
224
|
component=bug_bunny event=consumer.connection_error attempt_count=2 retry_in_s=10 error_message="..."
|
|
223
225
|
```
|
|
224
226
|
|
|
225
|
-
|
|
227
|
+
Las claves sensibles (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) se filtran automáticamente a `[FILTERED]` en toda la salida de logs.
|
|
226
228
|
|
|
227
229
|
---
|
|
228
230
|
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -152,6 +152,15 @@ module BugBunny
|
|
|
152
152
|
def process_message(delivery_info, properties, body)
|
|
153
153
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
154
154
|
|
|
155
|
+
# Campos OTel semantic conventions para los log events del consumer.
|
|
156
|
+
# Se mergean con ** en los safe_log de recepción y procesamiento.
|
|
157
|
+
otel_fields = BugBunny::OTel.messaging_headers(
|
|
158
|
+
operation: 'process',
|
|
159
|
+
destination: delivery_info.exchange,
|
|
160
|
+
routing_key: delivery_info.routing_key,
|
|
161
|
+
message_id: properties.correlation_id
|
|
162
|
+
)
|
|
163
|
+
|
|
155
164
|
# 1. Validación de Headers (URL path)
|
|
156
165
|
path = properties.type || (properties.headers && properties.headers['path'])
|
|
157
166
|
|
|
@@ -166,7 +175,7 @@ module BugBunny
|
|
|
166
175
|
http_method = (headers_hash['x-http-method'] || headers_hash['method'] || 'GET').to_s.upcase
|
|
167
176
|
|
|
168
177
|
safe_log(:info, 'consumer.message_received', method: http_method, path: path,
|
|
169
|
-
routing_key: delivery_info.routing_key)
|
|
178
|
+
routing_key: delivery_info.routing_key, **otel_fields)
|
|
170
179
|
safe_log(:debug, 'consumer.message_received_body', body: body.truncate(200))
|
|
171
180
|
|
|
172
181
|
# ===================================================================
|
|
@@ -239,10 +248,11 @@ module BugBunny
|
|
|
239
248
|
session.channel.ack(delivery_info.delivery_tag)
|
|
240
249
|
|
|
241
250
|
safe_log(:info, 'consumer.message_processed',
|
|
242
|
-
|
|
251
|
+
response_status: response_payload[:status],
|
|
243
252
|
duration_s: duration_s(start_time),
|
|
244
253
|
controller: controller_class_name,
|
|
245
|
-
action: route_info[:action]
|
|
254
|
+
action: route_info[:action],
|
|
255
|
+
**otel_fields)
|
|
246
256
|
rescue StandardError => e
|
|
247
257
|
safe_log(:error, 'consumer.execution_error', duration_s: duration_s(start_time), **exception_metadata(e))
|
|
248
258
|
safe_log(:debug, 'consumer.execution_error_backtrace', backtrace: e.backtrace.first(5).join(' | '))
|
|
@@ -257,14 +267,20 @@ module BugBunny
|
|
|
257
267
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
258
268
|
# @return [void]
|
|
259
269
|
def reply(payload, reply_to, correlation_id)
|
|
260
|
-
safe_log(:debug, 'consumer.rpc_reply', reply_to: reply_to,
|
|
270
|
+
safe_log(:debug, 'consumer.rpc_reply', reply_to: reply_to, messaging_message_id: correlation_id)
|
|
271
|
+
otel_headers = BugBunny::OTel.messaging_headers(
|
|
272
|
+
operation: 'publish',
|
|
273
|
+
destination: '',
|
|
274
|
+
routing_key: reply_to,
|
|
275
|
+
message_id: correlation_id
|
|
276
|
+
)
|
|
261
277
|
extra_headers = BugBunny.configuration.rpc_reply_headers&.call || {}
|
|
262
278
|
session.channel.default_exchange.publish(
|
|
263
279
|
payload.to_json,
|
|
264
280
|
routing_key: reply_to,
|
|
265
281
|
correlation_id: correlation_id,
|
|
266
282
|
content_type: 'application/json',
|
|
267
|
-
headers: extra_headers
|
|
283
|
+
headers: otel_headers.transform_keys(&:to_s).merge(extra_headers)
|
|
268
284
|
)
|
|
269
285
|
end
|
|
270
286
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BugBunny
|
|
4
|
+
# Helpers para emitir campos siguiendo las OTel semantic conventions for messaging.
|
|
5
|
+
# https://opentelemetry.io/docs/specs/otel/trace/semantic-conventions/messaging/
|
|
6
|
+
#
|
|
7
|
+
# Se usa tanto en el lado publisher (inyección en headers AMQP) como en el consumer
|
|
8
|
+
# (enriquecimiento de log events estructurados). Centraliza las claves para evitar
|
|
9
|
+
# strings mágicos dispersos y facilitar los tests.
|
|
10
|
+
module OTel
|
|
11
|
+
# Clave: sistema de mensajería. Siempre `"rabbitmq"` en BugBunny.
|
|
12
|
+
# Flat-naming siguiendo el patrón de ExisRay (underscore sin dots).
|
|
13
|
+
SYSTEM = :messaging_system
|
|
14
|
+
# Clave: tipo de operación (`publish`, `receive`, `process`).
|
|
15
|
+
OPERATION = :messaging_operation
|
|
16
|
+
# Clave: nombre del exchange destino.
|
|
17
|
+
DESTINATION = :messaging_destination_name
|
|
18
|
+
# Clave: routing key del mensaje (específica de RabbitMQ).
|
|
19
|
+
ROUTING_KEY = :messaging_routing_key
|
|
20
|
+
# Clave: identificador único del mensaje. En BugBunny se mapea a `correlation_id`.
|
|
21
|
+
MESSAGE_ID = :messaging_message_id
|
|
22
|
+
|
|
23
|
+
# Valor constante para {SYSTEM}.
|
|
24
|
+
SYSTEM_VALUE = 'rabbitmq'
|
|
25
|
+
|
|
26
|
+
# Construye el hash de campos OTel para messaging.
|
|
27
|
+
#
|
|
28
|
+
# Los campos son aptos tanto para inyectar en headers AMQP como para mergear
|
|
29
|
+
# en kwargs de log events estructurados.
|
|
30
|
+
#
|
|
31
|
+
# @param operation [String] Una de: `"publish"`, `"receive"`, `"process"`.
|
|
32
|
+
# @param destination [String, nil] Nombre del exchange destino (puede ser `""` para default exchange).
|
|
33
|
+
# @param routing_key [String, nil] Routing key final del mensaje.
|
|
34
|
+
# @param message_id [String, nil] Identificador del mensaje. Se omite si es `nil`.
|
|
35
|
+
# @return [Hash{String=>String}] Hash con los campos OTel de messaging.
|
|
36
|
+
def self.messaging_headers(operation:, destination:, routing_key:, message_id: nil)
|
|
37
|
+
fields = {
|
|
38
|
+
SYSTEM => SYSTEM_VALUE,
|
|
39
|
+
OPERATION => operation,
|
|
40
|
+
DESTINATION => destination.to_s,
|
|
41
|
+
ROUTING_KEY => routing_key.to_s
|
|
42
|
+
}
|
|
43
|
+
fields[MESSAGE_ID] = message_id.to_s if message_id
|
|
44
|
+
fields
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -79,7 +79,7 @@ module BugBunny
|
|
|
79
79
|
begin
|
|
80
80
|
fire(request)
|
|
81
81
|
|
|
82
|
-
safe_log(:debug, 'producer.rpc_waiting',
|
|
82
|
+
safe_log(:debug, 'producer.rpc_waiting', messaging_message_id: cid, timeout_s: wait_timeout)
|
|
83
83
|
|
|
84
84
|
# Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
|
|
85
85
|
result = future.value(wait_timeout)
|
|
@@ -88,7 +88,8 @@ module BugBunny
|
|
|
88
88
|
|
|
89
89
|
BugBunny.configuration.on_rpc_reply&.call(result[:headers])
|
|
90
90
|
|
|
91
|
-
safe_log(:debug, 'producer.rpc_response_received',
|
|
91
|
+
safe_log(:debug, 'producer.rpc_response_received',
|
|
92
|
+
messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid)
|
|
92
93
|
|
|
93
94
|
parse_response(result[:body])
|
|
94
95
|
ensure
|
|
@@ -109,13 +110,21 @@ module BugBunny
|
|
|
109
110
|
rk = request.final_routing_key
|
|
110
111
|
id = request.correlation_id
|
|
111
112
|
|
|
113
|
+
otel_fields = BugBunny::OTel.messaging_headers(
|
|
114
|
+
operation: 'publish',
|
|
115
|
+
destination: request.exchange,
|
|
116
|
+
routing_key: rk,
|
|
117
|
+
message_id: id
|
|
118
|
+
)
|
|
119
|
+
|
|
112
120
|
# 📊 LOGGING DE OBSERVABILIDAD: Calculamos las opciones finales para mostrarlas en consola
|
|
113
121
|
final_x_opts = BugBunny::Session::DEFAULT_EXCHANGE_OPTIONS
|
|
114
122
|
.merge(BugBunny.configuration.exchange_options || {})
|
|
115
123
|
.merge(request.exchange_options || {})
|
|
116
124
|
|
|
117
|
-
safe_log(:info, 'producer.publish', method: verb, path: target,
|
|
118
|
-
safe_log(:debug, 'producer.publish_detail',
|
|
125
|
+
safe_log(:info, 'producer.publish', method: verb, path: target, **otel_fields)
|
|
126
|
+
safe_log(:debug, 'producer.publish_detail', messaging_destination_name: request.exchange,
|
|
127
|
+
exchange_opts: final_x_opts)
|
|
119
128
|
safe_log(:debug, 'producer.publish_payload', payload: payload.truncate(300)) if payload.is_a?(String)
|
|
120
129
|
end
|
|
121
130
|
|
data/lib/bug_bunny/request.rb
CHANGED
|
@@ -86,10 +86,22 @@ module BugBunny
|
|
|
86
86
|
# **Importante:** Inyecta el verbo HTTP en los headers bajo la clave `x-http-method`.
|
|
87
87
|
# Esto permite al Consumer enrutar correctamente a la acción del controlador.
|
|
88
88
|
#
|
|
89
|
+
# También inyecta los campos de OTel semantic conventions for messaging
|
|
90
|
+
# (ver {BugBunny::OTel}) con `operation=publish`. Los headers del usuario
|
|
91
|
+
# pueden sobrescribir los valores OTel (escape hatch); `x-http-method`
|
|
92
|
+
# nunca se pisa porque es lo último en el merge.
|
|
93
|
+
#
|
|
89
94
|
# @return [Hash] Opciones listas para pasar a `exchange.publish`.
|
|
90
95
|
def amqp_options
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
otel_headers = BugBunny::OTel.messaging_headers(
|
|
97
|
+
operation: 'publish',
|
|
98
|
+
destination: exchange,
|
|
99
|
+
routing_key: final_routing_key,
|
|
100
|
+
message_id: correlation_id
|
|
101
|
+
)
|
|
102
|
+
# Orden del merge: OTel base -> headers del usuario -> x-http-method (inmutable)
|
|
103
|
+
# OTel keys son symbols internamente; los stringificamos para Bunny AMQP headers.
|
|
104
|
+
final_headers = otel_headers.transform_keys(&:to_s).merge(headers).merge('x-http-method' => method.to_s.upcase)
|
|
93
105
|
|
|
94
106
|
{
|
|
95
107
|
type: final_type,
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative 'bug_bunny/version'
|
|
|
6
6
|
require_relative 'bug_bunny/exception'
|
|
7
7
|
require_relative 'bug_bunny/configuration'
|
|
8
8
|
require_relative 'bug_bunny/observability'
|
|
9
|
+
require_relative 'bug_bunny/otel'
|
|
9
10
|
require_relative 'bug_bunny/routing/route_set'
|
|
10
11
|
require_relative 'bug_bunny/middleware/base'
|
|
11
12
|
require_relative 'bug_bunny/middleware/stack'
|