bug_bunny 3.0.6 → 3.1.1
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 +28 -0
- data/README.md +204 -148
- data/Rakefile +10 -6
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +40 -3
- data/lib/bug_bunny/consumer.rb +56 -29
- data/lib/bug_bunny/controller.rb +137 -93
- data/lib/bug_bunny/exception.rb +4 -0
- data/lib/bug_bunny/producer.rb +45 -29
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +176 -138
- data/lib/bug_bunny/session.rb +97 -47
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test/integration/infrastructure_test.rb +61 -0
- data/test/integration/manual_client_test.rb +203 -0
- data/test/test_helper.rb +109 -0
- metadata +47 -8
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
|
@@ -4,7 +4,14 @@ require 'logger'
|
|
|
4
4
|
|
|
5
5
|
module BugBunny
|
|
6
6
|
# Clase de configuración global para la gema BugBunny.
|
|
7
|
-
# Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ
|
|
7
|
+
# Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ,
|
|
8
|
+
# así como las opciones por defecto para la declaración de infraestructura AMQP.
|
|
9
|
+
#
|
|
10
|
+
# @example Configuración en un inicializador (e.g., config/initializers/bug_bunny.rb)
|
|
11
|
+
# BugBunny.configure do |config|
|
|
12
|
+
# config.host = '127.0.0.1'
|
|
13
|
+
# config.exchange_options = { durable: true, auto_delete: false }
|
|
14
|
+
# end
|
|
8
15
|
class Configuration
|
|
9
16
|
# @return [String] Host o IP del servidor RabbitMQ (ej: 'localhost').
|
|
10
17
|
attr_accessor :host
|
|
@@ -57,10 +64,30 @@ module BugBunny
|
|
|
57
64
|
# @return [Integer] Intervalo en segundos para verificar la salud de la cola.
|
|
58
65
|
attr_accessor :health_check_interval
|
|
59
66
|
|
|
67
|
+
# @return [String] Namespace base donde se buscarán los controladores (default: 'Rabbit::Controllers').
|
|
68
|
+
attr_accessor :controller_namespace
|
|
69
|
+
|
|
70
|
+
# @return [Array<Symbol, Proc, String>] Etiquetas para el log estructurado.
|
|
71
|
+
attr_accessor :log_tags
|
|
72
|
+
|
|
73
|
+
# @!group Configuración de Infraestructura Global
|
|
74
|
+
|
|
75
|
+
# @return [Hash] Opciones globales por defecto para la declaración de Exchanges.
|
|
76
|
+
# Estas opciones se fusionarán con los valores por defecto de la gema y las específicas del recurso.
|
|
77
|
+
# @example { durable: true, auto_delete: false }
|
|
78
|
+
attr_accessor :exchange_options
|
|
79
|
+
|
|
80
|
+
# @return [Hash] Opciones globales por defecto para la declaración de Colas.
|
|
81
|
+
# Estas opciones se fusionarán con los valores por defecto de la gema y las específicas del recurso.
|
|
82
|
+
# @example { durable: true, exclusive: false }
|
|
83
|
+
attr_accessor :queue_options
|
|
84
|
+
|
|
85
|
+
# @!endgroup
|
|
86
|
+
|
|
60
87
|
# Inicializa la configuración con valores por defecto seguros.
|
|
61
88
|
def initialize
|
|
62
|
-
@host = '127.0.0.1'
|
|
63
|
-
@port = 5672
|
|
89
|
+
@host = '127.0.0.1'
|
|
90
|
+
@port = 5672
|
|
64
91
|
@username = 'guest'
|
|
65
92
|
@password = 'guest'
|
|
66
93
|
@vhost = '/'
|
|
@@ -80,9 +107,19 @@ module BugBunny
|
|
|
80
107
|
@channel_prefetch = 1
|
|
81
108
|
@rpc_timeout = 10
|
|
82
109
|
@health_check_interval = 60
|
|
110
|
+
|
|
111
|
+
# Configuración por defecto para mantener compatibilidad
|
|
112
|
+
@controller_namespace = 'Rabbit::Controllers'
|
|
113
|
+
|
|
114
|
+
@log_tags = [:uuid]
|
|
115
|
+
|
|
116
|
+
# Inicialización de opciones de infraestructura como hashes vacíos para permitir fusiones posteriores.
|
|
117
|
+
@exchange_options = {}
|
|
118
|
+
@queue_options = {}
|
|
83
119
|
end
|
|
84
120
|
|
|
85
121
|
# Construye la URL de conexión AMQP basada en los atributos configurados.
|
|
122
|
+
# @return [String] URL formateada amqp://user:pass@host:port/vhost
|
|
86
123
|
def url
|
|
87
124
|
"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"
|
|
88
125
|
end
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -61,14 +61,24 @@ module BugBunny
|
|
|
61
61
|
q = session.queue(queue_name, queue_opts)
|
|
62
62
|
q.bind(x, routing_key: routing_key)
|
|
63
63
|
|
|
64
|
-
BugBunny.configuration.logger.info("[Consumer] Listening on #{queue_name}
|
|
64
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 🎧 Listening on '#{queue_name}' | Exchange: '#{exchange_name}' | Routing Key: '#{routing_key}'")
|
|
65
65
|
start_health_check(queue_name)
|
|
66
66
|
|
|
67
67
|
q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
|
|
68
|
-
|
|
68
|
+
trace_id = properties.correlation_id
|
|
69
|
+
|
|
70
|
+
logger = BugBunny.configuration.logger
|
|
71
|
+
|
|
72
|
+
if logger.respond_to?(:tagged)
|
|
73
|
+
logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
|
|
74
|
+
elsif defined?(Rails) && Rails.logger.respond_to?(:tagged)
|
|
75
|
+
Rails.logger.tagged(trace_id) { process_message(delivery_info, properties, body) }
|
|
76
|
+
else
|
|
77
|
+
process_message(delivery_info, properties, body)
|
|
78
|
+
end
|
|
69
79
|
end
|
|
70
80
|
rescue StandardError => e
|
|
71
|
-
BugBunny.configuration.logger.error("[Consumer] Connection Error: #{e.message}. Retrying...")
|
|
81
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Connection Error: #{e.message}. Retrying in #{BugBunny.configuration.network_recovery_interval}s...")
|
|
72
82
|
sleep BugBunny.configuration.network_recovery_interval
|
|
73
83
|
retry
|
|
74
84
|
end
|
|
@@ -84,20 +94,27 @@ module BugBunny
|
|
|
84
94
|
# @param body [String] El payload crudo del mensaje.
|
|
85
95
|
# @return [void]
|
|
86
96
|
def process_message(delivery_info, properties, body)
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
# 1. Validación de Headers
|
|
98
|
+
path = properties.type || (properties.headers && properties.headers['path'])
|
|
99
|
+
|
|
100
|
+
if path.nil? || path.empty?
|
|
101
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] ⛔ Rejected: Missing 'type' header.")
|
|
89
102
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
90
103
|
return
|
|
91
104
|
end
|
|
92
105
|
|
|
93
|
-
#
|
|
94
|
-
|
|
106
|
+
# 2. Recuperación Robusta del Verbo HTTP
|
|
107
|
+
headers_hash = properties.headers || {}
|
|
108
|
+
http_method = headers_hash['x-http-method'] || headers_hash['method'] || 'GET'
|
|
109
|
+
|
|
110
|
+
# 3. Router: Inferencia de Controlador y Acción
|
|
111
|
+
route_info = router_dispatch(http_method, path)
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 📥 Started #{http_method} \"/#{path}\" for Routing Key: #{delivery_info.routing_key}")
|
|
114
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
type:
|
|
116
|
+
request_metadata = {
|
|
117
|
+
type: path,
|
|
101
118
|
http_method: http_method,
|
|
102
119
|
controller: route_info[:controller],
|
|
103
120
|
action: route_info[:action],
|
|
@@ -106,33 +123,42 @@ module BugBunny
|
|
|
106
123
|
content_type: properties.content_type,
|
|
107
124
|
correlation_id: properties.correlation_id,
|
|
108
125
|
reply_to: properties.reply_to
|
|
109
|
-
}
|
|
126
|
+
}.merge(properties.headers)
|
|
127
|
+
|
|
128
|
+
# 4. Instanciación Dinámica del Controlador
|
|
129
|
+
# Utilizamos el namespace configurado en lugar de hardcodear "Rabbit::Controllers"
|
|
130
|
+
begin
|
|
131
|
+
namespace = BugBunny.configuration.controller_namespace
|
|
132
|
+
controller_name = route_info[:controller].camelize
|
|
110
133
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
controller_class_name = "rabbit/controllers/#{route_info[:controller]}".camelize
|
|
114
|
-
controller_class = controller_class_name.constantize
|
|
134
|
+
# Construcción: "Messaging::Handlers" + "::" + "Users"
|
|
135
|
+
controller_class_name = "#{namespace}::#{controller_name}Controller"
|
|
115
136
|
|
|
116
|
-
|
|
117
|
-
response_payload = controller_class.call(headers: headers, body: body)
|
|
137
|
+
controller_class = controller_class_name.constantize
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
unless controller_class < BugBunny::Controller
|
|
140
|
+
raise BugBunny::SecurityError, "Class #{controller_class} is not a valid BugBunny Controller"
|
|
141
|
+
end
|
|
142
|
+
rescue NameError => _e
|
|
143
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Controller not found: #{controller_class_name} (Path: #{path})")
|
|
144
|
+
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
145
|
+
session.channel.reject(delivery_info.delivery_tag, false)
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# 5. Ejecución del Pipeline (Middleware + Acción)
|
|
150
|
+
response_payload = controller_class.call(headers: request_metadata, body: body)
|
|
151
|
+
|
|
152
|
+
# 6. Respuesta RPC
|
|
120
153
|
if properties.reply_to
|
|
121
154
|
reply(response_payload, properties.reply_to, properties.correlation_id)
|
|
122
155
|
end
|
|
123
156
|
|
|
124
|
-
#
|
|
157
|
+
# 7. Acknowledge
|
|
125
158
|
session.channel.ack(delivery_info.delivery_tag)
|
|
126
159
|
|
|
127
|
-
rescue NameError => e
|
|
128
|
-
# Error 501/404: El controlador o la acción no existen.
|
|
129
|
-
BugBunny.configuration.logger.error("[Consumer] Routing Error: #{e.message}")
|
|
130
|
-
handle_fatal_error(properties, 501, "Routing Error", e.message)
|
|
131
|
-
session.channel.reject(delivery_info.delivery_tag, false)
|
|
132
|
-
|
|
133
160
|
rescue StandardError => e
|
|
134
|
-
|
|
135
|
-
BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
|
|
161
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
|
|
136
162
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
137
163
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
138
164
|
end
|
|
@@ -189,6 +215,7 @@ module BugBunny
|
|
|
189
215
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
190
216
|
# @return [void]
|
|
191
217
|
def reply(payload, reply_to, correlation_id)
|
|
218
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📤 Sending RPC Reply to #{reply_to} | ID: #{correlation_id}")
|
|
192
219
|
session.channel.default_exchange.publish(
|
|
193
220
|
payload.to_json,
|
|
194
221
|
routing_key: reply_to,
|
|
@@ -219,7 +246,7 @@ module BugBunny
|
|
|
219
246
|
Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
|
|
220
247
|
session.channel.queue_declare(q_name, passive: true)
|
|
221
248
|
rescue StandardError
|
|
222
|
-
BugBunny.configuration.logger.warn("[Consumer] Queue check failed. Reconnecting session...")
|
|
249
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed. Reconnecting session...")
|
|
223
250
|
session.close
|
|
224
251
|
end.execute
|
|
225
252
|
end
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -1,115 +1,133 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'active_model'
|
|
3
4
|
require 'rack'
|
|
5
|
+
require 'active_support/core_ext/class/attribute'
|
|
4
6
|
|
|
5
7
|
module BugBunny
|
|
6
|
-
# Clase base para Controladores de Mensajes.
|
|
8
|
+
# Clase base para todos los Controladores de Mensajes en BugBunny.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# y normalización de parámetros.
|
|
10
|
+
# @author Gabriel
|
|
11
|
+
# @since 3.0.6
|
|
11
12
|
class Controller
|
|
12
13
|
include ActiveModel::Model
|
|
13
14
|
include ActiveModel::Attributes
|
|
14
15
|
|
|
15
|
-
# @return [Hash] Metadatos del mensaje
|
|
16
|
+
# @return [Hash] Metadatos del mensaje entrante.
|
|
16
17
|
attribute :headers
|
|
17
18
|
|
|
18
|
-
# @return [ActiveSupport::HashWithIndifferentAccess] Parámetros unificados
|
|
19
|
+
# @return [ActiveSupport::HashWithIndifferentAccess] Parámetros unificados.
|
|
19
20
|
attribute :params
|
|
20
21
|
|
|
21
|
-
# @return [String] Cuerpo crudo
|
|
22
|
+
# @return [String] Cuerpo crudo.
|
|
22
23
|
attribute :raw_string
|
|
23
24
|
|
|
24
|
-
# @return [Hash
|
|
25
|
+
# @return [Hash] Headers de respuesta.
|
|
26
|
+
attr_reader :response_headers
|
|
27
|
+
|
|
28
|
+
# @return [Hash, nil] Respuesta renderizada.
|
|
25
29
|
attr_reader :rendered_response
|
|
26
30
|
|
|
27
|
-
# --- INFRAESTRUCTURA DE FILTROS (
|
|
31
|
+
# --- INFRAESTRUCTURA DE FILTROS (DEFINICIÓN) ---
|
|
32
|
+
# Deben definirse ANTES de ser usados por la configuración de logs.
|
|
28
33
|
|
|
29
34
|
# @api private
|
|
30
35
|
def self.before_actions
|
|
31
36
|
@before_actions ||= Hash.new { |h, k| h[k] = [] }
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
39
|
+
# @api private
|
|
40
|
+
def self.around_actions
|
|
41
|
+
@around_actions ||= Hash.new { |h, k| h[k] = [] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Registra un filtro que se ejecutará **antes** de la acción.
|
|
40
45
|
def self.before_action(method_name, **options)
|
|
46
|
+
register_callback(before_actions, method_name, options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Registra un filtro que **envuelve** la ejecución de la acción.
|
|
50
|
+
def self.around_action(method_name, **options)
|
|
51
|
+
register_callback(around_actions, method_name, options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Helper interno para registrar callbacks.
|
|
55
|
+
def self.register_callback(collection, method_name, options)
|
|
41
56
|
only = Array(options[:only]).map(&:to_sym)
|
|
42
57
|
target_actions = only.empty? ? [:_all_actions] : only
|
|
58
|
+
target_actions.each { |action| collection[action] << method_name }
|
|
59
|
+
end
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
# --- CONFIGURACIÓN DE LOGGING ---
|
|
62
|
+
|
|
63
|
+
# Define los tags que se antepondrán a cada línea de log.
|
|
64
|
+
class_attribute :log_tags
|
|
65
|
+
self.log_tags = []
|
|
66
|
+
|
|
67
|
+
# AHORA SÍ: Podemos llamar a around_action porque ya fue definido arriba.
|
|
68
|
+
around_action :apply_log_tags
|
|
69
|
+
|
|
70
|
+
# --- INICIALIZACIÓN ---
|
|
71
|
+
|
|
72
|
+
def initialize(attributes = {})
|
|
73
|
+
super
|
|
74
|
+
@response_headers = {}
|
|
47
75
|
end
|
|
48
76
|
|
|
49
|
-
# ---
|
|
77
|
+
# --- MANEJO DE ERRORES ---
|
|
50
78
|
|
|
51
79
|
# @api private
|
|
52
80
|
def self.rescue_handlers
|
|
53
81
|
@rescue_handlers ||= []
|
|
54
82
|
end
|
|
55
83
|
|
|
56
|
-
# Registra un manejador para una o más excepciones.
|
|
57
|
-
# Los manejadores se evalúan en orden inverso (el último registrado tiene prioridad).
|
|
58
|
-
#
|
|
59
|
-
# @param klasses [Class] Clases de excepción a capturar.
|
|
60
|
-
# @param with [Symbol] Nombre del método manejador.
|
|
61
|
-
# @param block [Proc] Bloque manejador.
|
|
62
|
-
#
|
|
63
|
-
# @example Con método
|
|
64
|
-
# rescue_from User::NotAuthorized, with: :deny_access
|
|
65
|
-
#
|
|
66
|
-
# @example Con bloque
|
|
67
|
-
# rescue_from ActiveRecord::RecordNotFound do |e|
|
|
68
|
-
# render status: :not_found, json: { error: e.message }
|
|
69
|
-
# end
|
|
70
84
|
def self.rescue_from(*klasses, with: nil, &block)
|
|
71
85
|
handler = with || block
|
|
72
86
|
raise ArgumentError, "Need a handler. Supply 'with: :method' or a block." unless handler
|
|
73
87
|
|
|
74
88
|
klasses.each do |klass|
|
|
75
|
-
# Insertamos al principio para que las últimas definiciones tengan prioridad (LIFO)
|
|
76
89
|
rescue_handlers.unshift([klass, handler])
|
|
77
90
|
end
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
# --- PIPELINE DE EJECUCIÓN ---
|
|
81
94
|
|
|
82
|
-
# Punto de entrada principal llamado por el Consumer.
|
|
83
|
-
# Instancia el controlador y procesa el mensaje.
|
|
84
|
-
#
|
|
85
|
-
# @param headers [Hash] Metadatos del mensaje.
|
|
86
|
-
# @param body [Hash, String] Payload deserializado.
|
|
87
|
-
# @return [Hash] La respuesta final { status, body }.
|
|
88
95
|
def self.call(headers:, body: {})
|
|
89
96
|
new(headers: headers).process(body)
|
|
90
97
|
end
|
|
91
98
|
|
|
92
|
-
# Ejecuta el ciclo de vida de la petición: Params -> Filtros -> Acción.
|
|
93
|
-
# Captura cualquier error y delega al sistema `rescue_from`.
|
|
94
|
-
#
|
|
95
|
-
# @param body [Hash, String] Payload.
|
|
96
|
-
# @return [Hash] Respuesta RPC.
|
|
97
99
|
def process(body)
|
|
98
100
|
prepare_params(body)
|
|
101
|
+
|
|
102
|
+
# Inyección de configuración global de logs si no hay específica
|
|
103
|
+
if self.class.log_tags.empty? && BugBunny.configuration.log_tags.any?
|
|
104
|
+
self.class.log_tags = BugBunny.configuration.log_tags
|
|
105
|
+
end
|
|
106
|
+
|
|
99
107
|
action_name = headers[:action].to_sym
|
|
108
|
+
current_arounds = resolve_callbacks(self.class.around_actions, action_name)
|
|
100
109
|
|
|
101
|
-
#
|
|
102
|
-
|
|
110
|
+
# Definir el núcleo de ejecución
|
|
111
|
+
core_execution = lambda do
|
|
112
|
+
return unless run_before_actions(action_name)
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
if respond_to?(action_name)
|
|
115
|
+
public_send(action_name)
|
|
116
|
+
else
|
|
117
|
+
raise NameError, "Action '#{action_name}' not found in #{self.class.name}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Construir la cadena de responsabilidad
|
|
122
|
+
execution_chain = current_arounds.reverse.inject(core_execution) do |next_step, method_name|
|
|
123
|
+
lambda { send(method_name, &next_step) }
|
|
109
124
|
end
|
|
110
125
|
|
|
111
|
-
#
|
|
112
|
-
|
|
126
|
+
# Ejecutar la cadena
|
|
127
|
+
execution_chain.call
|
|
128
|
+
|
|
129
|
+
# Respuesta final
|
|
130
|
+
rendered_response || { status: 204, headers: response_headers, body: nil }
|
|
113
131
|
|
|
114
132
|
rescue StandardError => e
|
|
115
133
|
handle_exception(e)
|
|
@@ -117,61 +135,59 @@ module BugBunny
|
|
|
117
135
|
|
|
118
136
|
private
|
|
119
137
|
|
|
120
|
-
#
|
|
121
|
-
|
|
138
|
+
# --- HELPERS INTERNOS ---
|
|
139
|
+
|
|
140
|
+
def resolve_callbacks(collection, action_name)
|
|
141
|
+
(collection[:_all_actions] || []) + (collection[action_name] || [])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def run_before_actions(action_name)
|
|
145
|
+
current_befores = resolve_callbacks(self.class.before_actions, action_name)
|
|
146
|
+
current_befores.uniq.each do |method_name|
|
|
147
|
+
send(method_name)
|
|
148
|
+
return false if rendered_response
|
|
149
|
+
end
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
|
|
122
153
|
def handle_exception(exception)
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
handler_entry = self.class.rescue_handlers.find do |klass, _|
|
|
155
|
+
if klass.is_a?(String)
|
|
156
|
+
exception.class.name == klass
|
|
157
|
+
else
|
|
158
|
+
exception.is_a?(klass)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
125
161
|
|
|
126
162
|
if handler_entry
|
|
127
163
|
_, handler = handler_entry
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if handler.is_a?(Symbol)
|
|
131
|
-
send(handler, exception)
|
|
132
|
-
elsif handler.respond_to?(:call)
|
|
133
|
-
instance_exec(exception, &handler)
|
|
164
|
+
if handler.is_a?(Symbol); send(handler, exception)
|
|
165
|
+
elsif handler.respond_to?(:call); instance_exec(exception, &handler)
|
|
134
166
|
end
|
|
135
|
-
|
|
136
|
-
# Si el handler hizo render, retornamos esa respuesta
|
|
137
167
|
return rendered_response if rendered_response
|
|
138
168
|
end
|
|
139
169
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
BugBunny.configuration.logger.error("Controller Error (#{exception.class}): #{exception.message}")
|
|
143
|
-
BugBunny.configuration.logger.error(exception.backtrace.join("\n"))
|
|
144
|
-
|
|
145
|
-
{ status: 500, body: { error: exception.message, type: exception.class.name } }
|
|
146
|
-
end
|
|
170
|
+
BugBunny.configuration.logger.error("[BugBunny::Controller] 💥 Unhandled Exception (#{exception.class}): #{exception.message}")
|
|
171
|
+
BugBunny.configuration.logger.error(exception.backtrace.first(5).join("\n")) # Limitamos a 5 líneas para no ensuciar
|
|
147
172
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
chain.uniq.each do |method_name|
|
|
154
|
-
send(method_name)
|
|
155
|
-
# Si un filtro llamó a 'render', detenemos la cadena (halt)
|
|
156
|
-
return false if rendered_response
|
|
157
|
-
end
|
|
158
|
-
true
|
|
173
|
+
{
|
|
174
|
+
status: 500,
|
|
175
|
+
headers: response_headers,
|
|
176
|
+
body: { error: exception.message, type: exception.class.name }
|
|
177
|
+
}
|
|
159
178
|
end
|
|
160
179
|
|
|
161
|
-
# Construye la respuesta RPC normalizada.
|
|
162
|
-
#
|
|
163
|
-
# @param status [Symbol, Integer] Código HTTP (ej: :ok, 200, :not_found).
|
|
164
|
-
# @param json [Object] Objeto a serializar en el body.
|
|
165
180
|
def render(status:, json: nil)
|
|
166
181
|
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status] || 200
|
|
167
|
-
@rendered_response = {
|
|
182
|
+
@rendered_response = {
|
|
183
|
+
status: code,
|
|
184
|
+
headers: response_headers,
|
|
185
|
+
body: json
|
|
186
|
+
}
|
|
168
187
|
end
|
|
169
188
|
|
|
170
|
-
# Normaliza y fusiona parámetros de múltiples fuentes.
|
|
171
|
-
# Prioridad: Body > ID Ruta > Query Params.
|
|
172
189
|
def prepare_params(body)
|
|
173
190
|
self.params = {}.with_indifferent_access
|
|
174
|
-
|
|
175
191
|
params.merge!(headers[:query_params]) if headers[:query_params].present?
|
|
176
192
|
params[:id] = headers[:id] if headers[:id].present?
|
|
177
193
|
|
|
@@ -184,5 +200,33 @@ module BugBunny
|
|
|
184
200
|
self.raw_string = body
|
|
185
201
|
end
|
|
186
202
|
end
|
|
203
|
+
|
|
204
|
+
# --- LÓGICA DE LOGGING ENCAPSULADA ---
|
|
205
|
+
|
|
206
|
+
def apply_log_tags
|
|
207
|
+
tags = compute_tags
|
|
208
|
+
if defined?(Rails) && Rails.logger.respond_to?(:tagged) && tags.any?
|
|
209
|
+
Rails.logger.tagged(*tags) { yield }
|
|
210
|
+
else
|
|
211
|
+
yield
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def compute_tags
|
|
216
|
+
self.class.log_tags.map do |tag|
|
|
217
|
+
case tag
|
|
218
|
+
when Proc
|
|
219
|
+
tag.call(self)
|
|
220
|
+
when Symbol
|
|
221
|
+
respond_to?(tag, true) ? send(tag) : tag
|
|
222
|
+
else
|
|
223
|
+
tag
|
|
224
|
+
end
|
|
225
|
+
end.compact
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def uuid
|
|
229
|
+
headers[:correlation_id] || headers['X-Request-Id']
|
|
230
|
+
end
|
|
187
231
|
end
|
|
188
232
|
end
|
data/lib/bug_bunny/exception.rb
CHANGED
|
@@ -10,6 +10,10 @@ module BugBunny
|
|
|
10
10
|
# Suele envolver excepciones nativas de la gema `bunny` (ej: TCP connection failure).
|
|
11
11
|
class CommunicationError < Error; end
|
|
12
12
|
|
|
13
|
+
# Error lanzado cuando ocurren un acceso no permitido a controladores.
|
|
14
|
+
# Suele envolver excepciones nativas de la gema `bunny` (ej: TCP connection failure).
|
|
15
|
+
class SecurityError < Error; end
|
|
16
|
+
|
|
13
17
|
# === Categoría: Errores del Cliente (4xx) ===
|
|
14
18
|
|
|
15
19
|
# Clase base para errores causados por una petición incorrecta del cliente.
|