exis_ray 0.9.0 → 0.11.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +1 -0
- data/README.md +34 -5
- data/lib/exis_ray/configuration.rb +21 -0
- data/lib/exis_ray/json_formatter.rb +17 -1
- data/lib/exis_ray/log_subscriber.rb +13 -7
- data/lib/exis_ray/task_monitor.rb +24 -7
- data/lib/exis_ray/version.rb +1 -1
- data/skill/SKILL.md +32 -8
- data/skills.yml +14 -28
- data/spec/exis_ray/configuration_spec.rb +22 -0
- data/spec/exis_ray/json_formatter_spec.rb +66 -0
- data/spec/exis_ray/log_subscriber_spec.rb +43 -0
- data/spec/exis_ray/task_monitor_spec.rb +31 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e474f1122d88cba11551996f1fe73b01da92f28dcea658e6eb473e51277e838e
|
|
4
|
+
data.tar.gz: 708af8ed6b2ece0cf5e78f74543135ceb4e83d8e2e51788e980847c7675d586e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e314e520ab390a510953c78c8774c816e30a9fd814e9bbd36eb2e874e5ed81abbaeb91a7f0abea350c46f4783f8f93c176209a08a15a4691de2c13551534135
|
|
7
|
+
data.tar.gz: 478cfe36c1b7f5e60c37de8c6db1e86c239a13a2ec2c8a3bd6797b455cc27dd0ed7f3960c02e29049e0979c648a7ab7857982d84664ae43647dfb2af5cb99f16
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.11.0] - 2026-05-26
|
|
2
|
+
|
|
3
|
+
### Nuevas funcionalidades
|
|
4
|
+
- **`config.emit_legacy_exception_keys`** (#15): nuevo flag de `Configuration` (default `true`) que controla la emisión de las keys legacy `error_class`/`error_message` junto a `exception.type`/`exception.message`. Bajado a `false`, `ExisRay::LogSubscriber` (logs HTTP) y `ExisRay::TaskMonitor` (logs de tasks) emiten solo las keys OTel v1.0 (`exception.*`), reduciendo bytes y ruido downstream cuando los consumers (dashboards, alertas, queries) ya migraron. Mantiene `true` como default durante la ventana de transición — se flipea a `false` en v0.12.0 y se remueve el flag en v1.0 cuando los legacy se eliminen definitivamente.
|
|
5
|
+
- **`url.path` (OTel v1.0) + `config.emit_legacy_path_key`** (#15): `ExisRay::LogSubscriber` empieza a emitir `url.path` (nombre canónico OTel) en todos los logs HTTP. El alias legacy `path` se sigue emitiendo durante la ventana de transición y queda gateado por `config.emit_legacy_path_key` (default `true`). Cuando los consumers (queries, dashboards, alertas) hayan migrado a `url.path`, bajar el flag a `false` para suprimir el alias. Se flipea a `false` en v0.12.0 y se remueve el flag en v1.0.
|
|
6
|
+
- **Doc: `url.path` vs `http_route` no se deduplican** (#15): el caso reportado de "valores idénticos en endpoints estáticos" no es un bug — `url.path` es la URL **concreta** (alta cardinalidad), `http_route` es el **template** (baja cardinalidad). Cumplen roles distintos en dashboards/queries downstream; siguen emitiéndose siempre los dos. Documentado en `README.md` y `skill/SKILL.md` (subsección "Migración OTel — `path` → `url.path`" + nota explicativa de la coexistencia semántica).
|
|
7
|
+
|
|
8
|
+
## [0.10.0] - 2026-05-25
|
|
9
|
+
|
|
10
|
+
### Documentación
|
|
11
|
+
- **`SKILL.md`/`README` describían la auto-inyección como incondicional** (#11): la prosa de "Flujo runtime" en `SKILL.md` y "Flujo de propagación" en `README.md` listaba los campos auto-inyectados (`root_id`, `trace_id`, `source`, `request_id`, ...) sin desglosar los guards. `JsonFormatter#inject_tracer_context` corta su bloque con `return unless Tracer.root_id`, y `request_id` se emite **fuera** de ese guard (distinto ciclo de vida — fix de #9). Sin esa precisión, un implementador en `box_radius_manager` concluyó (apoyándose en la doc) que "`root_id` ya cubre la trazabilidad, `request_id` es redundante" y removió un workaround necesario; en su entrypoint sin trace header `root_id` era nil → logs sin `request_id`/`source`/`root_id`. Agregada subsección "Condiciones de emisión por campo" en `SKILL.md` con tabla "campo → condición → entrypoint que la garantiza", callout en el README arriba de "Campos auto-inyectados", y enlace cruzado a `docs/behavior/behavior.md` + `docs/glossary/glossary.md` (que ya tenían el detalle).
|
|
12
|
+
|
|
13
|
+
### Correcciones
|
|
14
|
+
- **Clave `task` duplicada en JSON cuando `TaskMonitor.run` loguea lifecycle** (#12): `TaskMonitor` emitía `task=#{task_name}` en los KV strings de `task_started`/`task_finished` (3 sites: `task_monitor.rb:32,41,48`); `JsonFormatter#inject_tracer_context` ya inyecta la misma clave desde `Tracer.task` con Symbol — al colapsar Symbol/String en `JSON.generate`, `json` emitía `warning: detected duplicate key "task"` y la línea entera va a romper en `json` 3.0. **Fix B** (origen): los KV de lifecycle ya no emiten `task=...`; la clave viene exclusivamente del formatter vía `Tracer.task`. **Fix A** (defensa en profundidad): `JsonFormatter` normaliza todas las claves del payload a String antes de `JSON.generate`, deduplicando con precedencia "última inserción gana" (developer payload pisa contexto canónico inyectado por Tracer, igual que `log_fields`). Cubre futuros casos análogos Tracer ↔ developer KV sobre la misma clave.
|
|
15
|
+
|
|
1
16
|
## [0.9.0] - 2026-05-19
|
|
2
17
|
|
|
3
18
|
### Correcciones
|
data/CLAUDE.md
CHANGED
|
@@ -178,6 +178,7 @@ ExisRay.configure do |config|
|
|
|
178
178
|
config.propagation_trace_header = 'X-Amzn-Trace-Id'
|
|
179
179
|
config.current_class = 'Current'
|
|
180
180
|
config.reporter_class = 'Reporter'
|
|
181
|
+
config.emit_legacy_exception_keys = true # default true; pasar a false cuando consumers usen exception.*
|
|
181
182
|
end
|
|
182
183
|
|
|
183
184
|
# BugBunny publisher — debe agregarse manualmente al cliente
|
data/README.md
CHANGED
|
@@ -52,7 +52,7 @@ ExisRay opera en tres capas que se combinan automáticamente:
|
|
|
52
52
|
**Flujo de propagación:**
|
|
53
53
|
|
|
54
54
|
1. Un request/job/mensaje llega al servicio. El middleware correspondiente hidrata el `Tracer` con el header entrante. **Todo entrypoint garantiza un `root_id`**: si no llega trace header (servicio que es punto de entrada, no eslabón intermedio), genera uno fresco. En HTTP además captura el `request_id` de Rails.
|
|
55
|
-
2. `JsonFormatter` inyecta
|
|
55
|
+
2. `JsonFormatter` inyecta `root_id`, `trace_id`, `source`, `request_id` y el contexto de negocio (`user_id`, `isp_id`, `correlation_id`) en cada línea de log. **Cada campo tiene un guard:** `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` salen solo si `Tracer.root_id` está presente; `request_id` se emite fuera de ese guard (distinto ciclo de vida). Como todo entrypoint asegura `root_id` (genera uno fresco si no llega header), `source` (mandatorio) nunca falta. Detalle por campo: tabla [Campos auto-inyectados](#campos-auto-inyectados) más abajo.
|
|
56
56
|
3. Cuando el servicio llama a otro servicio (HTTP, Sidekiq, RabbitMQ), el middleware de salida genera un nuevo header con `Tracer.generate_trace_header`, que incluye el `root_id` original, el `self_id` del servicio actual, el `CalledFrom` y el tiempo acumulado.
|
|
57
57
|
4. El servicio destino repite desde el paso 1. El `root_id` se mantiene constante a lo largo de toda la cadena.
|
|
58
58
|
|
|
@@ -339,7 +339,9 @@ config.logger.formatter = ExisRay::JsonFormatter
|
|
|
339
339
|
|
|
340
340
|
### Campos auto-inyectados
|
|
341
341
|
|
|
342
|
-
`JsonFormatter` inyecta estos campos automáticamente en cada línea. **Nunca** los incluyas manualmente en tus logs
|
|
342
|
+
`JsonFormatter` inyecta estos campos automáticamente en cada línea. **Nunca** los incluyas manualmente en tus logs.
|
|
343
|
+
|
|
344
|
+
> **No es incondicional.** `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` están gateados por `inject_tracer_context`'s `return unless Tracer.root_id`. Los 4 entrypoints (HTTP, Sidekiq server, BugBunny consumer, TaskMonitor) garantizan el `root_id` (fresco si no llega header), por eso `source` (mandatorio) nunca falta — pero si el código loguea fuera de un entrypoint (boot, código inicializador, hilos sueltos), esos campos pueden faltar. `request_id` se emite fuera de ese guard y tiene distinto ciclo de vida.
|
|
343
345
|
|
|
344
346
|
| Campo | Condición |
|
|
345
347
|
|:------|:----------|
|
|
@@ -368,8 +370,9 @@ config.logger.formatter = ExisRay::JsonFormatter
|
|
|
368
370
|
| `component` | String | Siempre `"exis_ray"` |
|
|
369
371
|
| `event` | String | Siempre `"http_request"` |
|
|
370
372
|
| `method` | String | Verbo HTTP |
|
|
371
|
-
| `path` | String | URL concreta del request |
|
|
372
|
-
| `
|
|
373
|
+
| `url.path` | String | URL concreta del request (OTel v1.0). Siempre presente |
|
|
374
|
+
| `path` | String | Alias legacy de `url.path`. Solo si `config.emit_legacy_path_key` (default `true`, deprecado) |
|
|
375
|
+
| `http_route` | String | Template (ej: `/users/:id`). Baja cardinalidad para dashboards. En endpoints sin params coincide en valor con `url.path` — la dupe es semánticamente esperada (concrete vs template) |
|
|
373
376
|
| `format` | Symbol/String | `html`, `json`, etc. |
|
|
374
377
|
| `controller` | String | Class name del controller |
|
|
375
378
|
| `action` | String | Nombre del action |
|
|
@@ -380,11 +383,37 @@ config.logger.formatter = ExisRay::JsonFormatter
|
|
|
380
383
|
| `db_runtime_s` | Float\|nil | Solo si ActiveRecord lo reporta |
|
|
381
384
|
| `user_agent_original` | String | Header `User-Agent` |
|
|
382
385
|
| `server_address` | String | Hostname sin puerto del header `Host` |
|
|
383
|
-
| `error_class`, `error_message` | String | Solo en fallo (
|
|
386
|
+
| `error_class`, `error_message` | String | Solo en fallo y si `config.emit_legacy_exception_keys` (default `true`, deprecadas) |
|
|
384
387
|
| `exception.type`, `exception.message`, `exception.stacktrace` | String | Solo en fallo (OTel; stack limitado a 20 líneas) |
|
|
385
388
|
|
|
386
389
|
Severity del log: `ERROR` si `http_status >= 500`, sino `INFO`.
|
|
387
390
|
|
|
391
|
+
### Migración OTel — `error_class`/`error_message` → `exception.*`
|
|
392
|
+
|
|
393
|
+
Durante la ventana de transición OTel v1.0, `LogSubscriber` y `TaskMonitor` emiten ambos sets de keys en cada log de error. Cuando todos los consumers (dashboards, queries, alertas) hayan migrado a `exception.type`/`exception.message`/`exception.stacktrace`, desactivar los legacy:
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
ExisRay.configure do |c|
|
|
397
|
+
c.emit_legacy_exception_keys = false # default: true
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Roadmap: default `false` en v0.12.0; flag y legacy keys removidos en v1.0.
|
|
402
|
+
|
|
403
|
+
### Migración OTel — `path` → `url.path`
|
|
404
|
+
|
|
405
|
+
`LogSubscriber` emite `url.path` (nombre OTel v1.0) en todos los logs HTTP. Durante la ventana de transición emite también `path` (alias legacy Wispro). Cuando las queries/dashboards/alertas migren a `url.path`, desactivar el legacy:
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
ExisRay.configure do |c|
|
|
409
|
+
c.emit_legacy_path_key = false # default: true
|
|
410
|
+
end
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Roadmap: default `false` en v0.12.0; flag y key `path` removidos en v1.0.
|
|
414
|
+
|
|
415
|
+
> **Sobre `url.path` vs `http_route`:** son semánticamente distintos — `url.path` es la URL **concreta** (alta cardinalidad, ej. `/users/42`), `http_route` es el **template** matcheado (baja cardinalidad, ej. `/users/:id`). En endpoints sin params coinciden en valor (ej. `/up`), pero ambos se siguen emitiendo porque cumplen roles distintos en dashboards/queries downstream. La dupe en valor es esperada, no es un bug.
|
|
416
|
+
|
|
388
417
|
### Filtrado de claves sensibles
|
|
389
418
|
|
|
390
419
|
Las claves que matcheen `/password|pass|passwd|secret|token|api_key|auth/i` se reemplazan automáticamente por `[FILTERED]`, tanto en strings KV como en Hashes (incluyendo anidados).
|
|
@@ -52,6 +52,25 @@ module ExisRay
|
|
|
52
52
|
# @example 'production'
|
|
53
53
|
attr_accessor :deployment_environment
|
|
54
54
|
|
|
55
|
+
# @!attribute [rw] emit_legacy_exception_keys
|
|
56
|
+
# @return [Boolean] Si true, emite `error_class`/`error_message` junto a `exception.type`/`exception.message`.
|
|
57
|
+
# Default `true` durante la ventana de transición OTel v1.0. Setear a `false` cuando todos los
|
|
58
|
+
# consumers (dashboards, alertas, queries) hayan migrado a `exception.*` para reducir bytes
|
|
59
|
+
# y ruido en logs.
|
|
60
|
+
# Aplica a `ExisRay::LogSubscriber` (logs HTTP) y `ExisRay::TaskMonitor` (logs de tasks).
|
|
61
|
+
# @example false
|
|
62
|
+
attr_accessor :emit_legacy_exception_keys
|
|
63
|
+
|
|
64
|
+
# @!attribute [rw] emit_legacy_path_key
|
|
65
|
+
# @return [Boolean] Si true, `ExisRay::LogSubscriber` emite `path` (nombre legacy Wispro)
|
|
66
|
+
# junto a `url.path` (OTel v1.0). Default `true` durante la ventana de transición.
|
|
67
|
+
# Setear a `false` cuando los consumers (queries, dashboards, alertas) hayan migrado a
|
|
68
|
+
# `url.path` para reducir bytes y ruido en logs.
|
|
69
|
+
# Nota: `url.path` y `http_route` son semánticamente distintos (URL concreta vs template);
|
|
70
|
+
# coinciden como string solo en endpoints sin params — la dupe es esperada y no se elimina.
|
|
71
|
+
# @example false
|
|
72
|
+
attr_accessor :emit_legacy_path_key
|
|
73
|
+
|
|
55
74
|
# Inicializa la configuración con valores por defecto compatibles con AWS X-Ray.
|
|
56
75
|
def initialize
|
|
57
76
|
@trace_header = "HTTP_X_AMZN_TRACE_ID"
|
|
@@ -62,6 +81,8 @@ module ExisRay
|
|
|
62
81
|
@log_subscriber_class = nil
|
|
63
82
|
@service_version = default_service_version
|
|
64
83
|
@deployment_environment = default_deployment_environment
|
|
84
|
+
@emit_legacy_exception_keys = true
|
|
85
|
+
@emit_legacy_path_key = true
|
|
65
86
|
end
|
|
66
87
|
|
|
67
88
|
# Lee la versión del servicio desde la configuración de Rails.
|
|
@@ -61,13 +61,29 @@ module ExisRay
|
|
|
61
61
|
inject_current_tags(payload)
|
|
62
62
|
process_message(payload, msg)
|
|
63
63
|
|
|
64
|
-
"#{JSON.generate(payload
|
|
64
|
+
"#{JSON.generate(normalize_keys(payload), { ascii_only: false })}\n"
|
|
65
65
|
rescue StandardError
|
|
66
66
|
fallback_message(severity, timestamp, msg)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
+
# Normaliza todas las claves del payload a strings y deduplica con precedencia
|
|
72
|
+
# "última inserción gana" (developer payload pisa contexto canónico inyectado
|
|
73
|
+
# antes, igual que `log_fields`). Previene el warning `duplicate key` que `json`
|
|
74
|
+
# emite cuando coexisten `:task` (Symbol, vía Tracer) y `"task"` (String, vía
|
|
75
|
+
# KV string developer) — `json` 3.0 lo va a transformar en error.
|
|
76
|
+
#
|
|
77
|
+
# @param payload [Hash]
|
|
78
|
+
# @return [Hash{String => Object}]
|
|
79
|
+
def normalize_keys(payload)
|
|
80
|
+
payload.each_with_object({}) do |(key, value), result|
|
|
81
|
+
next if value.nil?
|
|
82
|
+
|
|
83
|
+
result[key.to_s] = value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
71
87
|
# @return [String, nil]
|
|
72
88
|
def service_version
|
|
73
89
|
ExisRay.configuration.service_version
|
|
@@ -98,7 +98,7 @@ module ExisRay
|
|
|
98
98
|
component: "exis_ray",
|
|
99
99
|
event: "http_request",
|
|
100
100
|
method: payload[:method],
|
|
101
|
-
path: payload[:path],
|
|
101
|
+
"url.path": payload[:path],
|
|
102
102
|
http_route: extract_http_route(payload),
|
|
103
103
|
format: payload[:format],
|
|
104
104
|
controller: payload[:controller],
|
|
@@ -112,6 +112,8 @@ module ExisRay
|
|
|
112
112
|
server_address: extract_server_address(headers["HTTP_HOST"])
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
data[:path] = payload[:path] if ExisRay.configuration.emit_legacy_path_key
|
|
116
|
+
|
|
115
117
|
data.merge!(exception_data)
|
|
116
118
|
data.merge!(self.class.extra_fields(event))
|
|
117
119
|
data.compact
|
|
@@ -129,15 +131,16 @@ module ExisRay
|
|
|
129
131
|
end
|
|
130
132
|
|
|
131
133
|
# Extrae los datos de excepción para logs OTel.
|
|
132
|
-
#
|
|
133
|
-
# (`
|
|
134
|
-
#
|
|
134
|
+
# Siempre emite `exception.type`/`exception.message`/`exception.stacktrace` (OTel v1.0).
|
|
135
|
+
# Emite además los campos legacy (`error_class`/`error_message`) cuando
|
|
136
|
+
# `ExisRay.configuration.emit_legacy_exception_keys` es `true` (default durante la
|
|
137
|
+
# ventana de transición). Setear a `false` cuando los consumers hayan migrado.
|
|
135
138
|
#
|
|
136
139
|
# El stacktrace se toma de `payload[:exception_object]` (expuesto por Rails), no
|
|
137
140
|
# de `payload[:exception]` que es solo `[class_name, message]`.
|
|
138
141
|
#
|
|
139
142
|
# @param payload [Hash] Payload completo del notification de Rails.
|
|
140
|
-
# @return [Hash] Campos exception.* y error_class/error_message
|
|
143
|
+
# @return [Hash] Campos exception.* (y error_class/error_message si el flag está activo).
|
|
141
144
|
def extract_exception_data(payload)
|
|
142
145
|
exception_info = payload[:exception]
|
|
143
146
|
return {} unless exception_info
|
|
@@ -146,12 +149,15 @@ module ExisRay
|
|
|
146
149
|
exception_message = exception_info.last
|
|
147
150
|
|
|
148
151
|
data = {
|
|
149
|
-
error_class: exception_class,
|
|
150
|
-
error_message: exception_message,
|
|
151
152
|
"exception.type": exception_class,
|
|
152
153
|
"exception.message": exception_message
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
if ExisRay.configuration.emit_legacy_exception_keys
|
|
157
|
+
data[:error_class] = exception_class
|
|
158
|
+
data[:error_message] = exception_message
|
|
159
|
+
end
|
|
160
|
+
|
|
155
161
|
exception_object = payload[:exception_object]
|
|
156
162
|
if exception_object.respond_to?(:backtrace) && exception_object.backtrace
|
|
157
163
|
data[:"exception.stacktrace"] = exception_object.backtrace.take(20).join("\n")
|
|
@@ -29,7 +29,7 @@ module ExisRay
|
|
|
29
29
|
curr.correlation_id = ExisRay::Tracer.correlation_id
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
log_event(:info, "component=exis_ray event=task_started
|
|
32
|
+
log_event(:info, "component=exis_ray event=task_started outcome=started")
|
|
33
33
|
|
|
34
34
|
# Bloque de ejecución con o sin tags dependiendo de la configuración
|
|
35
35
|
execute_with_optional_tags(&block)
|
|
@@ -38,18 +38,16 @@ module ExisRay
|
|
|
38
38
|
human_time = ExisRay::Tracer.format_duration(duration_s)
|
|
39
39
|
|
|
40
40
|
log_event(:info,
|
|
41
|
-
"component=exis_ray event=task_finished
|
|
41
|
+
"component=exis_ray event=task_finished " \
|
|
42
42
|
"outcome=success duration_s=#{duration_s} duration_human=\"#{human_time}\"")
|
|
43
43
|
rescue StandardError => e
|
|
44
44
|
duration_s = ExisRay::Tracer.current_duration_s
|
|
45
45
|
human_time = ExisRay::Tracer.format_duration(duration_s)
|
|
46
46
|
|
|
47
47
|
log_event(:error,
|
|
48
|
-
"component=exis_ray event=task_finished
|
|
48
|
+
"component=exis_ray event=task_finished " \
|
|
49
49
|
"outcome=failed duration_s=#{duration_s} duration_human=\"#{human_time}\" " \
|
|
50
|
-
"
|
|
51
|
-
"exception.type=#{e.class} exception.message=#{e.message.inspect} " \
|
|
52
|
-
"exception.stacktrace=#{format_stacktrace(e.backtrace)}")
|
|
50
|
+
"#{format_exception_fields(e)}")
|
|
53
51
|
raise e
|
|
54
52
|
ensure
|
|
55
53
|
# Limpieza centralizada obligatoria para evitar filtraciones de memoria o contexto
|
|
@@ -124,7 +122,26 @@ module ExisRay
|
|
|
124
122
|
'""'
|
|
125
123
|
end
|
|
126
124
|
|
|
125
|
+
# Construye los campos KV de excepción para el log de `task_finished` failed.
|
|
126
|
+
# Siempre incluye `exception.type` / `exception.message` / `exception.stacktrace` (OTel v1.0).
|
|
127
|
+
# Incluye además `error_class` / `error_message` cuando
|
|
128
|
+
# `ExisRay.configuration.emit_legacy_exception_keys` es `true` (default durante la ventana
|
|
129
|
+
# de transición).
|
|
130
|
+
#
|
|
131
|
+
# @param error [Exception]
|
|
132
|
+
# @return [String]
|
|
133
|
+
def self.format_exception_fields(error)
|
|
134
|
+
message_quoted = error.message.inspect
|
|
135
|
+
stack = format_stacktrace(error.backtrace)
|
|
136
|
+
|
|
137
|
+
parts = []
|
|
138
|
+
parts << "error_class=#{error.class} error_message=#{message_quoted}" if ExisRay.configuration.emit_legacy_exception_keys
|
|
139
|
+
parts << "exception.type=#{error.class} exception.message=#{message_quoted} " \
|
|
140
|
+
"exception.stacktrace=#{stack}"
|
|
141
|
+
parts.join(" ")
|
|
142
|
+
end
|
|
143
|
+
|
|
127
144
|
private_class_method :pod_identifier, :setup_tracer, :execute_with_optional_tags,
|
|
128
|
-
:log_event, :format_stacktrace
|
|
145
|
+
:log_event, :format_stacktrace, :format_exception_fields
|
|
129
146
|
end
|
|
130
147
|
end
|
data/lib/exis_ray/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ Para el complemento del estándar de logging Wispro (regla Data First, mapeo Ope
|
|
|
11
11
|
|
|
12
12
|
### Artefactos de detalle (RFC-008)
|
|
13
13
|
|
|
14
|
-
Este SKILL.md **resume e indexa**; el contrato y el significado de detalle viven en `docs/<capa>/`. **Version-lock por construcción:** `gemspec.files` empaqueta `docs/**` en el mismo tag que este `SKILL.md`; los links son rutas relativas dentro del paquete del release (nunca rama/`HEAD`/URL flotante). Contrato resumido anclado a **v0.
|
|
14
|
+
Este SKILL.md **resume e indexa**; el contrato y el significado de detalle viven en `docs/<capa>/`. **Version-lock por construcción:** `gemspec.files` empaqueta `docs/**` en el mismo tag que este `SKILL.md`; los links son rutas relativas dentro del paquete del release (nunca rama/`HEAD`/URL flotante). Contrato resumido anclado a **v0.10.0** (post fix issues #9, #11, #12):
|
|
15
15
|
|
|
16
16
|
- [`docs/behavior/behavior.md`](../docs/behavior/behavior.md) — secuencias de hidratación de trace por entrypoint + emisión en logs (parcial, incremental).
|
|
17
17
|
- [`docs/glossary/glossary.md`](../docs/glossary/glossary.md) — lenguaje ubicuo del bounded context (`root_id`, `trace_id`, `source`, `request_id`, `entrypoint`, ...).
|
|
@@ -79,8 +79,8 @@ ExisRay unifica trazabilidad distribuida, logging estructurado JSON, contexto de
|
|
|
79
79
|
2. `Tracer.parse_trace_id` extrae `root_id`, `self_id`, `called_from`, `total_time_so_far`
|
|
80
80
|
3. `ExisRay.sync_correlation_id` asigna `Tracer.correlation_id` a `Current.correlation_id`
|
|
81
81
|
4. Controller ejecuta `before_action` para setear `Current.user_id`, `Current.isp_id`
|
|
82
|
-
5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta
|
|
83
|
-
6. `LogSubscriber` emite un unico Hash al finalizar el request con campos default (`component`, `event`, `method`, `path`, `http_route`, `format`, `controller`, `action`, `http_status`, `duration_s`, `duration_human`, `view_runtime_s`, `db_runtime_s`, `user_agent_original`, `server_address`, y en error `error_class`/`error_message
|
|
82
|
+
5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta el contexto de ejecucion en cada linea. **No es incondicional:** cada campo tiene un guard especifico (ver tabla "Condiciones de emision" mas abajo). En particular `inject_tracer_context` corta el bloque `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` con `return unless Tracer.root_id` — la invariante "todo entrypoint garantiza `root_id`" es lo que hace que `source` (mandatorio) nunca falte. `request_id` se emite **fuera** de ese guard (distinto ciclo de vida que `root_id`). El developer aporta `component` (modulo de negocio) y `event` (que paso); estos NO son auto-inyectados porque dependen del call site, no del contexto de ejecucion.
|
|
83
|
+
6. `LogSubscriber` emite un unico Hash al finalizar el request con campos default (`component`, `event`, `method`, `url.path` siempre + `path` si `config.emit_legacy_path_key`, `http_route`, `format`, `controller`, `action`, `http_status`, `duration_s`, `duration_human`, `view_runtime_s`, `db_runtime_s`, `user_agent_original`, `server_address`, y en error `exception.type`/`exception.message`/`exception.stacktrace` siempre + `error_class`/`error_message` si `config.emit_legacy_exception_keys`).
|
|
84
84
|
7. En llamadas salientes, `FaradayMiddleware`/`ActiveResourceInstrumentation` inyectan `propagation_trace_header` con `Tracer.generate_trace_header`
|
|
85
85
|
8. Al finalizar, `ActiveSupport::CurrentAttributes` hace reset automatico
|
|
86
86
|
|
|
@@ -100,6 +100,8 @@ ExisRay.configure do |config|
|
|
|
100
100
|
config.log_subscriber_class = "MyLogSubscriber" # String|nil, default nil
|
|
101
101
|
config.service_version = "1.2.3" # String|nil, default: Rails config.version o config.x.version
|
|
102
102
|
config.deployment_environment = "production" # String|nil, default: Rails.env
|
|
103
|
+
config.emit_legacy_exception_keys = true # Boolean, default true (ventana transicion OTel v1.0)
|
|
104
|
+
config.emit_legacy_path_key = true # Boolean, default true (ventana transicion `path` -> `url.path` OTel v1.0)
|
|
103
105
|
end
|
|
104
106
|
|
|
105
107
|
ExisRay.configuration.json_logs? # => true si log_format == :json
|
|
@@ -233,8 +235,9 @@ ExisRay::TaskMonitor.run("billing:generate_invoices") do
|
|
|
233
235
|
InvoiceService.process_all
|
|
234
236
|
end
|
|
235
237
|
# Genera root_id propio, loguea task_started/task_finished con outcome y duration_s.
|
|
236
|
-
# En caso de error emite:
|
|
237
|
-
#
|
|
238
|
+
# En caso de error emite: exception.type, exception.message, exception.stacktrace
|
|
239
|
+
# (OTel, limitado a 20 lineas) + error_class/error_message si
|
|
240
|
+
# config.emit_legacy_exception_keys es true (default durante ventana de transicion).
|
|
238
241
|
# Re-lanza excepciones despues de loguearlas.
|
|
239
242
|
# Hace reset de Tracer, Current y Reporter en ensure.
|
|
240
243
|
```
|
|
@@ -250,8 +253,9 @@ Reemplaza Lograge. Se suscribe a `process_action.action_controller` y emite un H
|
|
|
250
253
|
| `component` | String | Siempre `"exis_ray"` |
|
|
251
254
|
| `event` | String | Siempre `"http_request"` |
|
|
252
255
|
| `method` | String | Verbo HTTP |
|
|
253
|
-
| `path` | String | URL concreta del request |
|
|
254
|
-
| `
|
|
256
|
+
| `url.path` | String | URL concreta del request (OTel v1.0). Siempre presente |
|
|
257
|
+
| `path` | String | Alias legacy de `url.path`. Solo si `config.emit_legacy_path_key` (default `true`, deprecado) |
|
|
258
|
+
| `http_route` | String | Template (ej: `/users/:id`). Baja cardinalidad para dashboards. En endpoints sin params coincide en valor con `url.path` — dupe semanticamente esperada (concrete vs template) |
|
|
255
259
|
| `format` | Symbol/String | `html`, `json`, etc. |
|
|
256
260
|
| `controller` | String | Class name del controller |
|
|
257
261
|
| `action` | String | Nombre del action |
|
|
@@ -262,7 +266,7 @@ Reemplaza Lograge. Se suscribe a `process_action.action_controller` y emite un H
|
|
|
262
266
|
| `db_runtime_s` | Float\|nil | Solo si ActiveRecord lo reporta |
|
|
263
267
|
| `user_agent_original` | String | Header `User-Agent` |
|
|
264
268
|
| `server_address` | String | Hostname sin puerto (de `Host` header) |
|
|
265
|
-
| `error_class`, `error_message` | String | Solo en fallo (
|
|
269
|
+
| `error_class`, `error_message` | String | Solo en fallo y si `config.emit_legacy_exception_keys` (default `true`, deprecadas) |
|
|
266
270
|
| `exception.type`, `exception.message`, `exception.stacktrace` | String | Solo en fallo (OTel; stack limitado a 20 lineas) |
|
|
267
271
|
|
|
268
272
|
Para inyectar campos extra, sobreescribir `extra_fields`:
|
|
@@ -295,6 +299,26 @@ Casteo automatico: integers, floats, objetos JSON (`{...}`, `[...]`). Filtra cla
|
|
|
295
299
|
|
|
296
300
|
Por eso `component` y `event` jamas se auto-inyectan, aunque el estandar Wispro los exija.
|
|
297
301
|
|
|
302
|
+
#### Condiciones de emision por campo
|
|
303
|
+
|
|
304
|
+
La auto-inyeccion **no es incondicional**: cada campo tiene un guard. `inject_tracer_context` corta su bloque con `return unless Tracer.root_id`, asi que `root_id`/`trace_id`/`source`/`task`/`sidekiq_job` solo se emiten cuando hay trace context activo. Los 4 entrypoints (HTTP, Sidekiq server, BugBunny consumer, TaskMonitor) garantizan ese `root_id` (fresco si no llega header), por eso `source` (mandatorio) nunca falta en una linea originada por un entrypoint. `request_id` se emite **fuera** del guard de `root_id` — distinto ciclo de vida.
|
|
305
|
+
|
|
306
|
+
| Campo | Condicion de emision | Entrypoint que la garantiza |
|
|
307
|
+
|:------|:---------------------|:----------------------------|
|
|
308
|
+
| `time`, `level`, `severity_number`, `service`, `service_version`, `deployment_environment` | Siempre (no depende de Tracer/Current) | — |
|
|
309
|
+
| `request_id` | `Tracer.request_id` presente. **Fuera del guard de `root_id`** (issue #9 Gap C): distinto ciclo de vida (UUID v4 de Rails vs formato X-Ray). | HTTP (via `ActionDispatch::RequestId`). Otros entrypoints solo si la app lo setea explicitamente. |
|
|
310
|
+
| `root_id` | `Tracer.root_id` presente. **Gatea todo el bloque de tracer context.** | Los 4 entrypoints garantizan `root_id` fresco si no llega trace header (issue #9 Gap A). |
|
|
311
|
+
| `source` | `Tracer.source` presente **y** `root_id` presente (esta dentro del bloque gateado). | Idem `root_id`. Como `source` es mandatorio del estandar Wispro, la invariante "todo entrypoint garantiza `root_id`" es lo que evita que falte. |
|
|
312
|
+
| `trace_id` | `Tracer.trace_id` presente **y** `root_id` presente. Solo cuando el servicio es **eslabon intermedio** (recibio trace header upstream). | — (entrypoint que no recibe header tiene `root_id` fresco pero `trace_id` nil). |
|
|
313
|
+
| `sidekiq_job` | `Tracer.sidekiq_job` presente **y** `root_id` presente. | Sidekiq `ServerMiddleware`. |
|
|
314
|
+
| `task` | `Tracer.task` presente **y** `root_id` presente. | `TaskMonitor.run`. |
|
|
315
|
+
| `correlation_id` | `Current.correlation_id` presente. | `ExisRay.sync_correlation_id` (HTTP middleware lo llama; otros entrypoints solo si la app lo invoca). |
|
|
316
|
+
| `user_id`, `isp_id` | `Current.<attr>` no nil. | Lo setea la app (login, before_actions, etc.). |
|
|
317
|
+
| `Current.log_fields` (cualquier key) | La subclass de `Current` overrideo el hook y retorno un Hash no vacio. | — |
|
|
318
|
+
| `tags` | Rails tagged logging activo. **Antipatron con JSON** (rompe el formato) — ver FAQ. | — |
|
|
319
|
+
|
|
320
|
+
Para el detalle por entrypoint (que setea que y cuando), ver [`docs/behavior/behavior.md`](../docs/behavior/behavior.md). Para el significado de cada campo, [`docs/glossary/glossary.md`](../docs/glossary/glossary.md).
|
|
321
|
+
|
|
298
322
|
#### Ejemplos: KV vs Hash producen output equivalente
|
|
299
323
|
|
|
300
324
|
```ruby
|
data/skills.yml
CHANGED
|
@@ -2,48 +2,34 @@ mcps:
|
|
|
2
2
|
- github
|
|
3
3
|
- clickup
|
|
4
4
|
skills:
|
|
5
|
-
skill-manager:
|
|
6
|
-
repo: sequre/ai_knowledge
|
|
7
|
-
scope: local
|
|
8
5
|
yard:
|
|
9
6
|
repo: sequre/ai_knowledge
|
|
10
|
-
scope: local
|
|
11
7
|
quality-code:
|
|
12
8
|
repo: sequre/ai_knowledge
|
|
13
|
-
scope: local
|
|
14
9
|
gem-release:
|
|
15
10
|
repo: sequre/ai_knowledge
|
|
16
|
-
|
|
17
|
-
skill-builder:
|
|
11
|
+
dev-structure:
|
|
18
12
|
repo: sequre/ai_knowledge
|
|
19
|
-
|
|
20
|
-
ai-reports:
|
|
13
|
+
dev-compose:
|
|
21
14
|
repo: sequre/ai_knowledge
|
|
22
|
-
|
|
23
|
-
environment:
|
|
24
|
-
space_id: "${AI_REPORTS_SPACE_ID}"
|
|
25
|
-
bug_reports_list_id: "${AI_REPORTS_BUG_REPORTS_LIST_ID}"
|
|
26
|
-
improvements_list_id: "${AI_REPORTS_IMPROVEMENTS_LIST_ID}"
|
|
27
|
-
agent-review:
|
|
15
|
+
dev-enrich:
|
|
28
16
|
repo: sequre/ai_knowledge
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
skill-feedback:
|
|
18
|
+
repo: sequre/ai_knowledge
|
|
19
|
+
agent-issue:
|
|
20
|
+
repo: sequre/ai_knowledge
|
|
21
|
+
dev-flow:
|
|
22
|
+
repo: sequre/ai_knowledge
|
|
23
|
+
matrix-element:
|
|
34
24
|
repo: sequre/ai_knowledge
|
|
35
25
|
environment:
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
homeserver: "https://matrix.cloud.wispro.co"
|
|
27
|
+
auth_token: "${MATRIX_AUTH_TOKEN}"
|
|
28
|
+
rooms:
|
|
29
|
+
agents: "!VCHwQXgmXdyhhhPhoz:matrix.cloud.wispro.co"
|
|
38
30
|
documentation-writer:
|
|
39
31
|
repo: github/awesome-copilot
|
|
40
32
|
path: skills/documentation-writer
|
|
41
|
-
scope: local
|
|
42
|
-
find-skills:
|
|
43
|
-
repo: vercel-labs/skills
|
|
44
|
-
path: skills/find-skills
|
|
45
|
-
scope: local
|
|
46
33
|
opentelemetry:
|
|
47
34
|
repo: bobmatnyc/claude-mpm-skills
|
|
48
35
|
path: universal/observability/opentelemetry
|
|
49
|
-
scope: local
|
|
@@ -37,6 +37,14 @@ RSpec.describe ExisRay::Configuration do
|
|
|
37
37
|
it "deployment_environment por defecto es nil fuera de Rails" do
|
|
38
38
|
expect(config.deployment_environment).to be_nil
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
it "emit_legacy_exception_keys por defecto es true (ventana de transición OTel)" do
|
|
42
|
+
expect(config.emit_legacy_exception_keys).to be true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "emit_legacy_path_key por defecto es true (ventana de transición OTel url.path)" do
|
|
46
|
+
expect(config.emit_legacy_path_key).to be true
|
|
47
|
+
end
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
describe "resource attributes OTel" do
|
|
@@ -158,5 +166,19 @@ RSpec.describe ExisRay::Configuration do
|
|
|
158
166
|
|
|
159
167
|
expect(config.log_subscriber_class).to eq("MyLogSubscriber")
|
|
160
168
|
end
|
|
169
|
+
|
|
170
|
+
it "permite cambiar emit_legacy_exception_keys" do
|
|
171
|
+
config = described_class.new
|
|
172
|
+
config.emit_legacy_exception_keys = false
|
|
173
|
+
|
|
174
|
+
expect(config.emit_legacy_exception_keys).to be false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "permite cambiar emit_legacy_path_key" do
|
|
178
|
+
config = described_class.new
|
|
179
|
+
config.emit_legacy_path_key = false
|
|
180
|
+
|
|
181
|
+
expect(config.emit_legacy_path_key).to be false
|
|
182
|
+
end
|
|
161
183
|
end
|
|
162
184
|
end
|
|
@@ -463,6 +463,72 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
463
463
|
end
|
|
464
464
|
end
|
|
465
465
|
|
|
466
|
+
describe "dedup de claves Symbol/String (issue #12)" do
|
|
467
|
+
def stub_tracer_with_task(task_value)
|
|
468
|
+
stub_const("ExisRay::Tracer", Module.new do
|
|
469
|
+
define_singleton_method(:service_name) { "test-service" }
|
|
470
|
+
define_singleton_method(:root_id) { "1-abc-def" }
|
|
471
|
+
define_singleton_method(:trace_id) { nil }
|
|
472
|
+
define_singleton_method(:request_id) { nil }
|
|
473
|
+
define_singleton_method(:source) { "task" }
|
|
474
|
+
define_singleton_method(:sidekiq_job) { nil }
|
|
475
|
+
define_singleton_method(:task) { task_value }
|
|
476
|
+
end)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
it "no emite warning de `duplicate key` cuando KV string y Tracer setean la misma key" do
|
|
480
|
+
stub_tracer_with_task("billing:invoices")
|
|
481
|
+
|
|
482
|
+
warning = nil
|
|
483
|
+
original_warn = Warning.method(:warn)
|
|
484
|
+
Warning.singleton_class.define_method(:warn) { |msg, **| warning = msg }
|
|
485
|
+
|
|
486
|
+
begin
|
|
487
|
+
call("event=task_started task=billing:invoices outcome=started")
|
|
488
|
+
ensure
|
|
489
|
+
Warning.singleton_class.define_method(:warn, original_warn)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
expect(warning).to be_nil
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
it "developer payload (KV string) pisa al contexto canónico inyectado por Tracer" do
|
|
496
|
+
stub_tracer_with_task("billing:invoices")
|
|
497
|
+
|
|
498
|
+
result = call("event=foo task=override outcome=success")
|
|
499
|
+
|
|
500
|
+
expect(result["task"]).to eq("override")
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
it "emite la key como String aunque internamente fuese Symbol" do
|
|
504
|
+
stub_tracer_with_task("billing:invoices")
|
|
505
|
+
|
|
506
|
+
json_str = formatter.call(severity, timestamp, progname, "event=task_started outcome=started")
|
|
507
|
+
|
|
508
|
+
# Si quedaran ambas (`:task` y `"task"`) el JSON tendría dos veces la key.
|
|
509
|
+
expect(json_str.scan(/"task"\s*:/).size).to eq(1)
|
|
510
|
+
expect(JSON.parse(json_str)["task"]).to eq("billing:invoices")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
it "no incluye keys con valor nil en el output" do
|
|
514
|
+
stub_const("ExisRay::Tracer", Module.new do
|
|
515
|
+
define_singleton_method(:service_name) { "test-service" }
|
|
516
|
+
define_singleton_method(:root_id) { nil }
|
|
517
|
+
define_singleton_method(:trace_id) { nil }
|
|
518
|
+
define_singleton_method(:request_id) { nil }
|
|
519
|
+
define_singleton_method(:source) { nil }
|
|
520
|
+
define_singleton_method(:sidekiq_job) { nil }
|
|
521
|
+
define_singleton_method(:task) { nil }
|
|
522
|
+
end)
|
|
523
|
+
|
|
524
|
+
result = call("event=foo")
|
|
525
|
+
|
|
526
|
+
expect(result).not_to have_key("root_id")
|
|
527
|
+
expect(result).not_to have_key("task")
|
|
528
|
+
expect(result).not_to have_key("source")
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
466
532
|
describe "#filter_sensitive_hash (privado)" do
|
|
467
533
|
it "filtra claves sensibles en el nivel raíz" do
|
|
468
534
|
result = formatter.send(:filter_sensitive_hash, { user: "gabriel", password: "secret" })
|
|
@@ -151,6 +151,33 @@ RSpec.describe ExisRay::LogSubscriber do
|
|
|
151
151
|
expect(data[:server_address]).to eq("api.example.com")
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
it "emite url.path (OTel v1.0) siempre con la URL concreta" do
|
|
155
|
+
event = build_event(payload: base_payload)
|
|
156
|
+
|
|
157
|
+
data = subscriber.send(:build_payload, event)
|
|
158
|
+
|
|
159
|
+
expect(data[:"url.path"]).to eq("/users/42")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "emite path (legacy Wispro) junto a url.path cuando emit_legacy_path_key=true (default)" do
|
|
163
|
+
event = build_event(payload: base_payload)
|
|
164
|
+
|
|
165
|
+
data = subscriber.send(:build_payload, event)
|
|
166
|
+
|
|
167
|
+
expect(data[:path]).to eq("/users/42")
|
|
168
|
+
expect(data[:"url.path"]).to eq("/users/42")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "omite path cuando emit_legacy_path_key=false (solo url.path)" do
|
|
172
|
+
allow(ExisRay.configuration).to receive(:emit_legacy_path_key).and_return(false)
|
|
173
|
+
event = build_event(payload: base_payload)
|
|
174
|
+
|
|
175
|
+
data = subscriber.send(:build_payload, event)
|
|
176
|
+
|
|
177
|
+
expect(data).not_to have_key(:path)
|
|
178
|
+
expect(data[:"url.path"]).to eq("/users/42")
|
|
179
|
+
end
|
|
180
|
+
|
|
154
181
|
# Helper: construye un mock de ActionDispatch::Journey::Route con la API REAL
|
|
155
182
|
# que usa el código de producción (route.defaults, route.path.spec, route.verb).
|
|
156
183
|
# Esto evita el anti-patrón anterior de stubear una API inventada.
|
|
@@ -268,6 +295,22 @@ RSpec.describe ExisRay::LogSubscriber do
|
|
|
268
295
|
expect(data[:error_message]).to eq("something went wrong")
|
|
269
296
|
end
|
|
270
297
|
|
|
298
|
+
it "omite error_class/error_message cuando emit_legacy_exception_keys=false" do
|
|
299
|
+
allow(ExisRay.configuration).to receive(:emit_legacy_exception_keys).and_return(false)
|
|
300
|
+
|
|
301
|
+
event = build_event(payload: base_payload.merge(
|
|
302
|
+
status: nil,
|
|
303
|
+
exception: ["RuntimeError", "something went wrong"]
|
|
304
|
+
))
|
|
305
|
+
|
|
306
|
+
data = subscriber.send(:build_payload, event)
|
|
307
|
+
|
|
308
|
+
expect(data[:"exception.type"]).to eq("RuntimeError")
|
|
309
|
+
expect(data[:"exception.message"]).to eq("something went wrong")
|
|
310
|
+
expect(data).not_to have_key(:error_class)
|
|
311
|
+
expect(data).not_to have_key(:error_message)
|
|
312
|
+
end
|
|
313
|
+
|
|
271
314
|
it "inyecta exception.stacktrace desde payload[:exception_object]" do
|
|
272
315
|
exception_object = RuntimeError.new("boom")
|
|
273
316
|
exception_object.set_backtrace(["a.rb:1", "b.rb:2", "c.rb:3"])
|
|
@@ -49,7 +49,22 @@ RSpec.describe ExisRay::TaskMonitor do
|
|
|
49
49
|
expect(start_log[0]).to eq(:info)
|
|
50
50
|
expect(start_log[1]).to include("outcome=started")
|
|
51
51
|
expect(start_log[1]).to include("event=task_started")
|
|
52
|
-
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "no emite `task=` en el KV (la clave viene del formatter vía Tracer.task)" do
|
|
55
|
+
captured_task = nil
|
|
56
|
+
described_class.run("billing:invoices") { captured_task = ExisRay::Tracer.task }
|
|
57
|
+
|
|
58
|
+
# task_name vive en el Tracer durante la ejecución del bloque
|
|
59
|
+
expect(captured_task).to eq("billing:invoices")
|
|
60
|
+
|
|
61
|
+
# Los KV de lifecycle NO incluyen `task=...` — evita la duplicación con
|
|
62
|
+
# `JsonFormatter#inject_tracer_context` (issue #12).
|
|
63
|
+
lifecycle = captured_logs.map { |_, msg| msg }.select { |m| m.include?("event=task_") }
|
|
64
|
+
expect(lifecycle).not_to be_empty
|
|
65
|
+
lifecycle.each do |msg|
|
|
66
|
+
expect(msg).not_to match(/\btask=/)
|
|
67
|
+
end
|
|
53
68
|
end
|
|
54
69
|
|
|
55
70
|
it "loguea task_finished con outcome=success (breaking rename v0.6.0)" do
|
|
@@ -94,6 +109,21 @@ RSpec.describe ExisRay::TaskMonitor do
|
|
|
94
109
|
expect(finish_log[1]).to include("exception.stacktrace=")
|
|
95
110
|
end
|
|
96
111
|
|
|
112
|
+
it "omite error_class/error_message cuando emit_legacy_exception_keys=false" do
|
|
113
|
+
allow(ExisRay.configuration).to receive(:emit_legacy_exception_keys).and_return(false)
|
|
114
|
+
|
|
115
|
+
expect do
|
|
116
|
+
described_class.run("task:boom") { raise "kaboom" }
|
|
117
|
+
end.to raise_error(RuntimeError, "kaboom")
|
|
118
|
+
|
|
119
|
+
finish_log = captured_logs.find { |lvl, msg| lvl == :error && msg.include?("task_finished") }
|
|
120
|
+
expect(finish_log[1]).not_to include("error_class=")
|
|
121
|
+
expect(finish_log[1]).not_to include("error_message=")
|
|
122
|
+
expect(finish_log[1]).to include("exception.type=RuntimeError")
|
|
123
|
+
expect(finish_log[1]).to include("exception.message=")
|
|
124
|
+
expect(finish_log[1]).to include("exception.stacktrace=")
|
|
125
|
+
end
|
|
126
|
+
|
|
97
127
|
it "resetea el Tracer incluso en caso de error" do
|
|
98
128
|
begin
|
|
99
129
|
described_class.run("task:boom") { raise "kaboom" }
|
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
|
+
version: 0.11.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-05-
|
|
11
|
+
date: 2026-05-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -90,8 +90,8 @@ licenses:
|
|
|
90
90
|
metadata:
|
|
91
91
|
homepage_uri: https://github.com/gedera/exis_ray
|
|
92
92
|
source_code_uri: https://github.com/gedera/exis_ray
|
|
93
|
-
changelog_uri: https://github.com/gedera/exis_ray/blob/v0.
|
|
94
|
-
documentation_uri: https://github.com/gedera/exis_ray/blob/v0.
|
|
93
|
+
changelog_uri: https://github.com/gedera/exis_ray/blob/v0.11.0/CHANGELOG.md
|
|
94
|
+
documentation_uri: https://github.com/gedera/exis_ray/blob/v0.11.0/skill
|
|
95
95
|
post_install_message:
|
|
96
96
|
rdoc_options: []
|
|
97
97
|
require_paths:
|