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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 979c38a4f25c9359afd10b0f3c728b794fdf4dee795adfaea7fd090e832134bc
4
- data.tar.gz: 658a0088c4e8cedde115074b81a9247e6cf1c15d226e880c444bd94ed8168cc5
3
+ metadata.gz: 67cd206fe5bbd998fd71e143d5b22972d841439115209a007223b776630ebf46
4
+ data.tar.gz: 6647fc999934cc49b636f32e50f8cdad8c21effd4005a8ff403b233498ee75af
5
5
  SHA512:
6
- metadata.gz: 0d80a2015e3f26626e6f0261597a261129a4c55f87ed5556f3bab7523327a747e1238241e5c49769f22dd83d231e151913a786b34119eabe43ec858f456c60d1
7
- data.tar.gz: 94635e6463d95eac5ffd65e7fe5ff3eaf3c946825052477a31b9ddde0da763b87b5e2f27e877b8c72c5a3bcb13920e28269b12f5c123567bd75ff0ed00d049ae
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
@@ -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 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("[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_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("[BugBunny::Consumer] Rejected: Missing 'type' header.")
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("[BugBunny::Consumer] 📥 Received #{http_method} \"/#{path}\" | RK: '#{delivery_info.routing_key}'")
143
- BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
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("[BugBunny::Consumer] ⚠️ No route matches [#{http_method}] \"/#{uri.path}\"")
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("[BugBunny::Consumer] ⚠️ Controller class not found: #{controller_class_name}")
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("[BugBunny::Consumer] Security Alert: #{controller_class} is not a valid BugBunny Controller")
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("[BugBunny::Consumer] 🎯 Routed to #{controller_class_name}##{route_info[:action]}")
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
- BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
220
- BugBunny.configuration.logger.debug(e.backtrace.join("\n"))
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("[BugBunny::Consumer] 📤 Sending RPC Reply to #{reply_to} | ID: #{correlation_id}")
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("[BugBunny::Consumer] ⚠️ Queue check failed: #{e.message}. Reconnecting session...")
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("[BugBunny::Consumer] ⚠️ Cannot touch health check file '#{file_path}': #{e.message}")
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
@@ -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_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,
@@ -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_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
- # 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_message=#{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.3.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,14 +1,14 @@
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.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-22 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny