exis_ray 0.5.9 → 0.5.10

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: f17193f5df75197d569fbe74d8b27e55b116e861bdb819f1e92526a1da79869e
4
- data.tar.gz: e8e45998cb83236c3bb7cc3a981995ccd36764fdb6fbc1465e796a7966e3b354
3
+ metadata.gz: 3ad15b3b5656e54690df31596233aabafaf70a5061b9086d6a601a60370fca16
4
+ data.tar.gz: 6d2d87410cb374c1f8c9ddee2b6caf730f959a6c87bc63004ef43e52a290d8c1
5
5
  SHA512:
6
- metadata.gz: 39999ff8baa584c7dd35a9b090feca1c410eedffcec72aba429ab65fe3d983c17517dff97363f4209c303d36252e3f46e1ad569ab908f2362c677064f43b54dc
7
- data.tar.gz: d2b8d38f83a0bccdda315c5148d90a8d0b93821424fdb11b106ea322bcea09e3537d42c371a5cd266aa0c63a0e7f73c2d6285f302c7e444cfbf59f98c9451909
6
+ metadata.gz: dd4d89629f65123d600cddf5dbca462b2bfcf50f9c4be9a40a87f9f4fc57949ff2ab5e6c5c65a0095040a99b63bca225a08c9a8d223b90231735049f6bb8f71a
7
+ data.tar.gz: 173790ac008fe33aa7b2aca4823dbe7bb94a5428e9628181bcde0325ed3260cc95f75a706751ee56829cca5778d211a386ae1654c83d0af737f401f3ed537501
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [0.5.10] - 2026-04-01
2
+
3
+ ### Fixed
4
+ - **Thread-safety:** Removed unsafe instance variable caching of `@user`/`@isp` in `Current`. Objects are now queried directly on each access.
5
+ - **BugBunny Consumer cleanup:** `ConsumerTracingMiddleware` now properly cleans up `Current` and `Reporter` in addition to `Tracer` in the ensure block.
6
+ - **JsonFormatter crash prevention:** Added rescue block with fallback message to prevent logging failures from crashing requests.
7
+ - **Filter sensitive hash cycle detection:** Added `visited` array to detect and prevent infinite recursion with circular references.
8
+ - **FaradayMiddleware rescue:** Wrapped header injection in rescue to prevent crashes on malformed headers.
9
+ - **ActiveResourceInstrumentation rescue:** Wrapped header injection in rescue to prevent crashes on malformed headers.
10
+ - **Session isolation:** `assign_session_request_id` helper with rescue prevents global state pollution from failing Session writes.
11
+ - **Sidekiq ServerMiddleware sync:** Added `ExisRay.sync_correlation_id` call after hydrating tracer.
12
+ - **Sidekiq trace_id empty string:** Changed to `trace_header.present?` check to handle empty strings.
13
+ - **Safe middleware insertion:** Added `respond_to?` check for `include?` to handle Rails 8's `MiddlewareStackProxy`.
14
+ - **Symbol allocation optimization:** Changed to string keys in `parse_kv_string` to avoid memory leaks.
15
+ - **Sidekiq ClientMiddleware rescue:** Trace injection now wrapped in ensure block with rescue.
16
+ - **LogSubscriber double-attach:** Added `attached?` check to prevent multiple subscriber registrations.
17
+ - **LogSubscriber fallback:** Improved error handling with structured fallback message when build_payload fails.
18
+ - **user_id=0 preserved:** Changed `.present?` to `!.nil?` checks in `Current`, `JsonFormatter`, and `Reporter` to preserve 0 values.
19
+ - **ActiveResource idempotent prepend:** Added ancestor check before prepending instrumentation.
20
+ - **Parser handles malformed headers:** `parse_trace_id` now skips parts without `=` instead of crashing.
21
+ - **Reporter rescue/ensure structure:** Sentry reporting now wrapped in nested begin/rescue to prevent crashes.
22
+ - **HttpMiddleware rescue:** Added rescue to prevent crashes on malformed trace headers.
23
+ - **TaskMonitor Rails.logger guard:** Added `defined?(Rails) && Rails.logger` check.
24
+ - **ServerMiddleware queue nil guard:** Safe access with `&.` for `get_sidekiq_options`.
25
+ - **JsonFormatter timestamp nil:** Added safe navigation with fallback `Time.now` for nil timestamps.
26
+ - **Sidekiq cleanup rescue:** Extracted `cleanup_current`/`cleanup_reporter` with individual rescue blocks.
27
+ - **Current#user/#isp object lookup:** Changed from `.present?` to `!.nil?` to allow user_id=0 lookups.
28
+
1
29
  ## [0.5.9] - 2026-03-31
