bug_bunny 4.8.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35c04cd113b0a3056cab1dcfda6d389fa996e13bb44992d4791bdef29b93fc7d
4
- data.tar.gz: aa7d145fb3eb1d80f435c0541e1009df78a636019e6c6452be0e0a0515b1f1a8
3
+ metadata.gz: 3d8e96ff8b1993700ead4fa98d3ce5a2484e6f13b59d63dbd270374101ef8c28
4
+ data.tar.gz: 856e18a1802e069b6446d7bb0960f8efd6e72a93570379bd301097ea5230812d
5
5
  SHA512:
6
- metadata.gz: 9efee5f3deb6a52c76a361230032e040c964e22a1b52a2419873cb3d1c06159a1c52a042f424974a697a591342e8116aea1e39dd57e0bf71aa617ed5245e0860
7
- data.tar.gz: 1f569f9e8b6fdfa2b48dd2270494a9a6b8f8aec2539ebadec5e20bc27f3ccd23407a340891b15c1567fe48ce593ee45a694b843987e5078852bb14d94ad37d70
6
+ metadata.gz: 92ca661889aeb364e72ec79934b1d27a227e8fe7ed71e772298bcc682d53076388481a5ffaa8d0ca51935031acaa664b859b7a5a1fc69f785c210ed9f78c2a22
7
+ data.tar.gz: f2e781eb9b13628edd52cdb0c5aceadc655128bb67c63f70c00775b6c5411429920b46b7d63d1dcb6fe83c2d5eb4ae5e5cb9a4152ab7f13e83274c9341d34fa4
@@ -20,7 +20,7 @@ Ejecutá `quality-code` para validar linting, tests, YARD incremental y skill.
20
20
  No asumas rutas fijas. Investigá el entorno:
21
21
  - Detectá el nombre de la gema del `.gemspec`.
22
22
  - Localizá el archivo de versión (`lib/**/version.rb`).
23
- - Leé la versión actual.
23
+ - **Versión actual:** Obtené el último tag publicado en el remoto (`git tag --sort=-v:refname | head -1` o `git ls-remote --tags origin`). El tag remoto es la fuente de verdad — NO leer `version.rb` para determinar la versión actual, ya que puede estar modificado localmente.
24
24
  - **Análisis de cambios:** Revisá **todas** las fuentes de cambios:
25
25
  1. Commits desde el último tag: `git log [último-tag]...HEAD`
26
26
  2. Diff commiteado contra el tag: `git diff [último-tag]...HEAD`
@@ -50,10 +50,12 @@ No asumas rutas fijas. Investigá el entorno:
50
50
  - `git commit -m "release: v[NUEVA_VERSION]"`
51
51
  - `git tag -a v[NUEVA_VERSION] -m "Version [NUEVA_VERSION]"`
52
52
 
53
- ### Paso 5 — Publicación (Requiere confirmación final)
54
- - Construí la gema: `gem build [nombre].gemspec`
55
- - Empujá a RubyGems: `gem push [nombre]-[NUEVA_VERSION].gem`
56
- - Eliminá el artefacto `.gem` local después de subirlo.
53
+ ### Paso 5 — Push (Requiere confirmación)
54
+ Mostrá un resumen del commit y el tag creados. Esperá confirmación explícita antes de pushear.
55
+ - `git push origin main`
56
+ - `git push origin v[NUEVA_VERSION]`
57
+
58
+ **Nota:** No es necesario hacer `gem build` ni `gem push` manualmente. Un GitHub Action se encarga de buildear y publicar la gema en RubyGems cuando detecta el tag.
57
59
 
58
60
  ---
59
61
 
@@ -210,11 +210,72 @@ Si el catálogo es extenso, mantener los errores más comunes acá y extraer el
210
210
 
211
211
  ## Paso 4 — Actualizar README.md
212
212
 
