exis_ray 0.5.3 → 0.5.5

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: d8bf4b504bfe6112b50328a358879a63b5ff19832e4724711a4650a110c94aaf
4
- data.tar.gz: fd3332690b0ff0b3ca95ccf4dddbddcb99322fd01481f9da4e11a12d3f0bf801
3
+ metadata.gz: a79e0a93c6efad500f943dad46daa5d429f133d8144db2fd1ce1e32e2b07ac68
4
+ data.tar.gz: 243915edbd6405a89656457b1371b34971efa5f377def03093d4a024ca73b661
5
5
  SHA512:
6
- metadata.gz: 639eb1ab36f013dbf034dd7be88e5c13ab77779ed6a130d0f28cb75ace6baf36b0bb7b387f7ec77cb9a37236cb5ac703daa9bfa7116160aeee59ccc18e43b9be
7
- data.tar.gz: '0882e98c833ebc8774e47f2ddca1d33ce73fd9b7d8a5427b9851eafcab4323c41b0253d512e2ec9b0fd2391120f6390eeb1be2995d924a3674e155cda7c36dcb'
6
+ metadata.gz: 6877511e716b0fb3ec6fe55a4542b7129163da596344ec1b98f8042d9bdedd9b514c2fe80ec78af474a4d18d5fd9b70bacd84669d3c467ecd2c69904555a6d94
7
+ data.tar.gz: cc0356850bfd66654bd241e3aa2654c5114ea2e3a6fbac91eab248e5b4cb5f10affd2ccfdf62065b5f70cbe6f6d362018572299cb307d365db20cd7d2e7c03b7
@@ -0,0 +1,32 @@
1
+ # OpenTelemetry Implementation Guidelines
2
+
3
+ This document provides expert guidance for aligning code and logs with OpenTelemetry (OTel) standards. Follow these rules for all new instrumentation.
4
+
5
+ ## 1. Log Data Model
6
+ - **Body:** Use `body` for the primary log message.
7
+ - **Attributes:** Metadata must be flat Key-Value pairs.
8
+ - **Severity:** `level` maps to `severity_text`.
9
+
10
+ ## 2. Semantic Conventions
11
+ When adding attributes, check the official OTel [Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/).
12
+
13
+ ### HTTP Attributes
14
+ - `http.request.method`: GET, POST, etc.
15
+ - `http.response.status_code`: 200, 404, etc.
16
+ - `url.path`: The request path.
17
+ - `user_agent.original`: The raw User-Agent string.
18
+
19
+ ### Database Attributes
20
+ - `db.system`: postgresql, redis, etc.
21
+ - `db.operation`: select, update, etc.
22
+ - `db.collection.name`: table or collection name.
23
+
24
+ ## 3. Metrics & Units
25
+ - Always use the lowest common denominator for units (seconds, bytes).
26
+ - Use `_s` suffix for time duration.
27
+ - Avoid compound strings like `"10ms"`. Use `duration_s=0.01`.
28
+
29
+ ## 4. Distributed Tracing
30
+ - **TraceID:** 16-byte array, represented as a 32-char lowercase hex string.
31
+ - **SpanID:** 8-byte array, represented as a 16-char lowercase hex string.
32
+ - **Context Propagation:** Follow W3C Trace Context (traceparent) when possible, or maintain AWS X-Ray compatibility as per project requirements.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [0.5.5] - 2026-03-24
2
+
3
+ ### Added
4
+ - **Deep Security Filtering:** The `JsonFormatter` now recursively filters sensitive data within `Array` and nested `Hash` structures.
5
+ - **OTel Semantic Mapping:** Added formal mapping of ExisRay fields to OpenTelemetry Semantic Conventions in `MANIFEST.md`.
6
+ - **Test Hardening:** Expanded test suite to cover deep filtering and ensure correct numeric type-casting in logs.
7
+
8
+ ## [0.5.4] - 2026-03-24
9
+
10
+ ### Changed
11
+ - **Human-readable durations:** `duration_human` now uses a smarter format: sub-second values show as `"7.2ms"`, values under 60s show as `"1.25s"`, and longer durations use `ActiveSupport::Duration` prose (e.g., `"2 minutes 5 seconds"`).
12
+ - **Shared duration helper:** Extracted `ExisRay::Tracer.format_duration` to consolidate duration formatting used by both `LogSubscriber` and `TaskMonitor`.
13
+
1
14
  ## [0.5.3] - 2026-03-24