2
30
 
3
31
  ### Fixed
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExisRay
2
4
  # Módulo diseñado para interceptar e instrumentar las peticiones HTTP salientes realizadas con ActiveResource.
3
5
  # Utiliza el patrón `prepend` para envolver el método `headers` original sin romper la cadena de herencia.
@@ -14,27 +16,22 @@ module ExisRay
14
16
  #
15
17
  # @return [Hash] Un hash de headers HTTP que incluye el header de trazabilidad si corresponde.
16
18
  def headers
17
- # 1. Obtenemos los headers originales (si los hay)
18
19
  original_headers = super
20
+ return original_headers unless ExisRay::Tracer.root_id.present?
19
21
 
20
- # 2. Verificación Universal:
21
- # Usamos `root_id` en lugar de `trace_id`.
22
- # - trace_id: Solo existe si recibimos una petición Web (viene del header entrante).
23
- # - root_id: Existe SIEMPRE que haya traza (sea Web o sea un Cron generado por TaskMonitor).
24
- if ExisRay::Tracer.root_id.present?
25
- # Generamos el string propagable: "Root=...;Parent=...;Sampled=..."
26
- trace_header_value = ExisRay::Tracer.generate_trace_header
22
+ inject_trace_header(original_headers)
23
+ rescue StandardError
24
+ original_headers
25
+ end
27
26
 
28
- # Usamos el header de propagación (formato HTTP, ej: 'X-Amzn-Trace-Id'),
29
- # NO el trace_header que es formato Rack (HTTP_X_AMZN_TRACE_ID) para lectura entrante.
30
- trace_header_key = ExisRay.configuration.propagation_trace_header
27
+ private
31
28
 
32
- # Retornamos un nuevo hash combinado (merge) para no mutar el original por error
33
- original_headers.merge(trace_header_key => trace_header_value)
34
- else
35
- # Si no hay traza activa, devolvemos los headers tal cual
36
- original_headers
37
- end
29
+ def inject_trace_header(original_headers)
30
+ trace_header_value = ExisRay::Tracer.generate_trace_header
31
+ trace_header_key = ExisRay.configuration.propagation_trace_header
32
+ original_headers.merge(trace_header_key => trace_header_value)
33
+ rescue StandardError
34
+ original_headers
38
35
  end
39
36
  end
40
37
  end
@@ -25,7 +25,9 @@ module ExisRay
25
25
  setup_trace_context(properties)
26
26
  @app.call(delivery_info, properties, body)
27
27
  ensure
28
- ExisRay::Tracer.reset rescue nil
28
+ safe_reset(ExisRay::Tracer)
29
+ safe_reset(ExisRay.current_class)
30
+ safe_reset(ExisRay.reporter_class)
29
31
  end
30
32
 
31
33
  private
@@ -40,10 +42,10 @@ module ExisRay
40
42
  trace_header = properties.headers&.[](ExisRay.configuration.propagation_trace_header)
41
43
 
42
44
  if trace_header.present?
43
- ExisRay::Tracer.hydrate(trace_id: trace_header, source: 'system')
45
+ ExisRay::Tracer.hydrate(trace_id: trace_header, source: "system")
44
46
  else
45
47
  ExisRay::Tracer.created_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