213
- Actualizá `README.md` (máx 150 líneas) con:
214
- - Descripción en una línea.
215
- - Setup básico y Quick start.
216
- - Features principales.
217
- - Link a `skill/SKILL.md` para conocimiento profundo.
213
+ Invocá la skill `documentation-writer` para auditar y actualizar el README.
214
+
215
+ **Regla fundamental:** El README es para **humanos** (devs que usan la gema/servicio). La skill (`skill/`) es para **agentes**. Son audiencias distintas. **Nunca referenciar `skill/` desde el README.**
216
+
217
+ ### README de una gema (máx 150 líneas)
218
+
219
+ ```markdown
220
+ # [Nombre de la gema]
221
+
222
+ Descripción en una línea.
223
+
224
+ ## Instalación
225
+
226
+ gem 'nombre', '~> X.X'
227
+
228
+ ## Quick Start
229
+
230
+ [Ejemplo mínimo funcional — copiar, pegar, funciona]
231
+
232
+ ## Uso
233
+
234
+ [Ejemplos de las operaciones principales]
235
+
236
+ ## Configuración
237
+
238
+ [Bloque de configuración con opciones, defaults y descripción]
239
+
240
+ ## Contribuir
241
+
242
+ [Cómo correr tests, linting, etc.]
243
+ ```
244
+
245
+ ### README de un servicio (máx 150 líneas)
246
+
247
+ ```markdown
248
+ # [Nombre del servicio]
249
+
250
+ Descripción en una línea.
251
+
252
+ ## Setup
253
+
254
+ [Pasos para levantar el servicio localmente: bin/setup, docker, etc.]
255
+
256
+ ## Endpoints / Contratos
257
+
258
+ [Resumen de los endpoints o queues principales]
259
+
260
+ ## Variables de entorno
261
+
262
+ [Lista de env vars necesarias]
263
+
264
+ ## Testing
265
+
266
+ [Cómo correr tests]
267
+
268
+ ## Deploy
269
+
270
+ [Cómo se despliega: branch, tag, Codefresh]
271
+ ```
272
+
273
+ ### Qué NO poner en el README
274
+ - Links a `skill/` ni a `skill/SKILL.md`
275
+ - Documentación interna para agentes
276
+ - Diagramas ASCII extensos (esos van en la skill)
277
+ - Catálogo completo de errores (eso va en la skill)
278
+ - FAQ técnico detallado (eso va en la skill)
218
279
 
219
280
  ---
220
281
 
