exis_ray 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb94b36fd16b408d5359621e78a03c722c89dab7c087322b67c975db743ad24e
4
- data.tar.gz: ef093be07091568e6bad52d26de306dd82f91fc8e8f554d4eb04a7ca8c3b1581
3
+ metadata.gz: c916d765b57e909cd3320e1670623536907d648c360039cbb5cada88b88a86fd
4
+ data.tar.gz: cab31ec104f79b58ecbd2ad8a8758f55f767e217f09c2880a3b911972a743112
5
5
  SHA512:
6
- metadata.gz: ce25522741b66f66318fa6d5223c0db754f1fe8f7eb1067a867f219988a1bc4ae78ef55c61e0895d553df955f129165af211a52dd41c86d6e3f6fe7066f7d7b2
7
- data.tar.gz: 7ab403c8790dae80419260834d18787faa060bb2528d10974a4ef255bd6a0ea79c9aa837040015360db531b6114c8d99b4412c592d6749a0cba298ce24d072bc
6
+ metadata.gz: b428640a4df2e1b64aa798568769c0d9902e503aa59fb14f7d2f34948115883f35e436097504de34a879eeb63890bbb031dc68e9f2adb672fa24c04c6064a47c
7
+ data.tar.gz: aa9c8bb741789384b66d1a98b9e5d5008d9e5eeea3802ecde39264c08c8ed95be8114285005c03dfb11d3506c2d4efb65c59336bc582d8b9cc8ccb1cf3bdb577
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [0.5.0] - 2026-03-23
2
+
3
+ ### Changed
4
+ - **Wispro-Observability-Spec (v1) Compliance:** Refactored telemetry across all execution layers (`TaskMonitor` and HTTP `LogSubscriber`).
5
+ - **Unit Standard:** Duration metrics now use `_s` suffix (Float) as the source of truth (e.g., `duration_s`, `view_runtime_s`, `db_runtime_s`).
6
+ - **Human Readability:** Added `duration_human` to both HTTP requests and Background Tasks.
7
+ - **Task Lifecycle:** The closing log now always uses `event=task_finished` with `status`, `duration_s`, and `duration_human` fields.
8
+ - **Error Consistency:** Failed tasks now include `error_class` and `error_message` fields in the JSON log.
9
+ - **Tracer Accuracy:** Improved `Tracer` with `current_duration_s` using monotonic clock precision.
10
+
11
+ ## [0.4.2] - 2026-03-23
12
+
13
+ ### Added
14
+ - **Full compliance with "Gabriel's Global Standards" for Logging:**
15
+ - **Sensitive Data Filtering:** The KV parser and Hash merger now automatically filter sensitive keys (`password`, `token`, `api_key`, `auth`, etc.) replacing values with `[FILTERED]`.
16
+ - **Standardized HTTP Logs:** Added `component=exis_ray` and `event=http_request` to all automatic Rails request logs.
17
+ - **Dynamic Log Levels:** `LogSubscriber` now uses `ERROR` level for 5xx status codes and `INFO` for the rest.
18
+ - **Performance:** Switched to block form for internal `DEBUG` logs in `Railtie` to avoid unnecessary string interpolation.
19
+
1
20
  ## [0.4.1] - 2026-03-23
2
21
 
3
22
  ### Changed