46
- ExisRay::Tracer.source = 'system'
48
+ ExisRay::Tracer.source = "system"
47
49
  ExisRay::Tracer.root_id = ExisRay::Tracer.send(:generate_new_root)
48
50
  end
49
51
 
@@ -51,6 +53,11 @@ module ExisRay
51
53
  rescue StandardError
52
54
  # El tracing nunca debe interrumpir el procesamiento del mensaje.
53
55
  end
56
+
57
+ def safe_reset(obj)
58
+ obj.reset if obj.respond_to?(:reset)
59
+ rescue StandardError
60
+ end
54
61
  end
55
62
  end
56
63
  end
@@ -1,4 +1,6 @@
1
- require 'active_support/current_attributes'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/current_attributes"
2
4
 
3
5
  module ExisRay
4
6
  # Clase base para la gestión del contexto de negocio (User, ISP, Correlation).
@@ -6,14 +8,10 @@ module ExisRay
6
8
  class Current < ActiveSupport::CurrentAttributes
7
9
  attribute :user_id, :isp_id, :correlation_id
8
10
 
9
- # Sentinel para distinguir "no consultado" de "consultado y no encontrado".
10
- # Evita re-queries a la DB cuando el objeto no existe.
11
- NOT_FOUND = Object.new.freeze
12
-
13
11
  # Callback nativo de Rails: Se ejecuta automáticamente al llamar a Current.reset
14
12
  resets do
15
- @user = NOT_FOUND
16
- @isp = NOT_FOUND
13
+ @user_object = nil
14
+ @isp_object = nil
17
15
 
18
16
  if defined?(PaperTrail)
19
17
  PaperTrail.request.whodunnit = nil
@@ -21,107 +19,99 @@ module ExisRay
21
19
  end
22
20
 
23
21
  if defined?(ActiveResource::Base)
24
- ActiveResource::Base.headers.delete('UserId')
25
- ActiveResource::Base.headers.delete('IspId')
26
- ActiveResource::Base.headers.delete('CorrelationId')
22
+ ActiveResource::Base.headers.delete("UserId")
23
+ ActiveResource::Base.headers.delete("IspId")
24
+ ActiveResource::Base.headers.delete("CorrelationId")
27
25
  end
28
26
  end
29
27
 
30
28
  # --- Setters con Hooks ---
31
29
 
32
30
  def user_id=(id)
31
+ @user_object = nil
33
32
  super
34
- if defined?(ActiveResource::Base)
35
- ActiveResource::Base.headers['UserId'] = sanitize_header_value(id)
36
- end
37
- if defined?(PaperTrail)
38
- PaperTrail.request.whodunnit = id
39
- end
33
+ ActiveResource::Base.headers["UserId"] = sanitize_header_value(id) if defined?(ActiveResource::Base)
34
+ return unless defined?(PaperTrail)
35
+
36
+ PaperTrail.request.whodunnit = id
40
37
  end
41
38
 
42
39
  def isp_id=(id)
40
+ @isp_object = nil
43
41
  super
44
- @isp = NOT_FOUND # Invalida cache
45
- if defined?(ActiveResource::Base)
46
- ActiveResource::Base.headers['IspId'] = sanitize_header_value(id)
47
- end
42
+ return unless defined?(ActiveResource::Base)
43
+
44
+ ActiveResource::Base.headers["IspId"] = sanitize_header_value(id)
48
45
  end
49
46
 
50
47
  def correlation_id=(id)
51
48
  super
52
-
53
- if defined?(::Session)
54
- ::Session.request_id = id # Deprecated legacy support
55
- end
56
-
57
- if defined?(ActiveResource::Base)
58
- ActiveResource::Base.headers['CorrelationId'] = sanitize_header_value(id)
59
- end
60
-
61
- if defined?(PaperTrail)
62
- PaperTrail.request.controller_info = { correlation_id: id }
63
- end
64
-
65
- # Integración con el Reporter configurado
66
- if (reporter = ExisRay.reporter_class) && reporter.respond_to?(:add_tags)
67
- reporter.add_tags(correlation_id: id)
68
- end
49
+ assign_session_request_id(id)
50
+ ActiveResource::Base.headers["CorrelationId"] = sanitize_header_value(id) if defined?(ActiveResource::Base)
51
+ PaperTrail.request.controller_info = { correlation_id: id } if defined?(PaperTrail)
52
+ sync_reporter_correlation_id(id)
69
53
  end