@@ -53,41 +53,89 @@ Si no existe `skills.yml` en la raíz, crealo detectando dependencias en el `Gem
53
53
  # skills.yml — Manifiesto único de skills del proyecto
54
54
 
55
55
  # --- MCPs requeridos ---
56
- # Declara qué MCPs necesita el proyecto. Las skills verifican
57
- # disponibilidad antes de usarlos. Si falta alguno, avisan pero no se rompen.
56
+ # Declara qué MCPs necesita el proyecto.
58
57
 
59
58
  mcps:
60
59
  - github
61
60
  - clickup
62
61
 
63
- # --- Dependencias (sync) ---
62
+ # --- Gemas ---
63
+ # Array de nombres. El sync busca skill/ en cada gema instalada.
64
64
 
65
- gems: # Skills empaquetadas en gemas (copia local)
66
- - name: mi_gema
65
+ gems:
66
+ - mi_gema
67
+ - otra_gema
67
68
 
68
- services: # Skills de microservicios (GitHub → skill/)
69
- - name: mi_servicio
69
+ # --- Servicios ---
70
+ # Hash { nombre => config }. Descarga skill/ del repo remoto.
71
+
72
+ services:
73
+ mi_servicio:
70
74
  repo: wispro/mi_servicio
71
75
 
72
- skills: # Skills de repos GitHub (con path opcional)
73
- # Sin path usa convención .agents/skills/[name]/
74
- - name: gem-release
75
- repo: wispro/ai_knowledge
76
- - name: skill-manager
77
- repo: wispro/ai_knowledge
78
- # Con path para skills.sh u otros repos con estructura distinta
79
- - name: rabbitmq-expert
76
+ # --- Skills ---
77
+ # Hash { nombre => config }. Formato estilo docker-compose.
78
+ # Cada skill es una clave con su configuración.
79
+ #
80
+ # Claves disponibles:
81
+ # - repo (requerido): org/repo de GitHub
82
+ # - scope (opcional): global | local (default: local)
83
+ # - path (opcional): path custom en el repo (default: skills/[nombre])
84
+ # - environment (opcional): configuración específica de la skill
85
+
86
+ skills:
87
+ skill-manager:
88
+ repo: sequre/ai_knowledge
89
+ scope: global
90
+ quality-code:
91
+ repo: sequre/ai_knowledge
92
+ scope: global
93
+ gem-release:
94
+ repo: sequre/ai_knowledge
95
+ service-release:
96
+ repo: sequre/ai_knowledge
97
+ skill-builder:
98
+ repo: sequre/ai_knowledge
99
+ yard:
100
+ repo: sequre/ai_knowledge
101
+ sentry:
102
+ repo: sequre/ai_knowledge
103
+ environment:
104
+ url: "https://sentry.cloud.wispro.co"
105
+ org: "wispro"
106
+ projects:
107
+ - billing-api
108
+ - billing-workers
109
+ agent-review:
110
+ repo: sequre/ai_knowledge
111
+ environment:
112
+ space_id: "90144913465"
113
+ list_id: "901415149921"
114
+ ai-reports:
115
+ repo: sequre/ai_knowledge
116
+ environment:
117
+ space_id: "90144913465"
118
+ bug_reports_list_id: "901415148810"
119
+ improvements_list_id: "901415148812"
120
+ # Skills externas con path custom
121
+ documentation-writer:
122
+ repo: github/awesome-copilot
123
+ path: skills/documentation-writer
124
+ rabbitmq-expert:
80
125
  repo: martinholovsky/claude-skills-generator
81
126
  path: skills/rabbitmq-expert
127
+ ```
82
128
 
83
- # --- Configuración de skills ---
84
- # Cada skill puede tener su sección de configuración específica.
85
- # Las skills leen su sección del skills.yml del proyecto.
129
+ ### Variables de entorno en skills.yml
130
+ El parser expande `${VAR}` con el valor de la variable de entorno. Útil para tokens o IDs sensibles:
86
131
 
87
- sentry:
88
- projects:
89
- - billing-api
90
- - billing-workers
132
+ ```yaml
133
+ skills:
134
+ ai-reports:
135
+ repo: sequre/ai_knowledge
136
+ environment:
137
+ space_id: "${CLICKUP_SPACE_ID}"
138
+ bug_reports_list_id: "${CLICKUP_BUG_REPORTS_LIST}"
91
139
  ```
92
140
 
93
141
  *Mostrá diff y pedí confirmación.*
@@ -97,14 +145,17 @@ Asegurate de que el script de sync esté configurado para ejecutarse:
97
145
  - Agregá al final de `bin/setup`:
98
146
  ```bash
99
147
  ruby .agents/skills/skill-manager/scripts/sync.rb
148
+
100
149
  ```
101
150
 
102
151
  ### Paso 6 — Git
103
- - Agregá las skills de dependencias al `.gitignore` (cada entrada declarada en `skills.yml`):
152
+ - Agregá `.agents/skills/` completo al `.gitignore`:
104
153
  ```gitignore
105
- # Skills de dependencias (generadas por skill-manager sync)
106
- .agents/skills/[dep-name]/
154
+ # Skills locales (descargadas por sync + propias del dev)
155
+ .agents/skills/
107
156
  ```
157
+ - `.agents/skills/` es siempre local y no se commitea. Contiene skills descargadas por el sync y opcionalmente skills privadas del dev.
158
+ - Si una skill debe ser compartida, se declara en `skills.yml` y se distribuye via sync.
108
159
 