data/CLAUDE.md ADDED
@@ -0,0 +1,27 @@
1
+ # ExisRay Project Memory
2
+
3
+ ## Architecture & Components
4
+ - `ExisRay::Tracer`: Distributed tracing (AWS X-Ray). Uses `CurrentAttributes` for thread-safety.
5
+ - `ExisRay::Current`: Business context (User, ISP). Abstract base class for host app subclassing.
6
+ - `ExisRay::Reporter`: Sentry wrapper. Abstract base class for host app subclassing.
7
+ - `ExisRay::JsonFormatter`: Core engine. Handles Hash, KV strings, and free-text with automatic masking.
8
+ - `ExisRay::LogSubscriber`: Native HTTP logger since v0.4.0. Replaces Lograge.
9
+
10
+ ## Technical Knowledge & Compatibility
11
+
12
+ ### Rails Compatibility (6, 7, 8)
13
+ - **Reloading:** Use `cache_classes?` helper (checks `respond_to?(:enable_reloading)`) to avoid deprecation warnings in Rails 7.1+.
14
+ - **Notifications:** Rails 7.1+ uses `all_listeners_for`, while 6/7.0 uses `listeners_for`. Always use `respond_to?` guards when manipulating subscribers.
15
+
16
+ ### Distributed Tracing (AWS X-Ray)
17
+ - **Propagation:** Use `propagation_trace_header` (standard HTTP format) for outgoing requests.
18
+ - **Parsing:** `trace_header` (Rack format) is ONLY for incoming request parsing.
19
+
20
+ ### Security & Privacy
21
+ - **Sentry Context:** Default `sentry_user_context` and `sentry_isp_context` to `{ id: }` only. Never send full objects to avoid leaking sensitive fields (PII/Tokens).
22
+ - **Masking:** `JsonFormatter` automatically filters `password`, `token`, `api_key`, `auth`, `secret`.
23
+
24
+ ## Execution Rules
25
+ - **No Lograge:** Do not suggest or re-add the Lograge dependency.
26
+ - **Pure Data Logging:** Gem internal logs must use KV strings (`component=exis_ray event=...`).
27
+ - **Resilience:** All logging operations must be wrapped in `rescue StandardError` to ensure the host application never crashes due to a telemetry failure.
@@ -25,6 +25,9 @@ module ExisRay
25
25
  # Extrae pares key=value de un string. Soporta valores sin espacios o entre comillas dobles.
26
26
  KV_PARSE_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/
27
27
 
28
+ # Claves sensibles que deben filtrarse automáticamente según el estándar de Gabriel.
29
+ SENSITIVE_KEYS = /password|pass|passwd|secret|token|api_key|auth/i
30
+
28
31
  # Procesa un mensaje de log y lo formatea como una cadena estructurada en JSON.
29
32
  #
30
33
  # @param severity [String] El nivel de severidad del log (ej. "INFO", "ERROR", "DEBUG").
@@ -103,7 +106,8 @@ module ExisRay
103
106
  # @return [void]
104
107
  def process_message(payload, msg)
105
108
  if msg.is_a?(Hash)
106
- payload.merge!(msg)
109
+ # Filtramos las claves del Hash antes del merge
110
+ payload.merge!(filter_sensitive_hash(msg))
107
111
  elsif msg.is_a?(String) && kv_string?(msg)
108
112
  parsed = parse_kv_string(msg)
109
113
  parsed.empty? ? payload[:message] = msg : payload.merge!(parsed)
@@ -129,9 +133,33 @@ module ExisRay
129
133
  def parse_kv_string(str)
130
134
  result = {}
131
135
  str.scan(KV_PARSE_RE) do |key, value|
132
- result[key.to_sym] = value.start_with?('"') ? (value[1..-2] || "").gsub('\\"', '"') : value
136
+ val = value.start_with?('"') ? (value[1..-2] || "").gsub('\\"', '"') : value
137
+ result[key.to_sym] = filter_sensitive_value(key, val)
133
138
  end
134
139
  result
135
140
  end
141
+
142
+ # Filtra un valor si la clave se considera sensible.
143
+ #
144
+ # @param key [String, Symbol]
145
+ # @param value [Object]
146
+ # @return [Object]
147
+ def filter_sensitive_value(key, value)
148
+ key.to_s.match?(SENSITIVE_KEYS) ? "[FILTERED]" : value
149
+ end
150
+
151
+ # Filtra recursivamente un Hash que contenga claves sensibles.
152
+ #
153
+ # @param hash [Hash]
154
+ # @return [Hash]
155
+ def filter_sensitive_hash(hash)
156
+ hash.each_with_object({}) do |(k, v), result|
157
+ result[k] = if v.is_a?(Hash)
158
+ filter_sensitive_hash(v)
159
+ else
160
+ filter_sensitive_value(k, v)
161
+ end
162
+ end
163
+ end
136
164
  end
137
165
  end
@@ -32,7 +32,12 @@ module ExisRay
32
32
  # @return [void]
33
33
  def process_action(event)
34
34
  payload = build_payload(event)
35
- logger.info(payload)
35
+ # Usamos el nivel ERROR si el status es 5xx, cumpliendo el estándar de Gabriel.
36
+ if payload[:status] && payload[:status] >= 500
37
+ logger.error(payload)
38
+ else
39
+ logger.info(payload)
40
+ end
36
41
  rescue StandardError
37
42
  # El logger nunca debe interrumpir el flujo del request.
38
43
  end
@@ -64,16 +69,24 @@ module ExisRay
64
69
  payload = event.payload