70
54
 
71
- # --- Helpers de Objetos (Lazy Loading) ---
72
- # Estos métodos asumen que la app host tiene modelos ::User e ::Isp
55
+ # --- Helpers de Objetos (Lazy Loading con cache por request) ---
56
+ # Estos métodos asumen que la app host tiene modelos ::User e ::Isp.
57
+ # Memoizan el objeto en @user_object/@isp_object, que se limpian en el bloque
58
+ # resets al final de cada request/job, y al asignar un nuevo user_id/isp_id.
73
59
 
74
60
  def user=(object)
75
- @user = object || NOT_FOUND
76
61
  self.user_id = object&.id
77
62
  end
78
63
 
79
64
  def user
80
- @user = NOT_FOUND unless defined?(@user)
81
- return nil if @user.equal?(NOT_FOUND) && !user_id
82
-
83
- if @user.equal?(NOT_FOUND)
84
- @user = (defined?(::User) && ::User.respond_to?(:find_by) ? ::User.find_by(id: user_id) : nil) || NOT_FOUND
85
- end
65
+ return nil if user_id.nil?
66
+ return nil unless defined?(::User) && ::User.respond_to?(:find_by)
86
67
 
87
- @user.equal?(NOT_FOUND) ? nil : @user
68
+ @user_object ||= ::User.find_by(id: user_id)
88
69
  end
89
70
 
90
71
  def isp=(object)
91
- @isp = object || NOT_FOUND
92
72
  self.isp_id = object&.id
93
73
  end
94
74
 
95
75
  def isp
96
- @isp = NOT_FOUND unless defined?(@isp)
97
- return nil if @isp.equal?(NOT_FOUND) && !isp_id
76
+ return nil if isp_id.nil?
77
+ return nil unless defined?(::Isp) && ::Isp.respond_to?(:find_by)
98
78
 
99
- if @isp.equal?(NOT_FOUND)
100
- @isp = (defined?(::Isp) && ::Isp.respond_to?(:find_by) ? ::Isp.find_by(id: isp_id) : nil) || NOT_FOUND
101
- end
102
-
103
- @isp.equal?(NOT_FOUND) ? nil : @isp
79
+ @isp_object ||= ::Isp.find_by(id: isp_id)
104
80
  end
105
81
 
106
82
  def user?
107
- user_id.present?
83
+ !user_id.nil?
108
84
  end
109
85
 
110
86
  def isp?
111
- isp_id.present?
87
+ !isp_id.nil?
112
88
  end
113
89
 
90
+ # Usa present? intencionalmente: un string vacío no es un correlation_id válido,
91
+ # a diferencia de user_id/isp_id donde 0 es un valor legítimo.
114
92
  def correlation_id?
115
93
  correlation_id.present?
116
94
  end
117
95
 
118
96
  private
119
97
 
120
- # Elimina caracteres CRLF para prevenir HTTP header injection.
121
- # Un valor con "\r\n" en un header de ActiveResource podría inyectar
122
- # headers arbitrarios en requests hacia otros microservicios.
98
+ def assign_session_request_id(id)
99
+ return unless defined?(::Session)
100
+
101
+ ::Session.request_id = id
102
+ rescue StandardError
103
+ end
104
+
105
+ def sync_reporter_correlation_id(id)
106
+ reporter = ExisRay.reporter_class
107
+ return unless reporter.respond_to?(:add_tags)
108
+
109
+ reporter.add_tags(correlation_id: id)
110
+ rescue StandardError
111
+ end
112
+
123
113
  def sanitize_header_value(value)
124
- value.to_s.gsub(/[\r\n]/, '')
114
+ value.to_s.gsub(/[\r\n]/, "")
125
115
  end