2
15
 
3
16
  ### Changed
data/CLAUDE.md CHANGED
@@ -32,6 +32,7 @@
32
32
  ## Execution Rules
33
33
  - **No Lograge:** Do not suggest or re-add the Lograge dependency.
34
34
  - **Pure Data Logging:** Internal logs use KV strings (`component=exis_ray event=...`).
35
+ - **OTel Compliance:** Always follow the guidelines in `.gemini/docs/TELEMETRY_GUIDELINES.md` and alignment in `MANIFEST.md`. Use OTel Semantic Conventions for new attributes.
35
36
  - **Resilience:** All logging operations must be wrapped in `rescue StandardError`.
36
37
  - **Automatic Fields:** NEVER manually log `time`, `level`, `service`, `source`, `root_id`, `correlation_id`, `sidekiq_job` or `task`. These are handled by the library.
37
38
  - **Source Values:** Valid values for `source` field: `http`, `sidekiq`, `task`, `system`.
data/MANIFEST.md CHANGED
@@ -26,10 +26,24 @@ Cada línea de log debe llevar los metadatos necesarios para identificar su orig
26
26
  * `source`: El punto de entrada de la ejecución (`http`, `sidekiq`, `task`, `system`).
27
27
 
28
28
  ### 3. Convenciones de Naming & Tipos
29
- - **Naming:** Las llaves (keys) deben usar siempre `snake_case`.
29
+ - **Naming:** Las llaves (keys) deben usar siempre `snake_case`. Para campos estándar, se prefiere seguir las **OpenTelemetry Semantic Conventions**.
30
30
  - **Valores Numéricos:** Deben emitirse como números reales (sin sufijos de texto) para permitir que el motor de logs realice el casting automático.
31
31
  - **Formato:** Pares `key=value` en una sola línea estructurada.
32
32
 
33
+ ## 🔭 Alineación con OpenTelemetry
34
+
35
+ Para garantizar la interoperabilidad, `exis_ray` sigue el **OpenTelemetry Log Data Model**. Siempre que sea posible, los campos deben mapearse a las convenciones semánticas oficiales:
36
+
37
+ | Campo ExisRay | OTel Semantic Convention | Descripción |
38
+ | :--- | :--- | :--- |
39
+ | `body` | `body` | El contenido principal del log. |
40
+ | `level` | `severity_text` | Nivel de importancia. |
41
+ | `duration_s` | `duration` (en segundos) | Tiempo de ejecución. |
42
+ | `method` | `http.request.method` | Método HTTP. |
43
+ | `status` | `http.response.status_code` | Código de respuesta. |
44
+ | `path` | `url.path` | Ruta del request. |
45
+ | `user_id` | `user.id` | Identificador del usuario. |
46
+
33
47
  ## 🏗 Infraestructura de Datos (Automática)
34
48
 
35
49
  Para evitar logs redundantes y pesados, **NUNCA** incluyas manualmente las siguientes llaves en tus mensajes de log. La capa de infraestructura (`exis_ray`) las inyecta automáticamente en el nivel raíz del JSON:
data/README.md CHANGED
@@ -286,6 +286,25 @@ If you don't need extra fields, skip this step — `ExisRay::LogSubscriber` is u
286
286
  * **`ExisRay::LogSubscriber`**: Replaces Lograge for HTTP request logging. Subscribes to `process_action.action_controller` and suppresses Rails' default multi-line log subscribers. Compatible with Rails 6, 7, and 8. Subclass it and override `self.extra_fields(event)` to inject custom fields.
287
287
  * **`ExisRay::TaskMonitor`**: The entry point for non-HTTP processes.
288
288
 
