bug_bunny 4.1.2 → 4.3.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 +23 -0
- data/lib/bug_bunny/consumer.rb +22 -16
- data/lib/bug_bunny/controller.rb +2 -2
- data/lib/bug_bunny/producer.rb +7 -10
- data/lib/bug_bunny/session.rb +2 -2
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67cd206fe5bbd998fd71e143d5b22972d841439115209a007223b776630ebf46
|
|
4
|
+
data.tar.gz: 6647fc999934cc49b636f32e50f8cdad8c21effd4005a8ff403b233498ee75af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1cc705a67bef35d982a2c2ed1117c70fc7dc14f79a6a4b9b70e8e7549f0d6db860070e0c6b85e859ef7239f7a322988d8d5054a0ef88e6a363faf911a87dd9d7
|
|
7
|
+
data.tar.gz: 7ba4187d186d391fb9e8b42da9d1e7c0a1656f059421a6249c8c3c2c45f1fcdeddf5070a0845245d0371e17250df0a83f674cf77eb188794e95d95e79103a9e3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.3.0] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### 📈 Observability Alignment (ExisRay Standards)
|
|
6
|
+
* **Monotonic Clock Durations:** Implementación de `Process.clock_gettime(Process::CLOCK_MONOTONIC)` para calcular todas las duraciones técnicas y de negocio (`duration_s`), garantizando precisión en entornos Cloud.
|
|
7
|
+
* **Unit-Suffix Keys (Data First):** Se renombraron las llaves de logs para incluir explícitamente su unidad:
|
|
8
|
+
* `timeout` -> `timeout_s`
|
|
9
|
+
* `retry_in` -> `retry_in_s`
|
|
10
|
+
* `attempt` -> `attempt_count`
|
|
11
|
+
* `max_attempts` -> `max_attempts_count`
|
|
12
|
+
* **Error Field Standardization:** Se renombraron todos los campos `error` a `error_message` para ser consistentes con los eventos de falla de `exis_ray`.
|
|
13
|
+
* **Automatic Field Removal:** Se eliminó la inyección manual de `source` delegando la responsabilidad a la gema `exis_ray`.
|
|
14
|
+
|
|
15
|
+
## [4.2.0] - 2026-03-22
|
|
16
|
+
|
|
17
|
+
### 🔠Observability & Structured Logging
|
|
18
|
+
* **Structured Logs (Key-Value):** Se migraron todos los logs del framework a un formato \`key=value\` estructurado, ideal para herramientas de monitoreo como Datadog o CloudWatch. Se eliminaron emojis y texto libre para mejorar el parseo automático.
|
|
19
|
+
* **Lazy Evaluation (Debug Blocks):** Las llamadas a \`logger.debug\` ahora utilizan bloques para evitar la interpolación de strings innecesaria en producción, optimizando el uso de CPU y memoria.
|
|
20
|
+
|
|
21
|
+
### ðŸ›¡ï¸ Resilience & Connectivity
|
|
22
|
+
* **Exponential Backoff:** El \`Consumer\` ahora implementa un algoritmo de reintento exponencial para reconectarse a RabbitMQ, evitando picos de carga durante caÃdas del broker.
|
|
23
|
+
* **Max Reconnect Attempts:** Nueva configuración \`max_reconnect_attempts\` que permite que el worker falle definitivamente tras N intentos, facilitando el reinicio del Pod por parte de orquestadores como Kubernetes.
|
|
24
|
+
* **Performance Tuning:** Se desactivaron los \`publisher_confirms\` en el canal del \`Consumer\` al responder RPCs para reducir la latencia de respuesta (round-trips innecesarios).
|
|
25
|
+
|
|
3
26
|
## [4.1.2] - 2026-03-22
|
|
4
27
|
|
|
5
28
|
### ✨ Improvements
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -77,8 +77,8 @@ module BugBunny
|
|
|
77
77
|
.merge(BugBunny.configuration.queue_options || {})
|
|
78
78
|
.merge(queue_opts || {})
|
|
79
79
|
|
|
80
|
-
BugBunny.configuration.logger.info("
|
|
81
|
-
BugBunny.configuration.logger.info("
|
|
80
|
+
BugBunny.configuration.logger.info("component=bug_bunny event=consumer_start queue=#{queue_name} queue_opts=#{final_q_opts}")
|
|
81
|
+
BugBunny.configuration.logger.info("component=bug_bunny event=consumer_bound exchange=#{exchange_name} exchange_type=#{exchange_type} routing_key=#{routing_key} exchange_opts=#{final_x_opts}")
|
|
82
82
|
|
|
83
83
|
start_health_check(queue_name)
|
|
84
84
|
|
|
@@ -100,7 +100,7 @@ module BugBunny
|
|
|
100
100
|
max_attempts = BugBunny.configuration.max_reconnect_attempts
|
|
101
101
|
|
|
102
102
|
if max_attempts && attempt >= max_attempts
|
|
103
|
-
BugBunny.configuration.logger.error
|
|
103
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=reconnect_exhausted max_attempts_count=#{max_attempts} error_message=#{e.message.inspect}" }
|
|
104
104
|
raise
|
|
105
105
|
end
|
|
106
106
|
|
|
@@ -109,7 +109,7 @@ module BugBunny
|
|
|
109
109
|
BugBunny.configuration.max_reconnect_interval
|
|
110
110
|
].min
|
|
111
111
|
|
|
112
|
-
BugBunny.configuration.logger.error
|
|
112
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=connection_error error_message=#{e.message.inspect} attempt_count=#{attempt} max_attempts_count=#{max_attempts || 'infinity'} retry_in_s=#{wait}" }
|
|
113
113
|
sleep wait
|
|
114
114
|
retry
|
|
115
115
|
end
|
|
@@ -126,11 +126,13 @@ module BugBunny
|
|
|
126
126
|
# @param body [String] El payload crudo del mensaje.
|
|
127
127
|
# @return [void]
|
|
128
128
|
def process_message(delivery_info, properties, body)
|
|
129
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
130
|
+
|
|
129
131
|
# 1. Validación de Headers (URL path)
|
|
130
132
|
path = properties.type || (properties.headers && properties.headers['path'])
|
|
131
133
|
|
|
132
134
|
if path.nil? || path.empty?
|
|
133
|
-
BugBunny.configuration.logger.error(
|
|
135
|
+
BugBunny.configuration.logger.error('component=bug_bunny event=message_rejected reason=missing_type_header')
|
|
134
136
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
135
137
|
return
|
|
136
138
|
end
|
|
@@ -139,8 +141,8 @@ module BugBunny
|
|
|
139
141
|
headers_hash = properties.headers || {}
|
|
140
142
|
http_method = (headers_hash['x-http-method'] || headers_hash['method'] || 'GET').to_s.upcase
|
|
141
143
|
|
|
142
|
-
BugBunny.configuration.logger.info("
|
|
143
|
-
BugBunny.configuration.logger.debug
|
|
144
|
+
BugBunny.configuration.logger.info("component=bug_bunny event=message_received method=#{http_method} path=#{path} routing_key=#{delivery_info.routing_key}")
|
|
145
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=message_received_body body=#{body.truncate(200).inspect}" }
|
|
144
146
|
|
|
145
147
|
# ===================================================================
|
|
146
148
|
# 3. Ruteo Declarativo
|
|
@@ -157,7 +159,7 @@ module BugBunny
|
|
|
157
159
|
route_info = BugBunny.routes.recognize(http_method, uri.path)
|
|
158
160
|
|
|
159
161
|
if route_info.nil?
|
|
160
|
-
BugBunny.configuration.logger.warn("
|
|
162
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=route_not_found method=#{http_method} path=#{uri.path}")
|
|
161
163
|
handle_fatal_error(properties, 404, "Not Found", "No route matches [#{http_method}] \"/#{uri.path}\"")
|
|
162
164
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
163
165
|
return
|
|
@@ -176,7 +178,7 @@ module BugBunny
|
|
|
176
178
|
begin
|
|
177
179
|
controller_class = controller_class_name.constantize
|
|
178
180
|
rescue NameError
|
|
179
|
-
BugBunny.configuration.logger.warn("
|
|
181
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=controller_not_found controller=#{controller_class_name}")
|
|
180
182
|
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
181
183
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
182
184
|
return
|
|
@@ -184,13 +186,13 @@ module BugBunny
|
|
|
184
186
|
|
|
185
187
|
# Verificación estricta de Seguridad (RCE Prevention)
|
|
186
188
|
unless controller_class < BugBunny::Controller
|
|
187
|
-
BugBunny.configuration.logger.error("
|
|
189
|
+
BugBunny.configuration.logger.error("component=bug_bunny event=security_violation reason=invalid_controller controller=#{controller_class}")
|
|
188
190
|
handle_fatal_error(properties, 403, "Forbidden", "Invalid Controller Class")
|
|
189
191
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
190
192
|
return
|
|
191
193
|
end
|
|
192
194
|
|
|
193
|
-
BugBunny.configuration.logger.debug
|
|
195
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=route_matched controller=#{controller_class_name} action=#{route_info[:action]}" }
|
|
194
196
|
|
|
195
197
|
request_metadata = {
|
|
196
198
|
type: path,
|
|
@@ -215,9 +217,13 @@ module BugBunny
|
|
|
215
217
|
|
|
216
218
|
session.channel.ack(delivery_info.delivery_tag)
|
|
217
219
|
|
|
220
|
+
duration_s = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(6)
|
|
221
|
+
BugBunny.configuration.logger.info("component=bug_bunny event=message_processed status=#{response_payload[:status]} duration_s=#{duration_s} controller=#{controller_class_name} action=#{route_info[:action]}")
|
|
222
|
+
|
|
218
223
|
rescue StandardError => e
|
|
219
|
-
|
|
220
|
-
BugBunny.configuration.logger.
|
|
224
|
+
duration_s = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).round(6)
|
|
225
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=execution_error error_class=#{e.class} error_message=#{e.message.inspect} duration_s=#{duration_s}" }
|
|
226
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=execution_error backtrace=#{e.backtrace.first(5).join(' | ').inspect}" }
|
|
221
227
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
222
228
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
223
229
|
end
|
|
@@ -229,7 +235,7 @@ module BugBunny
|
|
|
229
235
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
230
236
|
# @return [void]
|
|
231
237
|
def reply(payload, reply_to, correlation_id)
|
|
232
|
-
BugBunny.configuration.logger.debug
|
|
238
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_reply reply_to=#{reply_to} correlation_id=#{correlation_id}" }
|
|
233
239
|
session.channel.default_exchange.publish(
|
|
234
240
|
payload.to_json,
|
|
235
241
|
routing_key: reply_to,
|
|
@@ -278,7 +284,7 @@ module BugBunny
|
|
|
278
284
|
# 2. Si llegamos aquí, RabbitMQ y la cola están vivos. Avisamos al orquestador actualizando el archivo.
|
|
279
285
|
touch_health_file(file_path) if file_path
|
|
280
286
|
rescue StandardError => e
|
|
281
|
-
BugBunny.configuration.logger.warn("
|
|
287
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=health_check_failed queue=#{q_name} error_message=#{e.message.inspect}")
|
|
282
288
|
session.close
|
|
283
289
|
end
|
|
284
290
|
@health_timer.execute
|
|
@@ -293,7 +299,7 @@ module BugBunny
|
|
|
293
299
|
def touch_health_file(file_path)
|
|
294
300
|
FileUtils.touch(file_path)
|
|
295
301
|
rescue StandardError => e
|
|
296
|
-
BugBunny.configuration.logger.error("
|
|
302
|
+
BugBunny.configuration.logger.error("component=bug_bunny event=health_check_file_error path=#{file_path} error_message=#{e.message.inspect}")
|
|
297
303
|
end
|
|
298
304
|
end
|
|
299
305
|
end
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -204,8 +204,8 @@ module BugBunny
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
# Fallback genérico si la excepción no fue mapeada
|
|
207
|
-
BugBunny.configuration.logger.error
|
|
208
|
-
BugBunny.configuration.logger.error
|
|
207
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=unhandled_exception error_class=#{exception.class} error_message=#{exception.message.inspect}" }
|
|
208
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=unhandled_exception backtrace=#{exception.backtrace.first(5).join(' | ').inspect}" }
|
|
209
209
|
|
|
210
210
|
{
|
|
211
211
|
status: 500,
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -73,7 +73,7 @@ module BugBunny
|
|
|
73
73
|
begin
|
|
74
74
|
fire(request)
|
|
75
75
|
|
|
76
|
-
BugBunny.configuration.logger.debug
|
|
76
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_waiting correlation_id=#{cid} timeout_s=#{wait_timeout}" }
|
|
77
77
|
|
|
78
78
|
# Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
|
|
79
79
|
response_payload = future.value(wait_timeout)
|
|
@@ -106,12 +106,9 @@ module BugBunny
|
|
|
106
106
|
.merge(BugBunny.configuration.exchange_options || {})
|
|
107
107
|
.merge(request.exchange_options || {})
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
BugBunny.configuration.logger.
|
|
111
|
-
|
|
112
|
-
# DEBUG: Detalle completo de Infraestructura y Payload
|
|
113
|
-
BugBunny.configuration.logger.debug("[BugBunny::Producer] ⚙️ Exchange #{request.exchange} | Opts: #{final_x_opts}")
|
|
114
|
-
BugBunny.configuration.logger.debug("[BugBunny::Producer] 📦 Payload: #{payload.truncate(300)}") if payload.is_a?(String)
|
|
109
|
+
BugBunny.configuration.logger.info("component=bug_bunny event=publish method=#{verb} path=#{target} routing_key=#{rk} correlation_id=#{id}")
|
|
110
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=publish_detail exchange=#{request.exchange} exchange_opts=#{final_x_opts}" }
|
|
111
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=publish_payload payload=#{payload.truncate(300).inspect}" } if payload.is_a?(String)
|
|
115
112
|
end
|
|
116
113
|
|
|
117
114
|
# Serializa el mensaje para su transporte.
|
|
@@ -145,16 +142,16 @@ module BugBunny
|
|
|
145
142
|
@reply_listener_mutex.synchronize do
|
|
146
143
|
return if @reply_listener_started
|
|
147
144
|
|
|
148
|
-
BugBunny.configuration.logger.debug
|
|
145
|
+
BugBunny.configuration.logger.debug { 'component=bug_bunny event=reply_listener_start queue=amq.rabbitmq.reply-to' }
|
|
149
146
|
|
|
150
147
|
# Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
|
|
151
148
|
@session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
|
|
152
149
|
cid = props.correlation_id.to_s
|
|
153
|
-
BugBunny.configuration.logger.debug
|
|
150
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_response_received correlation_id=#{cid}" }
|
|
154
151
|
if (future = @pending_requests[cid])
|
|
155
152
|
future.set(body)
|
|
156
153
|
else
|
|
157
|
-
BugBunny.configuration.logger.warn("
|
|
154
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=rpc_response_orphaned correlation_id=#{cid}")
|
|
158
155
|
end
|
|
159
156
|
end
|
|
160
157
|
@reply_listener_started = true
|
data/lib/bug_bunny/session.rb
CHANGED
|
@@ -125,10 +125,10 @@ module BugBunny
|
|
|
125
125
|
def ensure_connection!
|
|
126
126
|
return if @connection.open?
|
|
127
127
|
|
|
128
|
-
BugBunny.configuration.logger.warn(
|
|
128
|
+
BugBunny.configuration.logger.warn('component=bug_bunny event=reconnect_attempt')
|
|
129
129
|
@connection.start
|
|
130
130
|
rescue StandardError => e
|
|
131
|
-
BugBunny.configuration.logger.error
|
|
131
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=reconnect_failed error_message=#{e.message.inspect}" }
|
|
132
132
|
raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
|
|
133
133
|
end
|
|
134
134
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -83,7 +83,7 @@ module BugBunny
|
|
|
83
83
|
|
|
84
84
|
@global_connection.close if @global_connection.open?
|
|
85
85
|
@global_connection = nil
|
|
86
|
-
configuration.logger.info('
|
|
86
|
+
configuration.logger.info('component=bug_bunny event=disconnect')
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
# @api private
|
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.3.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-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|