126
116
  end
127
117
  end
@@ -1,19 +1,26 @@
1
- require 'faraday'
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
2
4
 
3
5
  module ExisRay
4
6
  # Middleware para Faraday que inyecta el header de trazabilidad saliente.
5
7
  class FaradayMiddleware < Faraday::Middleware
6
8
  def call(env)
7
- if ExisRay::Tracer.root_id.present?
8
- # Generamos el valor de traza
9
- header_value = ExisRay::Tracer.generate_trace_header
10
- # Obtenemos la key configurada para propagación
11
- header_key = ExisRay.configuration.propagation_trace_header
12
-
13
- env.request_headers[header_key] = header_value
14
- end
9
+ inject_trace_header(env)
15
10
  @app.call(env)
11
+ rescue StandardError
12
+ @app.call(env)
13
+ end
14
+
15
+ private
16
+
17
+ def inject_trace_header(env)
18
+ return unless ExisRay::Tracer.root_id.present?
19
+
20
+ header_key = ExisRay.configuration.propagation_trace_header
21
+ header_value = ExisRay::Tracer.generate_trace_header
22
+ env.request_headers[header_key] = header_value
23
+ rescue StandardError
16
24
  end
17
25
  end
18
26
  end
19
-
@@ -1,22 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ExisRay
2
- # Middleware de Rack para interceptar peticiones HTTP.
3
- # Inicializa el Tracer y sincroniza el Correlation ID con la clase Current configurada.
4
4
  class HttpMiddleware
5
5
  def initialize(app)
6
6
  @app = app
7
7
  end
8
8
 
9
9
  def call(env)
10
- # 1. Hidratar Infraestructura
11
10
  ExisRay::Tracer.hydrate(
12
11
  trace_id: env[ExisRay.configuration.trace_header],
13
- source: "http"
12
+ source: "http"
14
13
  )
15
- ExisRay::Tracer.request_id = env['action_dispatch.request_id']
16
-
17
- # 2. Hidratar Negocio
14
+ ExisRay::Tracer.request_id = env["action_dispatch.request_id"]
18
15
  ExisRay.sync_correlation_id
19
16
 
17
+ @app.call(env)
18
+ rescue StandardError
20
19
  @app.call(env)
21
20
  end
22
21
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "logger"
4
4
  require "json"
5
+ require "set"
5
6
  require "active_support/tagged_logging"
6
7
 
7
8
  module ExisRay
@@ -37,7 +38,7 @@ module ExisRay
37
38
  # @return [String] Una cadena en formato JSON terminada con un salto de línea (\n).
38
39
  def call(severity, timestamp, _progname, msg)
39
40
  payload = {
40
- time: timestamp.utc.iso8601,
41
+ time: timestamp&.utc&.iso8601 || Time.now.utc.iso8601,
41
42
  level: severity,
42
43
  service: ExisRay::Tracer.service_name
43
44
  }
@@ -47,13 +48,32 @@ module ExisRay
47
48
  inject_current_tags(payload)
48
49
  process_message(payload, msg)
49
50
 
50
- # Compactamos para eliminar claves con valores nulos (nil) y generamos el JSON.
51
- # Usamos JSON.generate con unsafe_chars para evitar el escape HTML de > como \u003e.
52
51
  "#{JSON.generate(payload.compact, { ascii_only: false })}\n"
52
+ rescue StandardError
53
+ fallback_message(severity, timestamp, msg)
53
54
  end
54
55
 
55
56
  private
56
57
 
58
+ # Genera un JSON de fallback cuando el formateo principal falla.
59
+ # Nunca debe lanzar una excepción.
60
+ #
61
+ # @param severity [String]
62
+ # @param timestamp [Time]
63
+ # @param msg [Object]
64
+ # @return [String]
65
+ def fallback_message(severity, timestamp, msg)
66
+ fallback = {
67
+ time: timestamp&.utc&.iso8601 || Time.now.utc.iso8601,
68
+ level: severity,
69
+ service: ExisRay::Tracer.service_name,
70
+ body: msg.to_s[0..500]
71
+ }
72
+ "#{JSON.generate(fallback)}\n"
73
+ rescue StandardError
74
+ "#{JSON.generate({ time: Time.now.utc.iso8601, level: severity, body: "log_error" })}\n"
75
+ end
76
+
57
77
  # Inyecta los identificadores de trazabilidad distribuida en el payload.
