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/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
- ## Knowledge Base
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
- | Evento | Nivel | Cuándo |
126
- |---|---|---|
127
- | `consumer.start` | INFO | Consumer inicia subscribe |
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
- ### Campos estándar
138
-
139
- ```ruby
140
- safe_log(:error, "clase.error", **exception_metadata(e))
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
- ## Standards de Código
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
- Esta gema usa **rubocop-rails-omakase**. Todo código nuevo o modificado debe cumplir.
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
- Todo método público nuevo o modificado lleva documentación YARD:
169
-
170
- ```ruby
171
- # Descripción breve.
172
- #
173
- # Descripción extendida si es necesario.
174
- #
175
- # @param name [Type] Descripción
176
- # @return [Type] Descripción
177
- # @raise [ErrorClass] Cuándo se lanza
178
- # @example
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
- All internal events are emitted as structured key=value logs compatible with Datadog, CloudWatch, and ELK.
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
- Sensitive keys (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) are automatically filtered to `[FILTERED]` in all log output.
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
 
@@ -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
- status: response_payload[:status],
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, correlation_id: correlation_id)
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
@@ -79,7 +79,7 @@ module BugBunny
79
79
  begin
80
80
  fire(request)
81
81
 
82
- safe_log(:debug, 'producer.rpc_waiting', correlation_id: cid, timeout_s: wait_timeout)
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', correlation_id: cid)
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, routing_key: rk, correlation_id: id)
118
- safe_log(:debug, 'producer.publish_detail', exchange: request.exchange, exchange_opts: final_x_opts)
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
 
@@ -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
- # Inyectamos el verbo HTTP en los headers para el Router del Consumer
92
- final_headers = headers.merge('x-http-method' => method.to_s.upcase)
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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.8.0'
4
+ VERSION = '4.9.0'
5
5
  end
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'