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 +4 -4
- data/.gemini/docs/TELEMETRY_GUIDELINES.md +32 -0
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +1 -0
- data/MANIFEST.md +15 -1
- data/README.md +19 -0
- data/lib/exis_ray/json_formatter.rb +21 -4
- data/lib/exis_ray/log_subscriber.rb +1 -1
- data/lib/exis_ray/task_monitor.rb +2 -2
- data/lib/exis_ray/tracer.rb +17 -0
- data/lib/exis_ray/version.rb +1 -1
- data/spec/exis_ray/json_formatter_spec.rb +44 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a79e0a93c6efad500f943dad46daa5d429f133d8144db2fd1ce1e32e2b07ac68
|
|
4
|
+
data.tar.gz: 243915edbd6405a89656457b1371b34971efa5f377def03093d4a024ca73b661
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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] =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
data/lib/exis_ray/tracer.rb
CHANGED
|
@@ -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"
|
data/lib/exis_ray/version.rb
CHANGED
|
@@ -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" =>
|
|
96
|
-
"retries" =>
|
|
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:
|
|
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.
|
|
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
|