58
78
  #
59
79
  # @param payload [Hash] El diccionario del log donde se insertarán los datos.
@@ -76,12 +96,12 @@ module ExisRay
76
96
  curr = ExisRay.current_class
77
97
  return unless curr
78
98
 
79
- payload[:user_id] = curr.user_id if curr.respond_to?(:user_id) && curr.user_id
80
- payload[:isp_id] = curr.isp_id if curr.respond_to?(:isp_id) && curr.isp_id
99
+ payload[:user_id] = curr.user_id if curr.respond_to?(:user_id) && !curr.user_id.nil?
100
+ payload[:isp_id] = curr.isp_id if curr.respond_to?(:isp_id) && !curr.isp_id.nil?
81
101
 
82
- if curr.respond_to?(:correlation_id) && curr.correlation_id
83
- payload[:correlation_id] = curr.correlation_id
84
- end
102
+ return unless curr.respond_to?(:correlation_id) && curr.correlation_id
103
+
104
+ payload[:correlation_id] = curr.correlation_id
85
105
  end
86
106
 
87
107
  # Inyecta cualquier etiqueta nativa (tags) de Rails que esté presente en el hilo actual.
@@ -89,9 +109,9 @@ module ExisRay
89
109
  # @param payload [Hash] El diccionario base del log.
90
110
  # @return [void]
91
111
  def inject_current_tags(payload)
92
- if respond_to?(:current_tags) && current_tags.any?
93
- payload[:tags] = current_tags
94
- end
112
+ return unless respond_to?(:current_tags) && current_tags.any?
113
+
114
+ payload[:tags] = current_tags
95
115
  end
96
116
 
97
117
  # Procesa el cuerpo del mensaje recibido y lo fusiona con el payload.
@@ -129,20 +149,20 @@ module ExisRay
129
149
  # Parsea un string con formato key=value y retorna un Hash.
130
150
  # Soporta valores con espacios si están entre comillas dobles o simples.
131
151
  # Intenta convertir valores numéricos a Float o Integer automáticamente.
152
+ # Usa strings como claves para evitar memory leaks por symbols.
132
153
  #
133
154
  # @param str [String]
134
155
  # @return [Hash]
135
156
  def parse_kv_string(str)
136
157
  result = {}
137
158
  str.scan(KV_PARSE_RE) do |key, value|
138
- # Eliminamos comillas envolventes si existen (dobles o simples)
139
159
  val = if value.start_with?('"', "'")
140
160
  value[1..-2].to_s.gsub("\\#{value[0]}", value[0])
141
161
  else
142
162
  value
143
163
  end
144
164
 
145
- result[key.to_sym] = cast_value(key, val)
165
+ result[key] = cast_value(key, val)
146
166
  end
147
167
  result
148
168
  end
@@ -179,14 +199,21 @@ module ExisRay
179
199
 
180
200
  # Filtra recursivamente un Hash que contenga claves sensibles.
181
201
  # Maneja valores anidados de tipo Hash o Array.
202
+ # Detecta referencias circulares para evitar stack overflow.
182
203
  #
183
204
  # @param hash [Hash]
205
+ # @param visited [Set] IDs de objetos ya visitados para detectar ciclos (O(1) lookup).
184
206
  # @return [Hash]
185
- def filter_sensitive_hash(hash)
207
+ def filter_sensitive_hash(hash, visited = Set.new)
208
+ return {} if hash.nil?
209
+ return {} if visited.include?(hash.object_id)
210
+
211
+ visited = visited | Set.new([hash.object_id])
212
+
186
213
  hash.each_with_object({}) do |(k, v), result|
