bug_bunny 4.5.3 → 4.6.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/CHANGELOG.md +16 -0
- data/README.md +53 -0
- data/lib/bug_bunny/configuration.rb +21 -0
- data/lib/bug_bunny/consumer.rb +14 -9
- data/lib/bug_bunny/consumer_middleware.rb +82 -0
- data/lib/bug_bunny/observability.rb +3 -1
- data/lib/bug_bunny/producer.rb +8 -5
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +7 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c9f75619decb8d892d2bfe8c56424822672b6b6b5f3be85442be1399c38e76a
|
|
4
|
+
data.tar.gz: 7fc1e66a10f46d0a6120dd6164b10b0f1b30d2ff91e8d5daed4358f771f2cc49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd5f4141f5a7892f84d5b3894535632fe63ce24320637a5896530b025c4f3dc8f002f75c41f7cbec57258bca857d58cd1d531c0abf3d6f2047e049cc6119ae98
|
|
7
|
+
data.tar.gz: c13414d24ee769acb483fe4a9ee1f9aa2073ea6bf8bea81e1ac290499ad45cd85471ffd022a304bb6da9cc56e737f868bc441870a1b084b05a60892c6f3b6366
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.6.0] - 2026-03-31
|
|
4
|
+
|
|
5
|
+
### ✨ New Features
|
|
6
|
+
* **Consumer Middleware Stack:** Se introdujo `BugBunny::ConsumerMiddleware::Stack`, un pipeline de middlewares que se ejecuta antes de que la gema procese cada mensaje AMQP (antes del primer log `consumer.message_received`). Es el punto de extensión oficial para tracing distribuido, autenticación y auditoría a nivel de consumer.
|
|
7
|
+
* Clase base `BugBunny::ConsumerMiddleware::Base` con interfaz `call(delivery_info, properties, body)`.
|
|
8
|
+
* Acceso directo via `BugBunny.consumer_middlewares.use MyMiddleware`.
|
|
9
|
+
* Soporte de **auto-registro transparente**: gemas externas como `exis_ray` pueden registrarse al ser requeridas sin que el usuario toque el bloque `configure`.
|
|
10
|
+
* Orden de ejecución FIFO. Sin middlewares registrados, el overhead es cero.
|
|
11
|
+
* **RPC Trace Propagation (bidireccional):** BugBunny ahora propaga trace context en ambas direcciones del ciclo RPC:
|
|
12
|
+
* `config.rpc_reply_headers` (Proc) — callback invocado en el consumer justo antes del `basic_publish` del reply. Retorna headers AMQP a inyectar en la respuesta (ej: `X-Amzn-Trace-Id` actualizado con el span del consumer).
|
|
13
|
+
* `config.on_rpc_reply` (Proc) — callback invocado en el thread llamante del publisher tras recibir el reply, con los headers AMQP de la respuesta. Permite hidratar trace context en el publisher sin exponer los headers en la interfaz pública del método `rpc`.
|
|
14
|
+
* Ejemplo consumer: `config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => ExisRay::Tracer.generate_trace_header } }`
|
|
15
|
+
* Ejemplo publisher: `config.on_rpc_reply = ->(headers) { ExisRay::Tracer.hydrate(headers['X-Amzn-Trace-Id']) }` Retorna un Hash de headers AMQP que se inyectan en la respuesta, permitiendo propagar trace context generado por el consumer (ej: `X-Amzn-Trace-Id` actualizado). Cero overhead cuando no está configurado.
|
|
16
|
+
* Ejemplo: `config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => ExisRay::Tracer.generate_trace_header } }`
|
|
17
|
+
* **Observability — Hash quoting:** `safe_log` ahora aplica la misma regla de quoting a valores `Hash` que a `String`: si el JSON contiene espacios, se inspecciona; si no, se emite sin comillas, facilitando el parseo automático en motores de logs.
|
|
18
|
+
|
|
3
19
|
## [4.5.3] - 2026-03-30
|
|
4
20
|
|
|
5
21
|
### 🐛 Bug Fixes
|
data/README.md
CHANGED
|
@@ -206,6 +206,59 @@ end
|
|
|
206
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
209
|
+
## 🔗 Consumer Middleware Stack
|
|
210
|
+
|
|
211
|
+
BugBunny expone un middleware stack que se ejecuta **antes** de que la gema procese cada mensaje entrante (antes del primer log `consumer.message_received`). Es el punto ideal para hidratar contexto de tracing distribuido, autenticación, auditoría, etc.
|
|
212
|
+
|
|
213
|
+
### Implementar un middleware
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class MyMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
217
|
+
def call(delivery_info, properties, body)
|
|
218
|
+
# lógica pre-procesamiento
|
|
219
|
+
# properties.headers contiene todos los headers AMQP custom
|
|
220
|
+
@app.call(delivery_info, properties, body)
|
|
221
|
+
# lógica post-procesamiento (opcional)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Registrar un middleware
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
BugBunny.configure do |config|
|
|
230
|
+
# ...
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
BugBunny.consumer_middlewares.use MyMiddleware
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Auto-registro desde una gema externa
|
|
237
|
+
|
|
238
|
+
Las gemas de integración pueden registrarse automáticamente al ser requeridas, sin que el usuario tenga que tocar el bloque `configure`:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# lib/exis_ray/bug_bunny/consumer_tracing.rb
|
|
242
|
+
require 'exis_ray/bug_bunny/consumer_tracing_middleware'
|
|
243
|
+
BugBunny.consumer_middlewares.use ExisRay::BugBunny::ConsumerTracingMiddleware
|
|
244
|
+
|
|
245
|
+
# El usuario solo necesita:
|
|
246
|
+
# require 'exis_ray/bug_bunny/consumer_tracing'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Datos disponibles en el middleware
|
|
250
|
+
|
|
251
|
+
| Argumento | Tipo | Contenido |
|
|
252
|
+
|---|---|---|
|
|
253
|
+
| `delivery_info` | `Bunny::DeliveryInfo` | `routing_key`, `exchange`, `delivery_tag` |
|
|
254
|
+
| `properties` | `Bunny::MessageProperties` | `headers` (headers AMQP custom), `correlation_id`, `reply_to`, `content_type` |
|
|
255
|
+
| `body` | `String` | Payload crudo del mensaje |
|
|
256
|
+
|
|
257
|
+
> **Orden de ejecución:** FIFO — el primero en registrarse es el primero en ejecutarse.
|
|
258
|
+
> `Middleware A → Middleware B → process_message`
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
209
262
|
## 🔎 Observabilidad y Tracing
|
|
210
263
|
|
|
211
264
|
BugBunny implementa Distributed Tracing nativo y sigue los estándares de observabilidad de **ExisRay** para logs estructurados.
|
|
@@ -96,6 +96,23 @@ module BugBunny
|
|
|
96
96
|
# @example { durable: true, exclusive: false }
|
|
97
97
|
attr_accessor :queue_options
|
|
98
98
|
|
|
99
|
+
# @return [BugBunny::ConsumerMiddleware::Stack] Stack de middlewares ejecutados antes de procesar cada mensaje.
|
|
100
|
+
# Los middlewares se registran con {BugBunny::ConsumerMiddleware::Stack#use}.
|
|
101
|
+
attr_reader :consumer_middlewares
|
|
102
|
+
|
|
103
|
+
# @return [Proc, nil] Callback invocado justo antes del `basic_publish` del reply RPC.
|
|
104
|
+
# Debe retornar un Hash de headers AMQP a inyectar en la respuesta.
|
|
105
|
+
# Ideal para propagar trace context (ej: X-Amzn-Trace-Id) generado por el consumer.
|
|
106
|
+
# @example
|
|
107
|
+
# config.rpc_reply_headers = -> { { 'X-Amzn-Trace-Id' => ExisRay::Tracer.generate_trace_header } }
|
|
108
|
+
attr_accessor :rpc_reply_headers
|
|
109
|
+
|
|
110
|
+
# @return [Proc, nil] Callback invocado en el thread llamante tras recibir el reply RPC,
|
|
111
|
+
# con los headers AMQP de la respuesta. Permite hidratar trace context en el publisher.
|
|
112
|
+
# @example
|
|
113
|
+
# config.on_rpc_reply = ->(headers) { ExisRay::Tracer.hydrate(headers['X-Amzn-Trace-Id']) }
|
|
114
|
+
attr_accessor :on_rpc_reply
|
|
115
|
+
|
|
99
116
|
# @!endgroup
|
|
100
117
|
|
|
101
118
|
# Inicializa la configuración con valores por defecto seguros.
|
|
@@ -135,6 +152,10 @@ module BugBunny
|
|
|
135
152
|
# Inicialización de opciones de infraestructura como hashes vacíos para permitir fusiones posteriores.
|
|
136
153
|
@exchange_options = {}
|
|
137
154
|
@queue_options = {}
|
|
155
|
+
|
|
156
|
+
@consumer_middlewares = ConsumerMiddleware::Stack.new
|
|
157
|
+
@rpc_reply_headers = nil
|
|
158
|
+
@on_rpc_reply = nil
|
|
138
159
|
end
|
|
139
160
|
|
|
140
161
|
# Construye la URL de conexión AMQP basada en los atributos configurados.
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -87,16 +87,19 @@ module BugBunny
|
|
|
87
87
|
|
|
88
88
|
q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
|
|
89
89
|
trace_id = properties.correlation_id
|
|
90
|
-
|
|
91
90
|
logger = BugBunny.configuration.logger
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
logger.tagged
|
|
95
|
-
|
|
96
|
-
Rails.logger.tagged
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
core = -> {
|
|
93
|
+
if logger.respond_to?(:tagged)
|
|
94
|
+
logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
|
|
95
|
+
elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
|
|
96
|
+
Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
|
|
97
|
+
else
|
|
98
|
+
process_message(delivery_info, properties, body)
|
|
99
|
+
end
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
BugBunny.configuration.consumer_middlewares.call(delivery_info, properties, body, &core)
|
|
100
103
|
end
|
|
101
104
|
rescue StandardError => e
|
|
102
105
|
attempt += 1
|
|
@@ -241,11 +244,13 @@ module BugBunny
|
|
|
241
244
|
# @return [void]
|
|
242
245
|
def reply(payload, reply_to, correlation_id)
|
|
243
246
|
safe_log(:debug, "consumer.rpc_reply", reply_to: reply_to, correlation_id: correlation_id)
|
|
247
|
+
extra_headers = BugBunny.configuration.rpc_reply_headers&.call || {}
|
|
244
248
|
session.channel.default_exchange.publish(
|
|
245
249
|
payload.to_json,
|
|
246
250
|
routing_key: reply_to,
|
|
247
251
|
correlation_id: correlation_id,
|
|
248
|
-
content_type: 'application/json'
|
|
252
|
+
content_type: 'application/json',
|
|
253
|
+
headers: extra_headers
|
|
249
254
|
)
|
|
250
255
|
end
|
|
251
256
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BugBunny
|
|
4
|
+
# Infraestructura de middleware para el Consumer AMQP.
|
|
5
|
+
#
|
|
6
|
+
# Permite inyectar lógica transversal (tracing, autenticación, logging) en el
|
|
7
|
+
# pipeline de procesamiento de mensajes, antes de que la gema procese el mensaje.
|
|
8
|
+
#
|
|
9
|
+
# Cada middleware recibe `(delivery_info, properties, body)` y debe llamar a
|
|
10
|
+
# `@app.call(delivery_info, properties, body)` para continuar la cadena.
|
|
11
|
+
#
|
|
12
|
+
# @example Registrar un middleware desde una gema externa (auto-registro al hacer require)
|
|
13
|
+
# BugBunny.consumer_middlewares.use MyTracing::ConsumerMiddleware
|
|
14
|
+
#
|
|
15
|
+
# @example Implementar un middleware propio
|
|
16
|
+
# class MyMiddleware < BugBunny::ConsumerMiddleware::Base
|
|
17
|
+
# def call(delivery_info, properties, body)
|
|
18
|
+
# # lógica pre-procesamiento (ej: hidratar contexto de tracing)
|
|
19
|
+
# @app.call(delivery_info, properties, body)
|
|
20
|
+
# # lógica post-procesamiento
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
module ConsumerMiddleware
|
|
24
|
+
# Clase base para middlewares del Consumer.
|
|
25
|
+
#
|
|
26
|
+
# @abstract Subclasificá e implementá {#call}.
|
|
27
|
+
class Base
|
|
28
|
+
# @param app [#call] El siguiente eslabón en la cadena (otro middleware o el core).
|
|
29
|
+
def initialize(app)
|
|
30
|
+
@app = app
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Procesa el mensaje y delega al siguiente eslabón.
|
|
34
|
+
#
|
|
35
|
+
# @param delivery_info [Bunny::DeliveryInfo] Metadatos de entrega AMQP.
|
|
36
|
+
# @param properties [Bunny::MessageProperties] Headers y propiedades AMQP.
|
|
37
|
+
# @param body [String] Payload crudo del mensaje.
|
|
38
|
+
# @return [void]
|
|
39
|
+
def call(delivery_info, properties, body)
|
|
40
|
+
@app.call(delivery_info, properties, body)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Gestiona y ejecuta la cadena de middlewares del Consumer.
|
|
45
|
+
class Stack
|
|
46
|
+
def initialize
|
|
47
|
+
@middlewares = []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Registra un middleware en la cadena.
|
|
51
|
+
#
|
|
52
|
+
# @param middleware_class [Class] Clase que hereda de {Base}.
|
|
53
|
+
# @return [self]
|
|
54
|
+
def use(middleware_class)
|
|
55
|
+
@middlewares << middleware_class
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Boolean] `true` si no hay middlewares registrados.
|
|
60
|
+
def empty?
|
|
61
|
+
@middlewares.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Ejecuta la cadena de middlewares envolviendo el bloque core.
|
|
65
|
+
#
|
|
66
|
+
# @param delivery_info [Bunny::DeliveryInfo]
|
|
67
|
+
# @param properties [Bunny::MessageProperties]
|
|
68
|
+
# @param body [String]
|
|
69
|
+
# @yieldreturn [void] El bloque core a ejecutar al final de la cadena.
|
|
70
|
+
# @return [void]
|
|
71
|
+
def call(delivery_info, properties, body, &core)
|
|
72
|
+
terminal = ->(_di, _props, _body) { core.call }
|
|
73
|
+
|
|
74
|
+
chain = @middlewares.reverse.inject(terminal) do |next_step, middleware_class|
|
|
75
|
+
middleware_class.new(next_step)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
chain.call(delivery_info, properties, body)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -23,7 +23,9 @@ module BugBunny
|
|
|
23
23
|
|
|
24
24
|
formatted = case val
|
|
25
25
|
when Numeric then val
|
|
26
|
-
when Hash
|
|
26
|
+
when Hash
|
|
27
|
+
json = val.to_json
|
|
28
|
+
json.include?(" ") ? json.inspect : json
|
|
27
29
|
when String then val.include?(" ") ? val.inspect : val
|
|
28
30
|
else val.to_s.include?(" ") ? val.to_s.inspect : val
|
|
29
31
|
end
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -82,13 +82,17 @@ module BugBunny
|
|
|
82
82
|
safe_log(:debug, "producer.rpc_waiting", correlation_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)
|
|
86
86
|
|
|
87
|
-
if
|
|
87
|
+
if result.nil?
|
|
88
88
|
raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]"
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
BugBunny.configuration.on_rpc_reply&.call(result[:headers])
|
|
92
|
+
|
|
93
|
+
safe_log(:debug, "producer.rpc_response_received", correlation_id: cid)
|
|
94
|
+
|
|
95
|
+
parse_response(result[:body])
|
|
92
96
|
ensure
|
|
93
97
|
# Limpieza vital para evitar fugas de memoria en el mapa
|
|
94
98
|
@pending_requests.delete(cid)
|
|
@@ -153,9 +157,8 @@ module BugBunny
|
|
|
153
157
|
# Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
|
|
154
158
|
@session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
|
|
155
159
|
cid = props.correlation_id.to_s
|
|
156
|
-
safe_log(:debug, "producer.rpc_response_received", correlation_id: cid)
|
|
157
160
|
if (future = @pending_requests[cid])
|
|
158
|
-
future.set(body)
|
|
161
|
+
future.set({ body: body, headers: props.headers || {} })
|
|
159
162
|
else
|
|
160
163
|
safe_log(:warn, "producer.rpc_response_orphaned", correlation_id: cid)
|
|
161
164
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative 'bug_bunny/observability'
|
|
|
9
9
|
require_relative 'bug_bunny/routing/route_set'
|
|
10
10
|
require_relative 'bug_bunny/middleware/base'
|
|
11
11
|
require_relative 'bug_bunny/middleware/stack'
|
|
12
|
+
require_relative 'bug_bunny/consumer_middleware'
|
|
12
13
|
require_relative 'bug_bunny/middleware/raise_error'
|
|
13
14
|
require_relative 'bug_bunny/middleware/json_response'
|
|
14
15
|
require_relative 'bug_bunny/client'
|
|
@@ -41,6 +42,12 @@ module BugBunny
|
|
|
41
42
|
@routes ||= Routing::RouteSet.new
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
# @return [BugBunny::ConsumerMiddleware::Stack] Stack global de middlewares del Consumer.
|
|
46
|
+
# Atajo para `BugBunny.configuration.consumer_middlewares`.
|
|
47
|
+
def self.consumer_middlewares
|
|
48
|
+
configuration.consumer_middlewares
|
|
49
|
+
end
|
|
50
|
+
|
|
44
51
|
# Configura la librería BugBunny.
|
|
45
52
|
# Si no se ha configurado previamente, inicializa una nueva configuración por defecto.
|
|
46
53
|
#
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -237,6 +237,7 @@ files:
|
|
|
237
237
|
- lib/bug_bunny/client.rb
|
|
238
238
|
- lib/bug_bunny/configuration.rb
|
|
239
239
|
- lib/bug_bunny/consumer.rb
|
|
240
|
+
- lib/bug_bunny/consumer_middleware.rb
|
|
240
241
|
- lib/bug_bunny/controller.rb
|
|
241
242
|
- lib/bug_bunny/exception.rb
|
|
242
243
|
- lib/bug_bunny/middleware/base.rb
|