bug_bunny 4.1.2 → 4.2.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 +11 -0
- data/lib/bug_bunny/consumer.rb +16 -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 +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26437c12bdcd679124739cffc4d1c9769587bc6ffd241d6a03a75880c3bd910d
|
|
4
|
+
data.tar.gz: cc185721833cbf505d1fe44ac1dac7ac496fda1677b54898ce950a1d1badff68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71da0a97f33b38263957a878350e8a2daa935a0f211e0fa59af589b507a7d11635e84ac6170ee2a4fafb16b0c4f8ddeec61b388f38d7f9856d171e3059b004f3
|
|
7
|
+
data.tar.gz: 962de8a788d263c04cf0d9fa5da2e6c3a5ec3a2d6156b80ff542b8d2fd333c4d9d0915f94775f8fb9c8101d507691cf757701904fe1d0477d5dabfcd006efa7e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.2.0] - 2026-03-22
|
|
4
|
+
|
|
5
|
+
### 🔠Observability & Structured Logging
|
|
6
|
+
* **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.
|
|
7
|
+
* **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.
|
|
8
|
+
|
|
9
|
+
### ðŸ›¡ï¸ Resilience & Connectivity
|
|
10
|
+
* **Exponential Backoff:** El \`Consumer\` ahora implementa un algoritmo de reintento exponencial para reconectarse a RabbitMQ, evitando picos de carga durante caÃdas del broker.
|
|
11
|
+
* **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.
|
|
12
|
+
* **Performance Tuning:** Se desactivaron los \`publisher_confirms\` en el canal del \`Consumer\` al responder RPCs para reducir la latencia de respuesta (round-trips innecesarios).
|
|
13
|
+
|
|
3
14
|
## [4.1.2] - 2026-03-22
|
|
4
15
|
|
|
5
16
|
### ✨ 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 attempts=#{max_attempts} error=#{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=#{e.message.inspect} attempt=#{attempt} max_attempts=#{max_attempts || 'infinity'} retry_in=#{wait}" }
|
|
113
113
|
sleep wait
|
|
114
114
|
retry
|
|
115
115
|
end
|
|
@@ -130,7 +130,7 @@ module BugBunny
|
|
|
130
130
|
path = properties.type || (properties.headers && properties.headers['path'])
|
|
131
131
|
|
|
132
132
|
if path.nil? || path.empty?
|
|
133
|
-
BugBunny.configuration.logger.error(
|
|
133
|
+
BugBunny.configuration.logger.error('component=bug_bunny event=message_rejected reason=missing_type_header')
|
|
134
134
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
135
135
|
return
|
|
136
136
|
end
|
|
@@ -139,8 +139,8 @@ module BugBunny
|
|
|
139
139
|
headers_hash = properties.headers || {}
|
|
140
140
|
http_method = (headers_hash['x-http-method'] || headers_hash['method'] || 'GET').to_s.upcase
|
|
141
141
|
|
|
142
|
-
BugBunny.configuration.logger.info("
|
|
143
|
-
BugBunny.configuration.logger.debug
|
|
142
|
+
BugBunny.configuration.logger.info("component=bug_bunny event=message_received method=#{http_method} path=#{path} routing_key=#{delivery_info.routing_key}")
|
|
143
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=message_received_body body=#{body.truncate(200).inspect}" }
|
|
144
144
|
|
|
145
145
|
# ===================================================================
|
|
146
146
|
# 3. Ruteo Declarativo
|
|
@@ -157,7 +157,7 @@ module BugBunny
|
|
|
157
157
|
route_info = BugBunny.routes.recognize(http_method, uri.path)
|
|
158
158
|
|
|
159
159
|
if route_info.nil?
|
|
160
|
-
BugBunny.configuration.logger.warn("
|
|
160
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=route_not_found method=#{http_method} path=#{uri.path}")
|
|
161
161
|
handle_fatal_error(properties, 404, "Not Found", "No route matches [#{http_method}] \"/#{uri.path}\"")
|
|
162
162
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
163
163
|
return
|
|
@@ -176,7 +176,7 @@ module BugBunny
|
|
|
176
176
|
begin
|
|
177
177
|
controller_class = controller_class_name.constantize
|
|
178
178
|
rescue NameError
|
|
179
|
-
BugBunny.configuration.logger.warn("
|
|
179
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=controller_not_found controller=#{controller_class_name}")
|
|
180
180
|
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
181
181
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
182
182
|
return
|
|
@@ -184,13 +184,13 @@ module BugBunny
|
|
|
184
184
|
|
|
185
185
|
# Verificación estricta de Seguridad (RCE Prevention)
|
|
186
186
|
unless controller_class < BugBunny::Controller
|
|
187
|
-
BugBunny.configuration.logger.error("
|
|
187
|
+
BugBunny.configuration.logger.error("component=bug_bunny event=security_violation reason=invalid_controller controller=#{controller_class}")
|
|
188
188
|
handle_fatal_error(properties, 403, "Forbidden", "Invalid Controller Class")
|
|
189
189
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
190
190
|
return
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
-
BugBunny.configuration.logger.debug
|
|
193
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=route_matched controller=#{controller_class_name} action=#{route_info[:action]}" }
|
|
194
194
|
|
|
195
195
|
request_metadata = {
|
|
196
196
|
type: path,
|
|
@@ -216,8 +216,8 @@ module BugBunny
|
|
|
216
216
|
session.channel.ack(delivery_info.delivery_tag)
|
|
217
217
|
|
|
218
218
|
rescue StandardError => e
|
|
219
|
-
BugBunny.configuration.logger.error
|
|
220
|
-
BugBunny.configuration.logger.debug
|
|
219
|
+
BugBunny.configuration.logger.error { "component=bug_bunny event=execution_error error_class=#{e.class} error=#{e.message.inspect}" }
|
|
220
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=execution_error backtrace=#{e.backtrace.first(5).join(' | ').inspect}" }
|
|
221
221
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
222
222
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
223
223
|
end
|
|
@@ -229,7 +229,7 @@ module BugBunny
|
|
|
229
229
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
230
230
|
# @return [void]
|
|
231
231
|
def reply(payload, reply_to, correlation_id)
|
|
232
|
-
BugBunny.configuration.logger.debug
|
|
232
|
+
BugBunny.configuration.logger.debug { "component=bug_bunny event=rpc_reply reply_to=#{reply_to} correlation_id=#{correlation_id}" }
|
|
233
233
|
session.channel.default_exchange.publish(
|
|
234
234
|
payload.to_json,
|
|
235
235
|
routing_key: reply_to,
|
|
@@ -278,7 +278,7 @@ module BugBunny
|
|
|
278
278
|
# 2. Si llegamos aquí, RabbitMQ y la cola están vivos. Avisamos al orquestador actualizando el archivo.
|
|
279
279
|
touch_health_file(file_path) if file_path
|
|
280
280
|
rescue StandardError => e
|
|
281
|
-
BugBunny.configuration.logger.warn("
|
|
281
|
+
BugBunny.configuration.logger.warn("component=bug_bunny event=health_check_failed queue=#{q_name} error=#{e.message.inspect}")
|
|
282
282
|
session.close
|
|
283
283
|
end
|
|
284
284
|
@health_timer.execute
|
|
@@ -293,7 +293,7 @@ module BugBunny
|
|
|
293
293
|
def touch_health_file(file_path)
|
|
294
294
|
FileUtils.touch(file_path)
|
|
295
295
|
rescue StandardError => e
|
|
296
|
-
BugBunny.configuration.logger.error("
|
|
296
|
+
BugBunny.configuration.logger.error("component=bug_bunny event=health_check_file_error path=#{file_path} error=#{e.message.inspect}")
|
|
297
297
|
end
|
|
298
298
|
end
|
|
299
299
|
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=#{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=#{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=#{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
|