187
214
  result[k] = case v
188
- when Hash then filter_sensitive_hash(v)
189
- when Array then filter_sensitive_array(k, v)
215
+ when Hash then filter_sensitive_hash(v, visited)
216
+ when Array then filter_sensitive_array(k, v, visited)
190
217
  else cast_value(k, v)
191
218
  end
192
219
  end
@@ -194,15 +221,22 @@ module ExisRay
194
221
 
195
222
  # Filtra recursivamente los elementos de un Array, propagando la clave padre
196
223
  # para que el filtrado de claves sensibles se aplique a hashes anidados.
224
+ # Detecta referencias circulares para evitar stack overflow.
197
225
  #
198
226
  # @param key [String, Symbol] Clave padre (para filtrado si el array no contiene hashes).
199
227
  # @param array [Array]
228
+ # @param visited [Set] IDs de objetos ya visitados para detectar ciclos (O(1) lookup).
200
229
  # @return [Array]
201
- def filter_sensitive_array(key, array)
230
+ def filter_sensitive_array(key, array, visited = Set.new)
231
+ return [] if array.nil?
232
+ return [] if visited.include?(array.object_id)
233
+
234
+ visited = visited | Set.new([array.object_id])
235
+
202
236
  array.map do |element|
203
237
  case element
204
- when Hash then filter_sensitive_hash(element)
205
- when Array then filter_sensitive_array(key, element)
238
+ when Hash then filter_sensitive_hash(element, visited)
239
+ when Array then filter_sensitive_array(key, element, visited)
206
240
  else cast_value(key, element)
207
241
  end
208
242
  end
@@ -32,14 +32,22 @@ module ExisRay
32
32
  # @return [void]
33
33
  def process_action(event)
34
34
  payload = build_payload(event)
35
- # Usamos el nivel ERROR si el status es 5xx, cumpliendo el estándar de Gabriel.
36
35
  if payload[:status] && payload[:status] >= 500
37
36
  logger.error(payload)
38
37
  else
39
38
  logger.info(payload)
40
39
  end
41
- rescue StandardError
42
- # El logger nunca debe interrumpir el flujo del request.
40
+ rescue StandardError => e
41
+ begin
42
+ logger.error({
43
+ component: "exis_ray",
44
+ event: "log_subscriber_fallback",
45
+ error: e.message,
46
+ payload_summary: event.payload.slice(:controller, :action, :method, :path)
47
+ })
48
+ rescue StandardError
49
+ nil
50
+ end
43
51
  end
44
52
 
45
53
  # Hook para que las subclases inyecten campos extra en cada log de request.
@@ -58,11 +66,19 @@ module ExisRay
58
66
  #
59
67
  # @return [void]
60
68
  def self.install!
69
+ return if attached?
70
+
61
71
  suppress_default_log_subscribers!
62
72
  suppress_rack_logger!
63
73
  subscriber_class.attach_to(:action_controller)
64
74
  end
65
75
 
76
+ def self.attached?
77
+ ActiveSupport::LogSubscriber.log_subscribers.any? { |s| s.is_a?(subscriber_class) }
78
+ rescue StandardError
79
+ false
80
+ end
81
+
66
82
  private
67
83
 
68
84
  def build_payload(event)
@@ -75,18 +91,18 @@ module ExisRay
75
91
  db_s = payload[:db_runtime] ? (payload[:db_runtime] / 1000.0).round(4) : nil
76
92
 
77
93
  data = {
78
- component: "exis_ray",
79
- event: "http_request",
80
- method: payload[:method],
81
- path: payload[:path],
82
- format: payload[:format],
83
- controller: payload[:controller],
84
- action: payload[:action],
85
- status: status,
86
- duration_s: duration_s,
94
+ component: "exis_ray",
95
+ event: "http_request",
96
+ method: payload[:method],
97
+ path: payload[:path],
98
+ format: payload[:format],
99
+ controller: payload[:controller],
100
+ action: payload[:action],
101
+ status: status,
102
+ duration_s: duration_s,
87
103
  duration_human: ExisRay::Tracer.format_duration(duration_s),
88
104
  view_runtime_s: view_s,
89
- db_runtime_s: db_s
105
+ db_runtime_s: db_s
90
106
  }
