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 +4 -4
- data/.agents/skills/gem-release/SKILL.md +7 -5
- data/.agents/skills/skill-builder/SKILL.md +66 -5
- data/.agents/skills/skill-manager/SKILL.md +80 -27
- data/.agents/skills/skill-manager/scripts/sync.rb +82 -36
- data/CHANGELOG.md +6 -0
- data/CLAUDE.md +13 -7
- 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 +25 -2
- data/skill/references/client-middleware.md +17 -0
- data/skill/references/consumer.md +25 -7
- data/skills.lock +10 -4
- data/skills.yml +30 -9
- 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 +7 -4
- data/.agents/skills/rabbitmq-expert/SKILL.md +0 -1555
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d8e96ff8b1993700ead4fa98d3ce5a2484e6f13b59d63dbd270374101ef8c28
|
|
4
|
+
data.tar.gz: 856e18a1802e069b6446d7bb0960f8efd6e72a93570379bd301097ea5230812d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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 —
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
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
|
-
# ---
|
|
62
|
+
# --- Gemas ---
|
|
63
|
+
# Array de nombres. El sync busca skill/ en cada gema instalada.
|
|
64
64
|
|
|
65
|
-
gems:
|
|
66
|
-
-
|
|
65
|
+
gems:
|
|
66
|
+
- mi_gema
|
|
67
|
+
- otra_gema
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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á
|
|
152
|
+
- Agregá `.agents/skills/` completo al `.gitignore`:
|
|
104
153
|
```gitignore
|
|
105
|
-
# Skills
|
|
106
|
-
.agents/skills/
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
118
|
-
|
|
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.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
next unless
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
### Testing
|
|
31
37
|
- Framework: RSpec
|
|
32
38
|
- Correr: `bundle exec rspec`
|
|
33
39
|
- Todo código nuevo debe tener tests.
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
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
|