289
+ ## Known Behaviors
290
+
291
+ ### Third-party gem warnings in `body`
292
+
293
+ ExisRay captures all log output — including warnings emitted by third-party gems — and routes free-text lines to the `body` field. Some gems may include user-identifying data in their warnings.
294
+
295
+ **Example:** The [Bullet](https://github.com/flyerhzm/bullet) N+1 query detector emits warnings like:
296
+
297
+ ```
298
+ user: gabriel
299
+ GET /clients?page=1
300
+ USE eager loading detected
301
+ Client => [:gps_point]
302
+ ```
303
+
304
+ This text lands verbatim in `body`. ExisRay does not filter or redact `body` content, as it cannot know what third-party gems will include.
305
+
306
+ **Recommendation:** If you use Bullet or similar gems in production, configure them to use a separate notification channel (e.g., Slack, Honeybadger) instead of `Rails.logger`, to avoid leaking usernames or other PII into your structured logs.
307
+
289
308
  ## License
290
309
 
291
310
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -163,17 +163,34 @@ module ExisRay
163
163
  end
164
164
 
165
165
  # Filtra recursivamente un Hash que contenga claves sensibles.
166
+ # Maneja valores anidados de tipo Hash o Array.
166
167
  #
167
168
  # @param hash [Hash]
168
169
  # @return [Hash]
169
170
  def filter_sensitive_hash(hash)
170
171
  hash.each_with_object({}) do |(k, v), result|
171
- result[k] = if v.is_a?(Hash)
172
- filter_sensitive_hash(v)
173
- else
174
- cast_value(k, v)
172
+ result[k] = case v
173
+ when Hash then filter_sensitive_hash(v)
174
+ when Array then filter_sensitive_array(k, v)
175
+ else cast_value(k, v)
175
176
  end
176
177
  end
177
178
  end
179
+
180
+ # Filtra recursivamente los elementos de un Array, propagando la clave padre
181
+ # para que el filtrado de claves sensibles se aplique a hashes anidados.
182
+ #
183
+ # @param key [String, Symbol] Clave padre (para filtrado si el array no contiene hashes).
184
+ # @param array [Array]
185
+ # @return [Array]
186
+ def filter_sensitive_array(key, array)
187
+ array.map do |element|
188
+ case element
189
+ when Hash then filter_sensitive_hash(element)
190
+ when Array then filter_sensitive_array(key, element)
191
+ else cast_value(key, element)
192
+ end
193
+ end
194
+ end
178
195
  end
179
196
  end
@@ -84,7 +84,7 @@ module ExisRay
84
84
  action: payload[:action],
85
85
  status: status,
86
86
  duration_s: duration_s,
87
- duration_human: ActiveSupport::Duration.build(duration_s).inspect,
87
+ duration_human: ExisRay::Tracer.format_duration(duration_s),
88
88
  view_runtime_s: view_s,
89
89
  db_runtime_s: db_s
90
90
  }
@@ -35,12 +35,12 @@ module ExisRay
35
35
  execute_with_optional_tags { yield }
36
36
 
37
37
  duration_s = ExisRay::Tracer.current_duration_s
38
- human_time = ActiveSupport::Duration.build(duration_s).inspect
38
+ human_time = ExisRay::Tracer.format_duration(duration_s)
39
39
 
40
40
  log_event(:info, "component=exis_ray event=task_finished task=#{task_name} status=success duration_s=#{duration_s} duration_human=\"#{human_time}\"")
41
41
  rescue StandardError => e
42
42
  duration_s = ExisRay::Tracer.current_duration_s
43
- human_time = ActiveSupport::Duration.build(duration_s).inspect
43
+ human_time = ExisRay::Tracer.format_duration(duration_s)
44
44
 
45
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}")
46
46
  raise e
@@ -70,6 +70,23 @@ module ExisRay
70
70
  (Process.clock_gettime(Process::CLOCK_MONOTONIC) - created_at).round(4)
71
71
  end
72
72
 