91
107
 
92
108
  data.merge!(self.class.extra_fields(event))
@@ -13,7 +13,13 @@ module ExisRay
13
13
  # Intercepta las peticiones entrantes para hidratar el Tracer.
14
14
  initializer "exis_ray.configure_middleware" do |app|
15
15
  require "exis_ray/http_middleware"
16
- app.middleware.insert_after ActionDispatch::RequestId, ExisRay::HttpMiddleware
16
+ if app.middleware.respond_to?(:include?) && app.middleware.include?(ActionDispatch::RequestId)
17
+ app.middleware.insert_after ActionDispatch::RequestId, ExisRay::HttpMiddleware
18
+ else
19
+ app.middleware.use ExisRay::HttpMiddleware
20
+ end
21
+ rescue NoMethodError
22
+ app.middleware.use ExisRay::HttpMiddleware
17
23
  end
18
24
 
19
25
  # 2. Configuración de Estrategia de Logging
@@ -32,6 +38,25 @@ module ExisRay
32
38
  # 3. Integraciones Post-Boot y Forzado de Formateadores
33
39
  # Se ejecuta una vez que las gemas y el entorno de Rails están completamente cargados.
34
40
  config.after_initialize do
41
+ # Validación de configuración: solo cuando eager_load=true (producción/staging),
42
+ # donde todos los constantes de app/ están garantizados. En desarrollo con lazy
43
+ # loading las clases pueden no estar cargadas aún en este punto.
44
+ if Rails.application.config.eager_load
45
+ if (name = ExisRay.configuration.current_class).present?
46
+ klass = name.safe_constantize
47
+ unless klass&.<=(ExisRay::Current)
48
+ raise "ExisRay: current_class '#{name}' not found or doesn't inherit from ExisRay::Current"
49
+ end
50
+ end
51
+
52
+ if (name = ExisRay.configuration.reporter_class).present?
53
+ klass = name.safe_constantize
54
+ unless klass&.<=(ExisRay::Reporter)
55
+ raise "ExisRay: reporter_class '#{name}' not found or doesn't inherit from ExisRay::Reporter"
56
+ end
57
+ end
58
+ end
59
+
35
60
  # Aplicamos el formateador JSON globalmente al logger ya instanciado de Rails
36
61
  if ExisRay.configuration.json_logs? && Rails.logger
37
62
  Rails.logger.formatter = ExisRay::JsonFormatter.new
@@ -72,8 +97,10 @@ module ExisRay
72
97
  # --- Instrumentación de ActiveResource ---
73
98
  if defined?(ActiveResource::Base)
74
99
  require "exis_ray/active_resource_instrumentation"
75
- ActiveResource::Base.send(:prepend, ExisRay::ActiveResourceInstrumentation)
76
- log_boot("component=exis_ray event=active_resource_instrumented")
100
+ unless ActiveResource::Base.ancestors.include?(ExisRay::ActiveResourceInstrumentation)
101
+ ActiveResource::Base.prepend ExisRay::ActiveResourceInstrumentation
102
+ log_boot("component=exis_ray event=active_resource_instrumented")
103
+ end
77
104
  end
78
105
 
79
106
  # --- Instrumentación de Sidekiq ---
@@ -96,9 +123,7 @@ module ExisRay
96
123
  end
97
124
  end
98
125
 
99
- if ExisRay.configuration.json_logs? && ::Sidekiq.logger
100
- ::Sidekiq.logger.formatter = ExisRay::JsonFormatter.new
101
- end
126
+ ::Sidekiq.logger.formatter = ExisRay::JsonFormatter.new if ExisRay.configuration.json_logs? && ::Sidekiq.logger
102
127
  log_boot("component=exis_ray event=sidekiq_instrumented")
103
128
  end
104
129
  end