109
160
  ### Paso 7 — CLAUDE.md
110
161
  El bloque "Knowledge Base" debe estar al **tope absoluto** del archivo:
@@ -112,7 +163,8 @@ El bloque "Knowledge Base" debe estar al **tope absoluto** del archivo:
112
163
  ## Knowledge Base
113
164
  - **Mandato Crítico:** Las skills en `.agents/skills/` incluyen conocimiento de dependencias.
114
165
  - **Protocolo de Consulta:** El agente DEBE leer la skill de una dependencia antes de responder sobre ella.
115
- - **Rebuild:** `ruby .agents/skills/skill-manager/scripts/sync.rb` actualiza las skills de dependencias.
166
+ - **Rebuild:** `ruby .agents/skills/skill-manager/scripts/sync.rb
167
+ ` actualiza las skills de dependencias.
116
168
  ```
117
169
 
118
170
  ---
@@ -125,11 +177,12 @@ El script `scripts/sync.rb` lee `skills.yml` y sincroniza todas las skills de de
125
177
  |---|---|---|
126
178
  | `gems` | Gema Ruby instalada | Copia local desde `gem_dir/skill/` |
127
179
  | `services` | Repo de microservicio | GitHub API → `skill/` del repo |
128
- | `skills` | Repo GitHub | GitHub API → path configurable (default: `.agents/skills/[name]/`) |
180
+ | `skills` | Repo GitHub | GitHub API → path configurable (default: `skills/[name]/`) |
129
181
 
130
182
  ### Ejecución directa
131
183
  ```bash
132
184
  ruby .agents/skills/skill-manager/scripts/sync.rb