73
+ # Formatea una duración en segundos a un string legible por humanos.
74
+ # - Menos de 1s → "7.2ms"
75
+ # - Entre 1-60s → "1.25s"
76
+ # - 60s o más → "2 minutes 5 seconds" (via ActiveSupport::Duration)
77
+ #
78
+ # @param seconds [Float] Duración en segundos.
79
+ # @return [String]
80
+ def self.format_duration(seconds)
81
+ if seconds < 1.0
82
+ "#{(seconds * 1000).round(1)}ms"
83
+ elsif seconds < 60.0
84
+ "#{seconds.round(3)}s"
85
+ else
86
+ ActiveSupport::Duration.build(seconds.round).inspect
87
+ end
88
+ end
89
+
73
90
  # Construye el header de trazabilidad para enviar al siguiente servicio.
74
91
  #
75
92
  # @return [String] Header formateado: "Root=...;Self=...;CalledFrom=...;TotalTimeSoFar=...ms"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.5.3"
5
+ VERSION = "0.5.5"
6
6
  end
@@ -92,11 +92,10 @@ RSpec.describe ExisRay::JsonFormatter do
92
92
  expect(result).to include(
93
93
  "component" => "orders",
94
94
  "event" => "invoice_generated",
95
- "duration_ms" => "42.5",
96
- "retries" => "0"
95
+ "duration_ms" => 42.5,
96
+ "retries" => 0
97
97
  )
98
98
  end
99
- end
100
99
 
101
100
  it "cae a body si el string parece kv pero no produce ningún par" do
102
101
  result = call("key=")
@@ -156,10 +155,10 @@ RSpec.describe ExisRay::JsonFormatter do
156
155
  end
157
156
 
158
157
  describe "#parse_kv_string (privado)" do
159
- it "retorna un hash con claves symbol" do
158
+ it "retorna un hash con claves symbol y valores casteados" do
160
159
  result = formatter.send(:parse_kv_string, "a=1 b=2")
161
160
 
162
- expect(result).to eq({ a: "1", b: "2" })
161
+ expect(result).to eq({ a: 1, b: 2 })
163
162
  end
164
163
 
165
164
  it "desenvuelve las comillas dobles de los valores" do
@@ -168,4 +167,44 @@ RSpec.describe ExisRay::JsonFormatter do
168
167
  expect(result).to eq({ msg: "hello world" })
169
168
  end
170
169
  end
170
+
171
+ describe "#filter_sensitive_hash (privado)" do
172
+ it "filtra claves sensibles en el nivel raíz" do
173
+ result = formatter.send(:filter_sensitive_hash, { user: "gabriel", password: "secret" })
174
+
175
+ expect(result[:user]).to eq("gabriel")
176
+ expect(result[:password]).to eq("[FILTERED]")
177
+ end
178
+
179
+ it "filtra claves sensibles en hashes anidados" do
180
+ result = formatter.send(:filter_sensitive_hash, {
181
+ db: { host: "localhost", token: "abc123" }
182
+ })
183
+
184
+ expect(result[:db][:host]).to eq("localhost")
185
+ expect(result[:db][:token]).to eq("[FILTERED]")
186
+ end
187
+
188
+ it "filtra claves sensibles dentro de arrays de hashes" do
189
+ result = formatter.send(:filter_sensitive_hash, {
190
+ users: [{ name: "gabriel", password: "secret" }, { name: "ana", password: "other" }]
191
+ })
192
+
193
+ expect(result[:users][0][:name]).to eq("gabriel")
194
+ expect(result[:users][0][:password]).to eq("[FILTERED]")
195
+ expect(result[:users][1][:password]).to eq("[FILTERED]")
196
+ end
197
+
198
+ it "filtra claves sensibles en arrays primitivos cuando la clave padre es sensible" do
199
+ result = formatter.send(:filter_sensitive_hash, { tokens: ["abc", "def"] })
200
+
201
+ expect(result[:tokens]).to eq(["[FILTERED]", "[FILTERED]"])
202
+ end
203
+
204
+ it "no altera valores no sensibles en arrays" do
205
+ result = formatter.send(:filter_sensitive_hash, { tags: ["admin", "active"] })
206
+
207
+ expect(result[:tags]).to eq(["admin", "active"])
208
+ end
209
+ end
171
210
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exis_ray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Edera
@@ -45,6 +45,7 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".gemini/docs/TELEMETRY_GUIDELINES.md"
48
49
  - CHANGELOG.md
49
50
  - CLAUDE.md
50
51
  - LICENSE.txt