exis_ray 0.5.2 → 0.5.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 +15 -0
- data/CLAUDE.md +1 -0
- data/MANIFEST.md +16 -14
- data/README.md +21 -2
- data/lib/exis_ray/json_formatter.rb +13 -7
- 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 +7 -7
- 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: 13877001eee2022a6c6de0d2709ef5080b0cc2e43459d5e81ea99d9f28f9b216
|
|
4
|
+
data.tar.gz: c6b551a316813a3671ae980a0fc8061b32fc1b585bc39b49a5c761a73c3af145
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e41a2bc99ab67896aac64e6eb8becca74fd03217807fa16e24ed434477109b053f9d9580b206a2c4f4123fefa6beca5f580b934e22c468175d20fa808179a7eb
|
|
7
|
+
data.tar.gz: 80bc6a49ea2b9ed2e4b45aa3f4e501752edb2d5873a8871cce6ba3b4e1bf4f925fb793a3ba5ab1a0f9c717d4ee63c9411bc1d738ab7d9a742ed48585071664fd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.5.4] - 2026-03-24
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
- **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"`).
|
|
5
|
+
- **Shared duration helper:** Extracted `ExisRay::Tracer.format_duration` to consolidate duration formatting used by both `LogSubscriber` and `TaskMonitor`.
|
|
6
|
+
|
|
7
|
+
## [0.5.3] - 2026-03-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **OpenTelemetry Alignment:** Free-text log lines now emit their content under the `body` key instead of `message`, following the OpenTelemetry Log Data Model specification.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Robust KV Parsing:** Added support for single-quoted values (`' '`) in the KV parser. This fixes cases where human-readable durations or other strings used single quotes.
|
|
14
|
+
- **Quote Sanitization:** Values extracted from the KV parser now have surrounding quotes (single or double) removed before JSON emission.
|
|
15
|
+
|
|
1
16
|
## [0.5.2] - 2026-03-24
|
|
2
17
|
|
|
3
18
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -33,4 +33,5 @@
|
|
|
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
35
|
- **Resilience:** All logging operations must be wrapped in `rescue StandardError`.
|
|
36
|
+
- **Automatic Fields:** NEVER manually log `time`, `level`, `service`, `source`, `root_id`, `correlation_id`, `sidekiq_job` or `task`. These are handled by the library.
|
|
36
37
|
- **Source Values:** Valid values for `source` field: `http`, `sidekiq`, `task`, `system`.
|
data/MANIFEST.md
CHANGED
|
@@ -30,26 +30,28 @@ Cada línea de log debe llevar los metadatos necesarios para identificar su orig
|
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
## Infraestructura de Datos (Automática)
|
|
33
|
+
## 🏗 Infraestructura de Datos (Automática)
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
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:
|
|
38
36
|
|
|
39
|
-
| Llave | Descripción |
|
|
37
|
+
| Llave | Descripción | Por qué no incluirla |
|
|
40
38
|
| :--- | :--- | :--- |
|
|
41
|
-
| `time` |
|
|
42
|
-
| `level` |
|
|
43
|
-
| `service` | Nombre de la
|
|
44
|
-
| `source` | Entrypoint
|
|
45
|
-
| `root_id` |
|
|
46
|
-
| `correlation_id
|
|
47
|
-
| `user_id` / `isp_id
|
|
48
|
-
| `sidekiq_job` |
|
|
49
|
-
| `task` | Nombre de la tarea
|
|
39
|
+
| `time` | Timestamp ISO8601 | Lo añade el Logger base. |
|
|
40
|
+
| `level` | INFO, ERROR, etc. | Lo añade el Logger base. |
|
|
41
|
+
| `service` | Nombre de la App | Se obtiene de la configuración global. |
|
|
42
|
+
| `source` | Entrypoint (http, task) | Lo inyecta el middleware/monitor correspondiente. |
|
|
43
|
+
| `root_id` | Trace ID (AWS X-Ray) | Se gestiona a nivel de hilo/petición. |
|
|
44
|
+
| `correlation_id`| ID de rastreo cruzado | Se genera automáticamente al inicio de la ejecución. |
|
|
45
|
+
| `user_id` / `isp_id`| Contexto de negocio | Se extrae del estado global de la petición. |
|
|
46
|
+
| `sidekiq_job` | Clase del Worker | Inyectado automáticamente en procesos Sidekiq. |
|
|
47
|
+
| `task` | Nombre de la tarea | Inyectado automáticamente por el TaskMonitor. |
|
|
48
|
+
|
|
49
|
+
> **Regla de Oro:** Tu log manual solo debe contener datos de **tu lógica de negocio**. La infraestructura ya sabe quién eres, de dónde vienes y cuál es tu ID de traza.
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
+
## 🛰 Ciclo de Vida del Evento
|
|
54
|
+
|
|
53
55
|
## Ciclo de Vida del Evento
|
|
54
56
|
|
|
55
57
|
### Procesos, Trabajos y Tareas (Jobs/Tasks)
|
data/README.md
CHANGED
|
@@ -148,14 +148,14 @@ end
|
|
|
148
148
|
|
|
149
149
|
When `config.log_format = :json` is enabled, ExisRay transforms all your application outputs into single-line, context-rich JSON objects.
|
|
150
150
|
|
|
151
|
-
**HTTP Requests
|
|
151
|
+
**HTTP Requests:**
|
|
152
152
|
```json
|
|
153
153
|
{"time":"2026-03-12T14:30:00Z","level":"INFO","service":"App-HTTP","root_id":"Root=1-65a...bc","trace_id":"Root=1-65a...bc;Self=...","request_id":"9876-abcd-...","user_id":42,"isp_id":10,"method":"GET","path":"/api/v1/users","format":"html","controller":"UsersController","action":"index","status":200,"duration":45.2,"view":20.1,"db":15.0}
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
**Sidekiq Jobs & Rake Tasks:**
|
|
157
157
|
```json
|
|
158
|
-
{"time":"2026-03-12T14:31:00Z","level":"INFO","service":"Sidekiq-HardWorker","root_id":"Root=1-65a...bc","user_id":42,"
|
|
158
|
+
{"time":"2026-03-12T14:31:00Z","level":"INFO","service":"Sidekiq-HardWorker","root_id":"Root=1-65a...bc","user_id":42,"body":"[ExisRay] Processing payment..."}
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
### B. Automatic Sidekiq Integration
|
|
@@ -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).
|
|
@@ -22,8 +22,8 @@ module ExisRay
|
|
|
22
22
|
# Detecta si un string comienza con al menos un par key=value.
|
|
23
23
|
KV_DETECT_RE = /\A\w+=/
|
|
24
24
|
|
|
25
|
-
# Extrae pares key=value de un string. Soporta valores sin espacios o entre comillas dobles.
|
|
26
|
-
KV_PARSE_RE = /(\w+)=("(?:[^"\\]|\\.)*"|\S+)/
|
|
25
|
+
# Extrae pares key=value de un string. Soporta valores sin espacios o entre comillas dobles/simples.
|
|
26
|
+
KV_PARSE_RE = /(\w+)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)/
|
|
27
27
|
|
|
28
28
|
# Claves sensibles que deben filtrarse automáticamente según el estándar de Gabriel.
|
|
29
29
|
SENSITIVE_KEYS = /password|pass|passwd|secret|token|api_key|auth/i
|
|
@@ -99,7 +99,7 @@ module ExisRay
|
|
|
99
99
|
# se hace un merge directo. Si es un String con formato key=value (ej: "event=foo bar=baz"),
|
|
100
100
|
# se parsea y los campos se elevan al nivel raíz del JSON. Valores con espacios deben estar
|
|
101
101
|
# entre comillas (ej: message="algo salió mal"). Si el String no sigue ese formato, se asigna
|
|
102
|
-
# a la clave `:
|
|
102
|
+
# a la clave `:body` (OpenTelemetry log body).
|
|
103
103
|
#
|
|
104
104
|
# @param payload [Hash] El diccionario base del log.
|
|
105
105
|
# @param msg [String, Hash, Object] El mensaje original recibido por el logger.
|
|
@@ -110,9 +110,9 @@ module ExisRay
|
|
|
110
110
|
payload.merge!(filter_sensitive_hash(msg))
|
|
111
111
|
elsif msg.is_a?(String) && kv_string?(msg)
|
|
112
112
|
parsed = parse_kv_string(msg)
|
|
113
|
-
parsed.empty? ? payload[:
|
|
113
|
+
parsed.empty? ? payload[:body] = msg : payload.merge!(parsed)
|
|
114
114
|
else
|
|
115
|
-
payload[:
|
|
115
|
+
payload[:body] = msg.to_s
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
@@ -126,7 +126,7 @@ module ExisRay
|
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
# Parsea un string con formato key=value y retorna un Hash.
|
|
129
|
-
# Soporta valores con espacios si están entre comillas dobles
|
|
129
|
+
# Soporta valores con espacios si están entre comillas dobles o simples.
|
|
130
130
|
# Intenta convertir valores numéricos a Float o Integer automáticamente.
|
|
131
131
|
#
|
|
132
132
|
# @param str [String]
|
|
@@ -134,7 +134,13 @@ module ExisRay
|
|
|
134
134
|
def parse_kv_string(str)
|
|
135
135
|
result = {}
|
|
136
136
|
str.scan(KV_PARSE_RE) do |key, value|
|
|
137
|
-
|
|
137
|
+
# Eliminamos comillas envolventes si existen (dobles o simples)
|
|
138
|
+
val = if value.start_with?('"', "'")
|
|
139
|
+
value[1..-2].to_s.gsub("\\#{value[0]}", value[0])
|
|
140
|
+
else
|
|
141
|
+
value
|
|
142
|
+
end
|
|
143
|
+
|
|
138
144
|
result[key.to_sym] = cast_value(key, val)
|
|
139
145
|
end
|
|
140
146
|
result
|
|
@@ -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
|
@@ -98,32 +98,32 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
it "cae a
|
|
101
|
+
it "cae a body si el string parece kv pero no produce ningún par" do
|
|
102
102
|
result = call("key=")
|
|
103
103
|
|
|
104
|
-
expect(result["
|
|
104
|
+
expect(result["body"]).to eq("key=")
|
|
105
105
|
expect(result).not_to have_key("key")
|
|
106
106
|
end
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
context "cuando el mensaje es un String libre (sin formato key=value)" do
|
|
110
|
-
it "asigna el string completo al campo
|
|
110
|
+
it "asigna el string completo al campo body" do
|
|
111
111
|
result = call("algo salió mal")
|
|
112
112
|
|
|
113
|
-
expect(result["
|
|
113
|
+
expect(result["body"]).to eq("algo salió mal")
|
|
114
114
|
expect(result).to include("level" => "INFO", "service" => "test-service")
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
it "asigna un string vacío al campo
|
|
117
|
+
it "asigna un string vacío al campo body" do
|
|
118
118
|
result = call("")
|
|
119
119
|
|
|
120
|
-
expect(result["
|
|
120
|
+
expect(result["body"]).to eq("")
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
it "convierte objetos arbitrarios a string via to_s" do
|
|
124
124
|
result = call(42)
|
|
125
125
|
|
|
126
|
-
expect(result["
|
|
126
|
+
expect(result["body"]).to eq("42")
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|