185
+
133
186
  ```
134
187
 
135
188
  ### Requisitos del script
@@ -27,7 +27,7 @@ module SkillsSync
27
27
  exit 1
28
28
  end
29
29
 
30
- config = YAML.safe_load_file(config_path)
30
+ config = load_config(config_path)
31
31
  @local_skills_path = File.join(Dir.pwd, SKILLS_DIR)
32
32
  FileUtils.mkdir_p(@local_skills_path)
33
33
 
@@ -53,6 +53,15 @@ module SkillsSync
53
53
 
54
54
  private
55
55
 
56
+ # --- Config parsing ---
57
+
58
+ # Lee skills.yml y expande variables de entorno ${VAR}
59
+ def load_config(path)
60
+ content = File.read(path)
61
+ content = content.gsub(/\$\{(\w+)\}/) { ENV[Regexp.last_match(1)].to_s }
62
+ YAML.safe_load(content)
63
+ end
64
+
56
65
  # --- Lock file ---
57
66
 
58
67
  def load_lock
@@ -96,6 +105,9 @@ module SkillsSync
96
105
 
97
106
  # --- Scope resolution ---
98
107
 
108
+ # Retorna un hash { dest:, install: } donde install indica si hay que descargar.
109
+ # Si install es false (porque ya existe globalmente), dest contiene el path global
110
+ # para que igualmente se registre en el lock y no se borre en cleanup.
99
111
  def resolve_dest(name, scope)
100
112
  case scope
101
113
  when 'global'
@@ -109,16 +121,17 @@ module SkillsSync
109
121
  FileUtils.rm_rf(local_path)
110
122
  end
111
123
 
112
- dest
124
+ { dest: dest, install: true }
113
125
  else # local o sin especificar
114
- # Si existe global, saltear
126
+ # Si existe global, saltear la instalación pero registrar en el lock
115
127
  existing_global = find_global_path(name)
116
128
  if existing_global
117
- puts " #{name} — disponible globalmente en #{File.join(existing_global, name)}. Saltando."
118
- return nil
129
+ global_dest = File.join(existing_global, name)
130
+ puts " #{name} — disponible globalmente en #{global_dest}. Saltando."
131
+ return { dest: global_dest, install: false }
119
132
  end
120
133
 
121
- File.join(@local_skills_path, name)
134
+ { dest: File.join(@local_skills_path, name), install: true }
122
135
  end
123
136
  end
124
137
 
@@ -139,12 +152,18 @@ module SkillsSync
139
152
 
140
153
  # --- Sync methods ---
141
154
 
155
+ # Nuevo formato: gems es un array de strings (nombres de gemas)
142
156
  def sync_gems(gems)
143
- gems.each do |gem_config|
144
- name = gem_config['name']
145
- scope = gem_config['scope']
146
- dest = resolve_dest(name, scope)
147
- next unless dest
157
+ return unless gems.is_a?(Array)
158
+
159
+ gems.each do |name|
160
+ resolution = resolve_dest(name, 'local')
161
+ next unless resolution
162
+
163
+ unless resolution[:install]
164
+ record_lock(name, 'local', resolution[:dest])
165
+ next
166
+ end
148
167
 
149
168
  begin
150
169
  spec = Gem::Specification.find_by_name(name)
@@ -161,44 +180,71 @@ module SkillsSync
161
180
  next
162
181
  end
163
182
 
164
- scope_label = scope == 'global' ? 'global' : 'local'
165
- puts " #{name} v#{spec.version} (#{scope_label})"
166
- replace_dir(dest)
167
- FileUtils.cp_r(Dir[File.join(skill_dir, '*')], dest)
168
- record_lock(name, scope, dest)
183
+ puts " #{name} v#{spec.version} (local)"
184
+ replace_dir(resolution[:dest])
185
+ FileUtils.cp_r(Dir[File.join(skill_dir, '*')], resolution[:dest])
186
+ record_lock(name, 'local', resolution[:dest])
169
187
  end
170
188
  end
171
189
 
190
+ # Nuevo formato: services es un Hash { nombre => config }
191
+ # Cada servicio tiene: repo (requerido), scope (opcional)
172
192
  def sync_services(services)
173
- services.each do |service|
174
- name = service['name']
175
- scope = service['scope']
176
- dest = resolve_dest(name, scope)
177
- next unless dest
193
+ return unless services.is_a?(Hash)
194
+
195
+ services.each do |name, service_config|
196
+ service_config ||= {}
197
+ scope = service_config['scope']
198
+ resolution = resolve_dest(name, scope)
199
+ next unless resolution
200
+
201
+ unless resolution[:install]
202
+ record_lock(name, scope, resolution[:dest])
203
+ next
204
+ end
205
+
206
+ repo = service_config['repo']
207
+ unless repo
208
+ puts " WARNING: service '#{name}' no tiene 'repo' definido. Saltando."
209
+ next
210
+ end
178
211
 
179
- repo = service['repo']
180
212
  scope_label = scope == 'global' ? 'global' : 'local'
181
213
  puts " #{name} (GitHub: #{repo}, skill/) [#{scope_label}]"
182
- replace_dir(dest)
183
- download_github_dir(repo, 'main', 'skill', dest)
184
- record_lock(name, scope, dest)
214
+ replace_dir(resolution[:dest])
215
+ download_github_dir(repo, 'main', 'skill', resolution[:dest])
216
+ record_lock(name, scope, resolution[:dest])
185
217
  end
186
218
  end
187
219
 
220
+ # Nuevo formato: skills es un Hash { nombre => config }
221
+ # Cada skill tiene: repo (requerido), scope (opcional), path (opcional), environment (opcional)
188
222
  def sync_skills(skills)
189
- skills.each do |skill|
190
- name = skill['name']
191
- scope = skill['scope']
192
- dest = resolve_dest(name, scope)
193
- next unless dest
194
-
195
- repo = skill['repo']
196
- remote_path = skill['path'] || "#{SKILLS_DIR}/#{name}"
223
+ return unless skills.is_a?(Hash)
224
+
225
+ skills.each do |name, skill_config|
226
+ skill_config ||= {}
227
+ scope = skill_config['scope']
228
+ resolution = resolve_dest(name, scope)
229
+ next unless resolution
230
+
231
+ unless resolution[:install]
232
+ record_lock(name, scope, resolution[:dest])
233
+ next
234
+ end
235
+
236
+ repo = skill_config['repo']
237
+ unless repo
238
+ puts " WARNING: skill '#{name}' no tiene 'repo' definido. Saltando."
239
+ next
240
+ end
241
+
242
+ remote_path = skill_config['path'] || "skills/#{name}"
197
243
  scope_label = scope == 'global' ? 'global' : 'local'
198
244
  puts " #{name} (GitHub: #{repo}, #{remote_path}/) [#{scope_label}]"
199
- replace_dir(dest)
200
- download_github_dir(repo, 'main', remote_path, dest)
201
- record_lock(name, scope, dest)
245
+ replace_dir(resolution[:dest])
246
+ download_github_dir(repo, 'main', remote_path, resolution[:dest])
247
+ record_lock(name, scope, resolution[:dest])
202
248
  end
203
249
  end
204
250
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.9.0] - 2026-04-05
4
+
5
+ ### ✨ New Features
6
+ * **OTel messaging semantic conventions:** BugBunny ahora emite los campos del estándar [OpenTelemetry semantic conventions for messaging](https://opentelemetry.io/docs/specs/otel/trace/semantic-conventions/messaging/) tanto en los headers AMQP de publish/reply como en los log events del consumer. Los campos emitidos son `messaging.system` (`"rabbitmq"`), `messaging.operation` (`"publish"` / `"process"`), `messaging.destination.name`, `messaging.rabbitmq.destination.routing_key` y `messaging.message.id` (cuando hay `correlation_id`). Permite que dashboards OTel-native (Tempo, Jaeger, Honeycomb) rendericen correctamente los spans de RabbitMQ y que ExisRay los consuma automáticamente desde `properties.headers`.
7
+ * **`BugBunny::OTel` module:** Nuevo módulo con las constantes de las claves OTel y el helper `messaging_headers` para construir el hash de campos. Los headers del usuario pueden sobrescribir valores OTel como escape hatch, pero `x-http-method` sigue siendo inmutable.
8
+
3
9
  ## [4.8.1] - 2026-04-04
4
10
 
5
11
  ### Mejoras internas
data/CLAUDE.md CHANGED
@@ -6,32 +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
+ ## Documentación
10
+
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.
14
+
9
15
  ## Knowledge Base
10
16
  - Las skills en `.agents/skills/` incluyen conocimiento de dependencias.
11
17
  - Leer la skill de una dependencia ANTES de responder sobre ella.
12
18
  - Rebuild: `ruby .agents/skills/skill-manager/scripts/sync.rb`
13
19
 
14
- ## Entorno
20
+ ### Entorno
15
21
  - Versión de Ruby: leer `.ruby-version`
16
22
  - Versión de Rails y gemas: leer `Gemfile.lock`
17
23
  - Gestor de Ruby: chruby (no usar rvm ni rbenv)
18
24
  - Package manager: Bundler
19
25
 
20
- ## RuboCop
26
+ ### RuboCop
21
27
  - Usamos rubocop-rails-omakase como base.
22
28
  - Correr `bundle exec rubocop -a` antes de commitear.
23
29
  - No deshabilitar cops sin justificación en el PR.
24
30
 
25
- ## YARD
31
+ ### YARD
26
32
  - Documentación incremental: si tocás un método, documentalo con YARD.
27
33
  - Consultar la skill `yard` para tags y tipos correctos.
28
34
  - Verificar cobertura: `bundle exec yard stats --list-undoc`
29
35
 
30
- ## Testing
36
+ ### Testing
31
37
  - Framework: RSpec
32
38
  - Correr: `bundle exec rspec`
33
39
  - Todo código nuevo debe tener tests.
34
40
 
35
- ## Releases
36
- - Gemas: `/gem-release`
37
- - Servicios: `/service-release build` o `/service-release deploy`
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