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 +4 -4
- data/CHANGELOG.md +28 -0
- data/lib/exis_ray/active_resource_instrumentation.rb +14 -17
- data/lib/exis_ray/bug_bunny/consumer_tracing_middleware.rb +10 -3
- data/lib/exis_ray/current.rb +51 -61
- data/lib/exis_ray/faraday_middleware.rb +17 -10
- data/lib/exis_ray/http_middleware.rb +6 -7
- data/lib/exis_ray/json_formatter.rb +53 -19
- data/lib/exis_ray/log_subscriber.rb +29 -13
- data/lib/exis_ray/railtie.rb +31 -6
- data/lib/exis_ray/reporter.rb +33 -30
- data/lib/exis_ray/sidekiq/client_middleware.rb +28 -19
- data/lib/exis_ray/sidekiq/server_middleware.rb +41 -22
- data/lib/exis_ray/task_monitor.rb +10 -7
- data/lib/exis_ray/tracer.rb +26 -18
- data/lib/exis_ray/version.rb +1 -1
- data/spec/exis_ray/configuration_spec.rb +93 -0
- data/spec/exis_ray/current_spec.rb +115 -0
- data/spec/exis_ray/json_formatter_spec.rb +68 -19
- data/spec/exis_ray/reporter_spec.rb +95 -0
- data/spec/exis_ray/tracer_spec.rb +232 -0
- data/spec/spec_helper.rb +16 -2
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ad15b3b5656e54690df31596233aabafaf70a5061b9086d6a601a60370fca16
|
|
4
|
+
data.tar.gz: 6d2d87410cb374c1f8c9ddee2b6caf730f959a6c87bc63004ef43e52a290d8c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
data/lib/exis_ray/current.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
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
|
-
@
|
|
16
|
-
@
|
|
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(
|
|
25
|
-
ActiveResource::Base.headers.delete(
|
|
26
|
-
ActiveResource::Base.headers.delete(
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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?(::
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
81
|
-
return nil
|
|
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
|
-
@
|
|
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
|
-
|
|
97
|
-
return nil
|
|
76
|
+
return nil if isp_id.nil?
|
|
77
|
+
return nil unless defined?(::Isp) && ::Isp.respond_to?(:find_by)
|
|
98
78
|
|
|
99
|
-
|
|
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.
|
|
83
|
+
!user_id.nil?
|
|
108
84
|
end
|
|
109
85
|
|
|
110
86
|
def isp?
|
|
111
|
-
isp_id.
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
12
|
+
source: "http"
|
|
14
13
|
)
|
|
15
|
-
ExisRay::Tracer.request_id = env[
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
79
|
-
event:
|
|
80
|
-
method:
|
|
81
|
-
path:
|
|
82
|
-
format:
|
|
83
|
-
controller:
|
|
84
|
-
action:
|
|
85
|
-
status:
|
|
86
|
-
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:
|
|
105
|
+
db_runtime_s: db_s
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
data.merge!(self.class.extra_fields(event))
|
data/lib/exis_ray/railtie.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
76
|
-
|
|
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
|