exis_ray 0.3.2 → 0.3.4
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 +27 -0
- data/README.md +20 -0
- data/lib/exis_ray/active_resource_instrumentation.rb +3 -2
- data/lib/exis_ray/current.rb +33 -18
- data/lib/exis_ray/http_middleware.rb +1 -1
- data/lib/exis_ray/json_formatter.rb +12 -5
- data/lib/exis_ray/reporter.rb +25 -4
- data/lib/exis_ray/sidekiq/client_middleware.rb +4 -4
- data/lib/exis_ray/sidekiq/server_middleware.rb +5 -3
- data/lib/exis_ray/task_monitor.rb +7 -3
- data/lib/exis_ray/tracer.rb +5 -2
- data/lib/exis_ray/version.rb +1 -1
- data/lib/exis_ray.rb +31 -4
- data/spec/exis_ray/json_formatter_spec.rb +12 -0
- 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: 368df7fa55a8dc849917a5854026d50752f3701afede7bc9418c83e229a1fe50
|
|
4
|
+
data.tar.gz: 7998fda22e20966eae15450faf4c2e73e103c24e23b2decd3c41b76c605ec036
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 791eca1e5cd91703ebd7d43803b09a55f718918ac65808eecd9ae8d8141944297f9d06ab1f7b46519dcd0018e0174ebc307d14a297eefa9581440e2c6448cdb0
|
|
7
|
+
data.tar.gz: e2c88c9e68556110a8ab757009e2f824849123202f692aa71d15015d3c4aaa385fc90cd093bb7657f112a1a217f98c7ed4385de11d0c6e1f9cf3c5a5b36abfee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## [0.3.4] - 2026-03-23
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- **Compatibilidad con Rails 7.1+ y Rails 8:** `config.cache_classes` fue deprecado en Rails 7.1 en favor de `config.enable_reloading` (semántica inversa). El helper interno `cache_classes?` ahora detecta cuál API está disponible y usa la correcta, manteniendo compatibilidad con Rails 6, 7 y 8 sin deprecation warnings.
|
|
5
|
+
|
|
6
|
+
## [0.3.3] - 2026-03-23
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **`ActiveResourceInstrumentation` header incorrecto:** Se corrigió el uso de `trace_header` (formato Rack, ej: `HTTP_X_AMZN_TRACE_ID`) para requests salientes. Ahora se usa `propagation_trace_header` (formato HTTP, ej: `X-Amzn-Trace-Id`), igual que `FaradayMiddleware`. Antes, los microservicios downstream recibían un header con nombre incorrecto y la traza distribuida no se propagaba.
|
|
10
|
+
- **Header injection en `Current` setters:** Los valores asignados a `user_id=`, `isp_id=` y `correlation_id=` ahora son sanitizados antes de escribirse en `ActiveResource::Base.headers`. Se eliminan caracteres CRLF (`\r\n`) para prevenir HTTP header injection hacia otros microservicios.
|
|
11
|
+
- **`Reporter` exponía datos sensibles a Sentry:** `build_from_current` enviaba `user.as_json` completo (incluyendo `password_digest`, tokens, etc.) al contexto de Sentry. Ahora el comportamiento por defecto es enviar solo `{ id: }`. Se exponen los hooks `sentry_user_context` y `sentry_isp_context` para que la app host controle qué campos incluir.
|
|
12
|
+
- **Reloj de pared en cálculo de duración:** `Tracer.created_at` y `current_duration_ms` usaban `Time.now` (afectado por NTP/leap seconds). Ahora usan `Process.clock_gettime(Process::CLOCK_MONOTONIC)` en todos los puntos de asignación (`HttpMiddleware`, `Sidekiq::ServerMiddleware`, `TaskMonitor`) y en el cálculo de duración.
|
|
13
|
+
- **`generate_new_root` ignoraba sufijos no numéricos:** El sufijo del pod/hostname se convertía con `.to_i`, retornando siempre `0` para strings alfanuméricos (ej: `"worker01"` → `00000000`). Ahora se codifican los bytes del string directamente a hex, preservando la unicidad del identificador.
|
|
14
|
+
- **`JsonFormatter#parse_kv_string` crash con quote suelto:** Un valor de un solo caracter `"` provocaba que `value[1..-2]` retornara `nil` y `.gsub` explotara con `NoMethodError`. Corregido con `|| ""` como fallback.
|
|
15
|
+
- **Pérdida silenciosa de mensaje en `JsonFormatter`:** Si `kv_string?` detectaba un string como kv pero `parse_kv_string` no extraía ningún par (ej: `"key="`), el mensaje original desaparecía del JSON sin ir al campo `message`. Ahora cae al fallback correctamente.
|
|
16
|
+
- **`TaskMonitor#log_event` podía enmascarar excepciones del negocio:** Si el logger fallaba dentro de `log_event`, su excepción reemplazaba la excepción original de la tarea. Ahora `log_event` está protegido con `rescue StandardError` interno.
|
|
17
|
+
- **`current_class` y `reporter_class` resueltos dos veces en `ensure`:** En `Sidekiq::ServerMiddleware` y `TaskMonitor`, el bloque `ensure` llamaba `safe_constantize` dos veces por clase. Ahora se asigna a variable local antes del `ensure`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **`sentry_user_context` y `sentry_isp_context` como hooks públicos en `Reporter`:** La subclase puede sobreescribir estos métodos para definir exactamente qué atributos del modelo se envían a Sentry, sin exponer datos sensibles por defecto.
|
|
21
|
+
|
|
22
|
+
### Performance
|
|
23
|
+
- **Memoización de `current_class` y `reporter_class` en producción:** En entornos con `cache_classes=true`, la resolución vía `safe_constantize` se ejecuta una sola vez. En desarrollo se sigue resolviendo por request para respetar el reloading de Zeitwerk.
|
|
24
|
+
- **Regex de `JsonFormatter` extraídas a constantes:** `KV_DETECT_RE` y `KV_PARSE_RE` son ahora constantes de clase, eliminando recompilaciones innecesarias en cada línea de log.
|
|
25
|
+
- **`Current#user` e `Current#isp` usan sentinel para cachear `nil`:** Si `find_by` no encuentra el registro, el resultado se cachea con un objeto sentinel `NOT_FOUND`. Las llamadas subsiguientes retornan `nil` sin consultar la DB.
|
|
26
|
+
- **`Sidekiq::ClientMiddleware` resuelve `current_class` una sola vez** por job encolado en lugar de cuatro veces.
|
|
27
|
+
|
|
1
28
|
## [0.3.2] - 2026-03-23
|
|
2
29
|
|
|
3
30
|
### Added
|
data/README.md
CHANGED
|
@@ -102,6 +102,26 @@ class Choto < ExisRay::Reporter
|
|
|
102
102
|
end
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
#### Controlling what user/ISP data is sent to Sentry
|
|
106
|
+
|
|
107
|
+
By default, ExisRay only sends `{ id: }` to Sentry for both user and ISP contexts. This is intentional — sending `user.as_json` without restrictions would expose sensitive attributes (`password_digest`, `reset_password_token`, etc.) to a third-party service.
|
|
108
|
+
|
|
109
|
+
To include additional fields, override the hooks in your subclass:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class Choto < ExisRay::Reporter
|
|
113
|
+
def self.sentry_user_context(current)
|
|
114
|
+
{ id: current.user_id, email: current.user&.email, role: current.user&.role }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.sentry_isp_context(current)
|
|
118
|
+
{ id: current.isp_id, name: current.isp&.name }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> **Important:** never include attributes like `password_digest`, tokens, or secrets in these methods.
|
|
124
|
+
|
|
105
125
|
### 3. Hydrate Context (Controller)
|
|
106
126
|
|
|
107
127
|
In your `ApplicationController`, verify the incoming request and set the context. ExisRay handles the Trace ID automatically, you just handle the Business Logic.
|
|
@@ -25,8 +25,9 @@ module ExisRay
|
|
|
25
25
|
# Generamos el string propagable: "Root=...;Parent=...;Sampled=..."
|
|
26
26
|
trace_header_value = ExisRay::Tracer.generate_trace_header
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
|
|
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
|
|
30
31
|
|
|
31
32
|
# Retornamos un nuevo hash combinado (merge) para no mutar el original por error
|
|
32
33
|
original_headers.merge(trace_header_key => trace_header_value)
|
data/lib/exis_ray/current.rb
CHANGED
|
@@ -6,10 +6,14 @@ module ExisRay
|
|
|
6
6
|
class Current < ActiveSupport::CurrentAttributes
|
|
7
7
|
attribute :user_id, :isp_id, :correlation_id
|
|
8
8
|
|
|
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
|
+
|
|
9
13
|
# Callback nativo de Rails: Se ejecuta automáticamente al llamar a Current.reset
|
|
10
14
|
resets do
|
|
11
|
-
@user =
|
|
12
|
-
@isp =
|
|
15
|
+
@user = NOT_FOUND
|
|
16
|
+
@isp = NOT_FOUND
|
|
13
17
|
|
|
14
18
|
if defined?(PaperTrail)
|
|
15
19
|
PaperTrail.request.whodunnit = nil
|
|
@@ -28,7 +32,7 @@ module ExisRay
|
|
|
28
32
|
def user_id=(id)
|
|
29
33
|
super
|
|
30
34
|
if defined?(ActiveResource::Base)
|
|
31
|
-
ActiveResource::Base.headers['UserId'] = id
|
|
35
|
+
ActiveResource::Base.headers['UserId'] = sanitize_header_value(id)
|
|
32
36
|
end
|
|
33
37
|
if defined?(PaperTrail)
|
|
34
38
|
PaperTrail.request.whodunnit = id
|
|
@@ -37,9 +41,9 @@ module ExisRay
|
|
|
37
41
|
|
|
38
42
|
def isp_id=(id)
|
|
39
43
|
super
|
|
40
|
-
@isp =
|
|
44
|
+
@isp = NOT_FOUND # Invalida cache
|
|
41
45
|
if defined?(ActiveResource::Base)
|
|
42
|
-
ActiveResource::Base.headers['IspId'] = id
|
|
46
|
+
ActiveResource::Base.headers['IspId'] = sanitize_header_value(id)
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -51,7 +55,7 @@ module ExisRay
|
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
if defined?(ActiveResource::Base)
|
|
54
|
-
ActiveResource::Base.headers['CorrelationId'] = id
|
|
58
|
+
ActiveResource::Base.headers['CorrelationId'] = sanitize_header_value(id)
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
if defined?(PaperTrail)
|
|
@@ -68,33 +72,35 @@ module ExisRay
|
|
|
68
72
|
# Estos métodos asumen que la app host tiene modelos ::User e ::Isp
|
|
69
73
|
|
|
70
74
|
def user=(object)
|
|
71
|
-
@user = object
|
|
75
|
+
@user = object || NOT_FOUND
|
|
72
76
|
self.user_id = object&.id
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
def user
|
|
76
|
-
|
|
80
|
+
@user = NOT_FOUND unless defined?(@user)
|
|
81
|
+
return nil if @user.equal?(NOT_FOUND) && !user_id
|
|
77
82
|
|
|
78
|
-
if
|
|
79
|
-
@user = ::User.find_by(id: user_id)
|
|
80
|
-
else
|
|
81
|
-
nil
|
|
83
|
+
if @user.equal?(NOT_FOUND)
|
|
84
|
+
@user = (defined?(::User) && ::User.respond_to?(:find_by) ? ::User.find_by(id: user_id) : nil) || NOT_FOUND
|
|
82
85
|
end
|
|
86
|
+
|
|
87
|
+
@user.equal?(NOT_FOUND) ? nil : @user
|
|
83
88
|
end
|
|
84
89
|
|
|
85
90
|
def isp=(object)
|
|
86
|
-
@isp = object
|
|
91
|
+
@isp = object || NOT_FOUND
|
|
87
92
|
self.isp_id = object&.id
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
def isp
|
|
91
|
-
|
|
96
|
+
@isp = NOT_FOUND unless defined?(@isp)
|
|
97
|
+
return nil if @isp.equal?(NOT_FOUND) && !isp_id
|
|
92
98
|
|
|
93
|
-
if
|
|
94
|
-
@isp = ::Isp.find_by(id: isp_id)
|
|
95
|
-
else
|
|
96
|
-
nil
|
|
99
|
+
if @isp.equal?(NOT_FOUND)
|
|
100
|
+
@isp = (defined?(::Isp) && ::Isp.respond_to?(:find_by) ? ::Isp.find_by(id: isp_id) : nil) || NOT_FOUND
|
|
97
101
|
end
|
|
102
|
+
|
|
103
|
+
@isp.equal?(NOT_FOUND) ? nil : @isp
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
def user?
|
|
@@ -108,5 +114,14 @@ module ExisRay
|
|
|
108
114
|
def correlation_id?
|
|
109
115
|
correlation_id.present?
|
|
110
116
|
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
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.
|
|
123
|
+
def sanitize_header_value(value)
|
|
124
|
+
value.to_s.gsub(/[\r\n]/, '')
|
|
125
|
+
end
|
|
111
126
|
end
|
|
112
127
|
end
|
|
@@ -8,7 +8,7 @@ module ExisRay
|
|
|
8
8
|
|
|
9
9
|
def call(env)
|
|
10
10
|
# 1. Hidratar Infraestructura
|
|
11
|
-
ExisRay::Tracer.created_at =
|
|
11
|
+
ExisRay::Tracer.created_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
12
12
|
ExisRay::Tracer.source = "http"
|
|
13
13
|
|
|
14
14
|
trace_header_key = ExisRay.configuration.trace_header
|
|
@@ -15,10 +15,16 @@ module ExisRay
|
|
|
15
15
|
# Automáticamente inyecta el contexto de trazabilidad ({ExisRay::Tracer})
|
|
16
16
|
# y el contexto de negocio ({ExisRay::Current}) en cada línea de log.
|
|
17
17
|
class JsonFormatter < ::Logger::Formatter
|
|
18
|
-
# Solución al NoMethodError: Garantiza que el formateador sea compatible
|
|
18
|
+
# Solución al NoMethodError: Garantiza que el formateador sea compatible
|
|
19
19
|
# con el wrapper de ActiveSupport::TaggedLogging de Rails.
|
|
20
20
|
include ActiveSupport::TaggedLogging::Formatter if defined?(ActiveSupport::TaggedLogging::Formatter)
|
|
21
21
|
|
|
22
|
+
# Detecta si un string comienza con al menos un par key=value.
|
|
23
|
+
KV_DETECT_RE = /\A\w+=/
|
|
24
|
+
|
|
25
|
+
# Extrae pares key=value de un string. Soporta valores sin espacios o entre comillas dobles.
|
|
26
|
+
KV_PARSE_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/
|
|
27
|
+
|
|
22
28
|
# Procesa un mensaje de log y lo formatea como una cadena estructurada en JSON.
|
|
23
29
|
#
|
|
24
30
|
# @param severity [String] El nivel de severidad del log (ej. "INFO", "ERROR", "DEBUG").
|
|
@@ -99,7 +105,8 @@ module ExisRay
|
|
|
99
105
|
if msg.is_a?(Hash)
|
|
100
106
|
payload.merge!(msg)
|
|
101
107
|
elsif msg.is_a?(String) && kv_string?(msg)
|
|
102
|
-
|
|
108
|
+
parsed = parse_kv_string(msg)
|
|
109
|
+
parsed.empty? ? payload[:message] = msg : payload.merge!(parsed)
|
|
103
110
|
else
|
|
104
111
|
payload[:message] = msg.to_s
|
|
105
112
|
end
|
|
@@ -111,7 +118,7 @@ module ExisRay
|
|
|
111
118
|
# @param str [String]
|
|
112
119
|
# @return [Boolean]
|
|
113
120
|
def kv_string?(str)
|
|
114
|
-
str.match?(
|
|
121
|
+
str.match?(KV_DETECT_RE)
|
|
115
122
|
end
|
|
116
123
|
|
|
117
124
|
# Parsea un string con formato key=value y retorna un Hash.
|
|
@@ -121,8 +128,8 @@ module ExisRay
|
|
|
121
128
|
# @return [Hash]
|
|
122
129
|
def parse_kv_string(str)
|
|
123
130
|
result = {}
|
|
124
|
-
str.scan(
|
|
125
|
-
result[key.to_sym] = value.start_with?('"') ? value[1..-2].gsub('\\"', '"') : value
|
|
131
|
+
str.scan(KV_PARSE_RE) do |key, value|
|
|
132
|
+
result[key.to_sym] = value.start_with?('"') ? (value[1..-2] || "").gsub('\\"', '"') : value
|
|
126
133
|
end
|
|
127
134
|
result
|
|
128
135
|
end
|
data/lib/exis_ray/reporter.rb
CHANGED
|
@@ -172,16 +172,37 @@ module ExisRay
|
|
|
172
172
|
add_tags(isp_id: klass.isp_id) if klass.respond_to?(:isp_id?) && klass.isp_id?
|
|
173
173
|
|
|
174
174
|
if klass.respond_to?(:user) && klass.user.present?
|
|
175
|
-
|
|
176
|
-
add_context(user: user_json)
|
|
175
|
+
add_context(user: sentry_user_context(klass))
|
|
177
176
|
end
|
|
178
177
|
|
|
179
178
|
if klass.respond_to?(:isp) && klass.isp.present?
|
|
180
|
-
|
|
181
|
-
add_context(isp: isp_json)
|
|
179
|
+
add_context(isp: sentry_isp_context(klass))
|
|
182
180
|
end
|
|
183
181
|
end
|
|
184
182
|
|
|
183
|
+
# Hook para que la app host controle qué datos del usuario se envían a Sentry.
|
|
184
|
+
# Por defecto solo se envía el ID para evitar exponer datos sensibles (password_digest, tokens, etc.).
|
|
185
|
+
#
|
|
186
|
+
# @example Sobreescribir en la subclase del Reporter de la app
|
|
187
|
+
# def self.sentry_user_context(current)
|
|
188
|
+
# { id: current.user_id, email: current.user&.email }
|
|
189
|
+
# end
|
|
190
|
+
#
|
|
191
|
+
# @param current [Class] La clase Current resuelta.
|
|
192
|
+
# @return [Hash]
|
|
193
|
+
def self.sentry_user_context(current)
|
|
194
|
+
{ id: current.user_id }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Hook para que la app host controle qué datos del ISP se envían a Sentry.
|
|
198
|
+
# Por defecto solo se envía el ID.
|
|
199
|
+
#
|
|
200
|
+
# @param current [Class] La clase Current resuelta.
|
|
201
|
+
# @return [Hash]
|
|
202
|
+
def self.sentry_isp_context(current)
|
|
203
|
+
{ id: current.isp_id }
|
|
204
|
+
end
|
|
205
|
+
|
|
185
206
|
private_class_method :build_from_current, :build_from_tracer
|
|
186
207
|
end
|
|
187
208
|
end
|
|
@@ -14,11 +14,11 @@ module ExisRay
|
|
|
14
14
|
|
|
15
15
|
# 2. Inyectamos el contexto de negocio (Current)
|
|
16
16
|
# Esto permite saber qué Usuario o ISP disparó el job.
|
|
17
|
-
if ExisRay.current_class.present?
|
|
17
|
+
if (curr = ExisRay.current_class).present?
|
|
18
18
|
context = {}
|
|
19
|
-
context[:user_id]
|
|
20
|
-
context[:isp_id]
|
|
21
|
-
context[:correlation_id] =
|
|
19
|
+
context[:user_id] = curr.user_id if curr.respond_to?(:user_id)
|
|
20
|
+
context[:isp_id] = curr.isp_id if curr.respond_to?(:isp_id)
|
|
21
|
+
context[:correlation_id] = curr.correlation_id if curr.respond_to?(:correlation_id)
|
|
22
22
|
|
|
23
23
|
job['exis_ray_context'] = context
|
|
24
24
|
end
|
|
@@ -32,8 +32,10 @@ module ExisRay
|
|
|
32
32
|
ensure
|
|
33
33
|
# Limpieza vital en Sidekiq para evitar fugas de contexto entre jobs en el mismo hilo.
|
|
34
34
|
ExisRay::Tracer.reset
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
current = ExisRay.current_class
|
|
36
|
+
reporter = ExisRay.reporter_class
|
|
37
|
+
current.reset if current&.respond_to?(:reset)
|
|
38
|
+
reporter.reset if reporter&.respond_to?(:reset)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
@@ -44,7 +46,7 @@ module ExisRay
|
|
|
44
46
|
# @param job [Hash] Payload de Sidekiq.
|
|
45
47
|
# @return [void]
|
|
46
48
|
def hydrate_tracer(worker, job)
|
|
47
|
-
ExisRay::Tracer.created_at =
|
|
49
|
+
ExisRay::Tracer.created_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
50
|
ExisRay::Tracer.sidekiq_job = worker.class.name
|
|
49
51
|
ExisRay::Tracer.source = "sidekiq"
|
|
50
52
|
|
|
@@ -41,8 +41,10 @@ module ExisRay
|
|
|
41
41
|
ensure
|
|
42
42
|
# Limpieza centralizada obligatoria para evitar filtraciones de memoria o contexto
|
|
43
43
|
ExisRay::Tracer.reset
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
current = ExisRay.current_class
|
|
45
|
+
reporter = ExisRay.reporter_class
|
|
46
|
+
current.reset if current&.respond_to?(:reset)
|
|
47
|
+
reporter.reset if reporter&.respond_to?(:reset)
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
# --- Métodos Privados ---
|
|
@@ -55,7 +57,7 @@ module ExisRay
|
|
|
55
57
|
ExisRay::Tracer.task = task_name.to_s
|
|
56
58
|
ExisRay::Tracer.source = "task"
|
|
57
59
|
ExisRay::Tracer.request_id = SecureRandom.uuid
|
|
58
|
-
ExisRay::Tracer.created_at =
|
|
60
|
+
ExisRay::Tracer.created_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
59
61
|
|
|
60
62
|
pod_id = get_pod_identifier
|
|
61
63
|
ExisRay::Tracer.root_id = ExisRay::Tracer.send(:generate_new_root, pod_id)
|
|
@@ -93,6 +95,8 @@ module ExisRay
|
|
|
93
95
|
else
|
|
94
96
|
Rails.logger.send(level, "[ExisRay] #{message}")
|
|
95
97
|
end
|
|
98
|
+
rescue StandardError
|
|
99
|
+
# El logger nunca debe interrumpir el flujo principal de la tarea.
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
private_class_method :get_pod_identifier, :setup_tracer, :execute_with_optional_tags, :log_event
|
data/lib/exis_ray/tracer.rb
CHANGED
|
@@ -59,7 +59,7 @@ module ExisRay
|
|
|
59
59
|
# @return [Integer] Duración en ms.
|
|
60
60
|
def self.current_duration_ms
|
|
61
61
|
return 0 unless created_at
|
|
62
|
-
((
|
|
62
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - created_at) * 1000).round
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Construye el header de trazabilidad para enviar al siguiente servicio.
|
|
@@ -89,7 +89,10 @@ module ExisRay
|
|
|
89
89
|
timestamp_hex = Time.now.to_i.to_s(16)
|
|
90
90
|
|
|
91
91
|
if suffix_id.present?
|
|
92
|
-
|
|
92
|
+
# Codificamos los bytes del string a hex para preservar unicidad
|
|
93
|
+
# independientemente de si el sufijo es numérico o alfanumérico.
|
|
94
|
+
# Ej: "worker01" → "776f726b657230 31", "abc" → "616263"
|
|
95
|
+
suffix_hex = suffix_id.to_s.bytes.map { |b| b.to_s(16).rjust(2, '0') }.join.first(8).rjust(8, '0')
|
|
93
96
|
unique_part = SecureRandom.hex(8) + suffix_hex
|
|
94
97
|
else
|
|
95
98
|
unique_part = SecureRandom.hex(12)
|
data/lib/exis_ray/version.rb
CHANGED
data/lib/exis_ray.rb
CHANGED
|
@@ -60,7 +60,8 @@ module ExisRay
|
|
|
60
60
|
# --- Helpers Centralizados de Resolución de Clases ---
|
|
61
61
|
|
|
62
62
|
# Resuelve y retorna la clase configurada para manejar el contexto de negocio (Current).
|
|
63
|
-
#
|
|
63
|
+
# En producción (cache_classes=true) memoiza el resultado para evitar safe_constantize
|
|
64
|
+
# en cada request. En desarrollo siempre resuelve para soportar el reloading de Zeitwerk.
|
|
64
65
|
#
|
|
65
66
|
# @return [Class, nil] La clase constante (ej: Current) o nil si no se encuentra/configura.
|
|
66
67
|
def current_class
|
|
@@ -69,12 +70,15 @@ module ExisRay
|
|
|
69
70
|
klass_name = configuration.current_class
|
|
70
71
|
return nil unless klass_name.present?
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
if cache_classes?
|
|
74
|
+
@current_class_cache ||= resolve_class(klass_name)
|
|
75
|
+
else
|
|
76
|
+
resolve_class(klass_name)
|
|
77
|
+
end
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
# Resuelve y retorna la clase configurada para el reporte de errores (Reporter).
|
|
77
|
-
#
|
|
81
|
+
# En producción memoiza el resultado. En desarrollo siempre resuelve.
|
|
78
82
|
#
|
|
79
83
|
# @return [Class, nil] La clase constante (ej: Choto) o nil si no se encuentra/configura.
|
|
80
84
|
def reporter_class
|
|
@@ -83,7 +87,30 @@ module ExisRay
|
|
|
83
87
|
klass_name = configuration.reporter_class
|
|
84
88
|
return nil unless klass_name.present?
|
|
85
89
|
|
|
90
|
+
if cache_classes?
|
|
91
|
+
@reporter_class_cache ||= resolve_class(klass_name)
|
|
92
|
+
else
|
|
93
|
+
resolve_class(klass_name)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def resolve_class(klass_name)
|
|
86
100
|
klass_name.is_a?(String) ? klass_name.safe_constantize : klass_name
|
|
87
101
|
end
|
|
102
|
+
|
|
103
|
+
def cache_classes?
|
|
104
|
+
return false unless defined?(Rails)
|
|
105
|
+
|
|
106
|
+
config = Rails.application.config
|
|
107
|
+
# Rails 7.1+ reemplazó cache_classes por enable_reloading (semántica inversa).
|
|
108
|
+
# Soportamos ambas APIs para mantener compatibilidad con Rails 6, 7 y 8.
|
|
109
|
+
if config.respond_to?(:enable_reloading)
|
|
110
|
+
!config.enable_reloading
|
|
111
|
+
else
|
|
112
|
+
config.cache_classes
|
|
113
|
+
end
|
|
114
|
+
end
|
|
88
115
|
end
|
|
89
116
|
end
|
|
@@ -82,6 +82,10 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
82
82
|
expect(result["msg"]).to eq('dijo "hola"')
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
it "no crashea con un quote suelto como valor (string malformado)" do
|
|
86
|
+
expect { call('key="') }.not_to raise_error
|
|
87
|
+
end
|
|
88
|
+
|
|
85
89
|
it "soporta múltiples pares con tipos de valor variados" do
|
|
86
90
|
result = call("component=orders event=invoice_generated duration_ms=42.5 retries=0")
|
|
87
91
|
|
|
@@ -94,6 +98,14 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
94
98
|
end
|
|
95
99
|
end
|
|
96
100
|
|
|
101
|
+
it "cae a message si el string parece kv pero no produce ningún par" do
|
|
102
|
+
result = call("key=")
|
|
103
|
+
|
|
104
|
+
expect(result["message"]).to eq("key=")
|
|
105
|
+
expect(result).not_to have_key("key")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
97
109
|
context "cuando el mensaje es un String libre (sin formato key=value)" do
|
|
98
110
|
it "asigna el string completo al campo message" do
|
|
99
111
|
result = call("algo salió mal")
|