65
70
  status = payload[:status] || exception_status(payload[:exception])
66
71
 
72
+ # Convertimos milisegundos (Rails default) a segundos (Estandar Wispro)
73
+ duration_s = (event.duration / 1000.0).round(4)
74
+ view_s = payload[:view_runtime] ? (payload[:view_runtime] / 1000.0).round(4) : nil
75
+ db_s = payload[:db_runtime] ? (payload[:db_runtime] / 1000.0).round(4) : nil
76
+
67
77
  data = {
68
- method: payload[:method],
69
- path: payload[:path],
70
- format: payload[:format],
71
- controller: payload[:controller],
72
- action: payload[:action],
73
- status: status,
74
- duration: event.duration.round(2),
75
- view: payload[:view_runtime]&.round(2),
76
- db: payload[:db_runtime]&.round(2)
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,
87
+ duration_human: ActiveSupport::Duration.build(duration_s).inspect,
88
+ view_runtime_s: view_s,
89
+ db_runtime_s: db_s
77
90
  }
78
91
 
79
92
  data.merge!(self.class.extra_fields(event))
@@ -85,7 +85,7 @@ module ExisRay
85
85
  # @param message [String] String en formato key=value.
86
86
  # @return [void]
87
87
  def self.log_boot(message)
88
- Rails.logger.debug(message)
88
+ Rails.logger.debug { message }
89
89
  end
90
90
  end
91
91
  end
@@ -34,9 +34,15 @@ module ExisRay
34
34
  # Bloque de ejecución con o sin tags dependiendo de la configuración
35
35
  execute_with_optional_tags { yield }
36
36
 
37
- log_event(:info, "component=exis_ray event=task_finished task=#{task_name} status=success")
37
+ duration_s = ExisRay::Tracer.current_duration_s
38
+ human_time = ActiveSupport::Duration.build(duration_s).inspect
39
+
40
+ log_event(:info, "component=exis_ray event=task_finished task=#{task_name} status=success duration_s=#{duration_s} duration_human=\"#{human_time}\"")
38
41
  rescue StandardError => e
39
- log_event(:error, "component=exis_ray event=task_failed task=#{task_name} status=failed error=#{e.message.inspect}")
42
+ duration_s = ExisRay::Tracer.current_duration_s
43
+ human_time = ActiveSupport::Duration.build(duration_s).inspect
44
+
45
+ log_event(:error, "component=exis_ray event=task_finished task=#{task_name} status=failed duration_s=#{duration_s} duration_human=\"#{human_time}\" error_class=#{e.class} error_message=#{e.message.inspect}")
40
46
  raise e
41
47
  ensure
42
48
  # Limpieza centralizada obligatoria para evitar filtraciones de memoria o contexto
@@ -58,8 +58,16 @@ module ExisRay
58
58
  #
59
59
  # @return [Integer] Duración en ms.
60
60
  def self.current_duration_ms
61
- return 0 unless created_at
62
- ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - created_at) * 1000).round
61
+ (current_duration_s * 1000).round
62
+ end
63
+
64
+ # Calcula el tiempo transcurrido en segundos desde el inicio de la request.
65
+ # Cumple con el estándar Wispro-Observability-Spec (v1).
66
+ #
67
+ # @return [Float] Duración en segundos.
68
+ def self.current_duration_s
69
+ return 0.0 unless created_at
70
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - created_at).round(4)
63
71
  end
64
72
 
65
73
  # Construye el header de trazabilidad para enviar al siguiente servicio.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
data/lib/exis_ray.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  require "exis_ray/version"
2
2
 
3
3
  # Dependencias externas
4
- # Necesario para 'safe_constantize', 'present?', y 'CurrentAttributes'
4
+ # Necesario para 'safe_constantize', 'present?', 'CurrentAttributes' y 'Duration'
5
5
  require "active_support"
6
6
  require "active_support/core_ext/string/inflections" # Para safe_constantize
7
7
  require "active_support/current_attributes"
8
+ require "active_support/duration"
9
+ require "active_support/core_ext/numeric/time" # Para .seconds, .minutes, etc.
8
10
 
9
11
  # Componentes internos del Core
10
12
  require "exis_ray/configuration"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exis_ray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Edera
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-23 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -46,6 +46,7 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - CHANGELOG.md
49
+ - CLAUDE.md
49
50
  - LICENSE.txt
50
51
  - README.md
51
52
  - Rakefile