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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 979c38a4f25c9359afd10b0f3c728b794fdf4dee795adfaea7fd090e832134bc
4
- data.tar.gz: 658a0088c4e8cedde115074b81a9247e6cf1c15d226e880c444bd94ed8168cc5
3
+ metadata.gz: 26437c12bdcd679124739cffc4d1c9769587bc6ffd241d6a03a75880c3bd910d
4
+ data.tar.gz: cc185721833cbf505d1fe44ac1dac7ac496fda1677b54898ce950a1d1badff68
5
5
  SHA512:
6
- metadata.gz: 0d80a2015e3f26626e6f0261597a261129a4c55f87ed5556f3bab7523327a747e1238241e5c49769f22dd83d231e151913a786b34119eabe43ec858f456c60d1
7
- data.tar.gz: 94635e6463d95eac5ffd65e7fe5ff3eaf3c946825052477a31b9ddde0da763b87b5e2f27e877b8c72c5a3bcb13920e28269b12f5c123567bd75ff0ed00d049ae
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
@@ -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("[BugBunny::Consumer] 🎧 Listening on '#{queue_name}' (Opts: #{final_q_opts})")
81
- BugBunny.configuration.logger.info("[BugBunny::Consumer] 🔀 Bounded to Exchange '#{exchange_name}' (#{exchange_type}) | Opts: #{final_x_opts} | RK: '#{routing_key}'")
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("[BugBunny::Consumer] 💥 Max reconnect attempts (#{max_attempts}) reached. Giving up: #{e.message}")
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("[BugBunny::Consumer] 💥 Connection Error: #{e.message}. Retrying in #{wait}s (attempt #{attempt}/#{max_attempts || ''})...")
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("[BugBunny::Consumer] Rejected: Missing 'type' header.")
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("[BugBunny::Consumer] 📥 Received #{http_method} \"/#{path}\" | RK: '#{delivery_info.routing_key}'")
143
- BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
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("[BugBunny::Consumer] ⚠️ No route matches [#{http_method}] \"/#{uri.path}\"")
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("[BugBunny::Consumer] ⚠️ Controller class not found: #{controller_class_name}")
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("[BugBunny::Consumer] Security Alert: #{controller_class} is not a valid BugBunny Controller")
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("[BugBunny::Consumer] 🎯 Routed to #{controller_class_name}##{route_info[:action]}")
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("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
220
- BugBunny.configuration.logger.debug(e.backtrace.join("\n"))
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("[BugBunny::Consumer] 📤 Sending RPC Reply to #{reply_to} | ID: #{correlation_id}")
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("[BugBunny::Consumer] ⚠️ Queue check failed: #{e.message}. Reconnecting session...")
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("[BugBunny::Consumer] ⚠️ Cannot touch health check file '#{file_path}': #{e.message}")
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
@@ -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("[BugBunny::Controller] 💥 Unhandled Exception (#{exception.class}): #{exception.message}")
208
- BugBunny.configuration.logger.error(exception.backtrace.first(5).join("\n"))
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,
@@ -73,7 +73,7 @@ module BugBunny
73
73
  begin
74
74
  fire(request)
75
75
 
76
- BugBunny.configuration.logger.debug("[BugBunny::Producer] Waiting for RPC response | ID: #{cid} | Timeout: #{wait_timeout}s")
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
- # INFO: Resumen de una línea (Traffic)
110
- BugBunny.configuration.logger.info("[BugBunny::Producer] 📤 #{verb} /#{target} | RK: '#{rk}' | ID: #{id}")
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("[BugBunny::Producer] 👂 Starting Reply Listener on 'amq.rabbitmq.reply-to'")
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("[BugBunny::Producer] 📥 RPC Response matched | ID: #{cid}")
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("[BugBunny::Producer] ⚠️ Orphaned RPC Response received | ID: #{cid}")
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
@@ -125,10 +125,10 @@ module BugBunny
125
125
  def ensure_connection!
126
126
  return if @connection.open?
127
127
 
128
- BugBunny.configuration.logger.warn("[BugBunny::Session] ⚠️ Connection lost. Attempting to reconnect...")
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("[BugBunny::Session] Critical connection failure: #{e.message}")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "4.1.2"
4
+ VERSION = "4.2.0"
5
5
  end
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('[BugBunny] 🔌 Global connection closed.')
86
+ configuration.logger.info('component=bug_bunny event=disconnect')
87
87
  end
88
88
 
89
89
  # @api private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.2
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix