exis_ray 0.8.0 → 0.10.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 +16 -0
- data/CLAUDE.md +2 -1
- data/README.md +22 -6
- data/docs/behavior/behavior.md +75 -0
- data/docs/glossary/glossary.md +76 -0
- data/lib/exis_ray/http_middleware.rb +7 -0
- data/lib/exis_ray/json_formatter.rb +23 -1
- data/lib/exis_ray/task_monitor.rb +3 -3
- data/lib/exis_ray/version.rb +1 -1
- data/skill/SKILL.md +31 -3
- data/skills.yml +14 -28
- data/spec/exis_ray/http_middleware_spec.rb +68 -0
- data/spec/exis_ray/json_formatter_spec.rb +123 -0
- data/spec/exis_ray/task_monitor_spec.rb +16 -1
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78d2b6cbc5e3da104fee7f3d2f481311acd2da7971d80295edf0bbad6eaab178
|
|
4
|
+
data.tar.gz: c54ebfc75e0c5abf640506b97b41b4b86bfe1f05a8d17a2d484941dae0fb21d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6642e744b2b9fc0d798e419664d76ebe2a0beba38722b1db09ac501d408938695990d4d752a0b0bf9cad78aa1a62a3939113e1444a623df890cabb8a4f0ad31
|
|
7
|
+
data.tar.gz: e96e7936add04a739ac1f325d1b768437c82381f5252420f2ab0eb72d6ad51e50bc272c7b136e4f1c0096d59f50b4b9fdc227ec05086e374e385ad543d5d47e8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [0.10.0] - 2026-05-25
|
|
2
|
+
|
|
3
|
+
### Documentación
|
|
4
|
+
- **`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).
|
|
5
|
+
|
|
6
|
+
### Correcciones
|
|
7
|
+
- **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.
|
|
8
|
+
|
|
9
|
+
## [0.9.0] - 2026-05-19
|
|
10
|
+
|
|
11
|
+
### Correcciones
|
|
12
|
+
- **HTTP entrypoint sin trace header no emitía `root_id`/`source`/`request_id`** (#9): `HttpMiddleware` era el único entrypoint sin fallback de `root_id`. Un servicio que es punto de entrada (no eslabón intermedio de un trace distribuido) emitía log lines sin `root_id` y, por efecto cascada en `JsonFormatter#inject_tracer_context` (`return unless root_id`), sin `source` —campo mandatorio del estándar Wispro—. Ahora `HttpMiddleware` genera un `root_id` fresco cuando no llega trace header, igual que `Sidekiq::ServerMiddleware`/`BugBunny::ConsumerTracingMiddleware`/`TaskMonitor`. Además `JsonFormatter` emite `request_id` fuera del guard de `root_id` (distinto ciclo de vida: UUID v4 de Rails vs formato X-Ray), así un servicio puede correlacionar por request aunque no haya trace context activo.
|
|
13
|
+
|
|
14
|
+
### Documentación
|
|
15
|
+
- **Artefactos dev-\* (RFC-008/010)**: nuevos `docs/behavior/behavior.md` (RFC-007 — secuencia de hidratación de trace por entrypoint y emisión en logs, cobertura declarada) y `docs/glossary/glossary.md` (RFC-009 — lenguaje ubicuo del bounded context: `root_id`, `trace_id`, `source`, `request_id`, `entrypoint`, ...). `README.md` y `skill/SKILL.md` recompuestos indexando el detalle (indexa, no duplica): índice de artefactos, nota de coexistencia transitoria para capas F2, version-lock declarado y `description` endurecida como contrato de discoverability.
|
|
16
|
+
|
|
1
17
|
## [0.8.0] - 2026-05-14
|
|
2
18
|
|
|
3
19
|
### Nuevas funcionalidades
|
data/CLAUDE.md
CHANGED
|
@@ -105,9 +105,10 @@ ExisRay.configuration.json_logs? # => true/false
|
|
|
105
105
|
| `service` | Siempre |
|
|
106
106
|
| `service_version` | Siempre (de `config.version` o `config.x.version`) |
|
|
107
107
|
| `deployment_environment` | Siempre (de `Rails.env`) |
|
|
108
|
+
| `request_id` | Cuando `Tracer.request_id` está presente (independiente de root_id) |
|
|
108
109
|
| `root_id` | Cuando hay trace context activo |
|
|
109
110
|
| `trace_id` | Cuando hay trace context activo |
|
|
110
|
-
| `source` | Cuando hay trace context activo |
|
|
111
|
+
| `source` | Cuando hay trace context activo (HTTP siempre genera root fresco si no llega header) |
|
|
111
112
|
| `correlation_id` | Cuando `Current.correlation_id` está presente |
|
|
112
113
|
| `user_id` | Cuando `Current.user_id` está presente |
|
|
113
114
|
| `isp_id` | Cuando `Current.isp_id` está presente |
|
data/README.md
CHANGED
|
@@ -51,11 +51,24 @@ ExisRay opera en tres capas que se combinan automáticamente:
|
|
|
51
51
|
|
|
52
52
|
**Flujo de propagación:**
|
|
53
53
|
|
|
54
|
-
1. Un request/job/mensaje llega al servicio. El middleware correspondiente hidrata el `Tracer` con el header entrante
|
|
55
|
-
2. `JsonFormatter` inyecta
|
|
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 `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
|
|
|
59
|
+
## Documentación de detalle
|
|
60
|
+
|
|
61
|
+
Artefactos de detalle (RFC-008 — el contrato y el significado viven acá; este README indexa y resume, no duplica):
|
|
62
|
+
|
|
63
|
+
| Capa | Artefacto | Estado |
|
|
64
|
+
|:-----|:----------|:-------|
|
|
65
|
+
| Comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) — secuencias de hidratación de trace por entrypoint y emisión en logs | parcial, incremental por PR |
|
|
66
|
+
| Glosario | [`docs/glossary/glossary.md`](docs/glossary/glossary.md) — lenguaje ubicuo (`root_id`, `trace_id`, `source`, `request_id`, `entrypoint`, ...) | sembrado inicial, acreta |
|
|
67
|
+
| Datos | — | n/a (gema sin DB) |
|
|
68
|
+
| Operaciones · Interfaz · Topología | — | F2 `dev-structure`, no implementado |
|
|
69
|
+
|
|
70
|
+
> **Coexistencia transitoria (RFC-008 §2):** las secciones _Cómo funciona_, _Campos auto-inyectados_ y _Referencia del Tracer_ de este README contienen contrato/arquitectura cuyo destino estructural (`docs/interface`, `docs/topology`) es **F2 no implementado**. Permanecen embebidas hasta que esas capas existan; el significado y las secuencias ya migraron a `docs/glossary` y `docs/behavior` y se referencian desde acá.
|
|
71
|
+
|
|
59
72
|
## Instalación
|
|
60
73
|
|
|
61
74
|
```ruby
|
|
@@ -326,7 +339,9 @@ config.logger.formatter = ExisRay::JsonFormatter
|
|
|
326
339
|
|
|
327
340
|
### Campos auto-inyectados
|
|
328
341
|
|
|
329
|
-
`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.
|
|
330
345
|
|
|
331
346
|
| Campo | Condición |
|
|
332
347
|
|:------|:----------|
|
|
@@ -336,9 +351,10 @@ config.logger.formatter = ExisRay::JsonFormatter
|
|
|
336
351
|
| `service` | Siempre (nombre de la app Rails en snake_case) |
|
|
337
352
|
| `service_version` | Siempre (lee de `config.version` o `config.x.version`) |
|
|
338
353
|
| `deployment_environment` | Siempre (lee de `Rails.env`) |
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
354
|
+
| `request_id` | Cuando `Tracer.request_id` está presente (independiente del trace context — ver [glosario](docs/glossary/glossary.md)) |
|
|
355
|
+
| `root_id` | Siempre en cualquier entrypoint (genera uno fresco si no llega trace header) |
|
|
356
|
+
| `trace_id` | Cuando hay trace header entrante parseado (eslabón intermedio) |
|
|
357
|
+
| `source` | Siempre en cualquier entrypoint (`http`, `sidekiq`, `task`, `system`) |
|
|
342
358
|
| `correlation_id` | Cuando `Current.correlation_id` está presente |
|
|
343
359
|
| `user_id` | Cuando `Current.user_id` no es nil |
|
|
344
360
|
| `isp_id` | Cuando `Current.isp_id` no es nil |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Comportamiento — exis_ray
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-007 (UML 2.x sequence / BPMN 2.0, render Mermaid) · generado dev-enrich · anclado a commit `00cf803` (v0.8.0 + fix issue #9) · cobertura **parcial incremental**
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
Comportamiento runtime de la trazabilidad distribuida: hidratación del `Tracer` en cada entrypoint y emisión del contexto en cada línea de log. Documentación **incremental** — este artefacto se inició con el PR del issue #9 (flujo de entrypoint HTTP); el resto de flujos se acreta cuando se tocan.
|
|
8
|
+
|
|
9
|
+
## 2. Cuerpo
|
|
10
|
+
|
|
11
|
+
### Cobertura (obligatoria — RFC-007 §2.a)
|
|
12
|
+
|
|
13
|
+
| Flujo | Estado | Origen |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Hidratación de trace en entrypoint HTTP + emisión en logs | **documentado** | PR issue #9 |
|
|
16
|
+
| Hidratación en entrypoint Sidekiq (server middleware) | **referenciado** (no diagramado) | — |
|
|
17
|
+
| Hidratación en entrypoint BugBunny consumer | **referenciado** (no diagramado) | — |
|
|
18
|
+
| Hidratación en entrypoint TaskMonitor (Rake/Cron) | **referenciado** (no diagramado) | — |
|
|
19
|
+
| Propagación saliente (Faraday / ActiveResource / BugBunny publisher) | **NO documentado** | — |
|
|
20
|
+
| Ciclo de vida RPC reply (BugBunny) | **NO documentado** | — |
|
|
21
|
+
|
|
22
|
+
Ausencia de un flujo en este artefacto ≠ inexistencia del flujo. Acreta por PR.
|
|
23
|
+
|
|
24
|
+
### Flujo: Hidratación de trace en entrypoint HTTP + emisión en logs
|
|
25
|
+
|
|
26
|
+
Invariante de paridad: los 4 entrypoints (`HttpMiddleware`, `Sidekiq::ServerMiddleware`, `BugBunny::ConsumerTracingMiddleware`, `TaskMonitor`) garantizan un `root_id` aunque no llegue trace header entrante. Antes del fix de issue #9, `HttpMiddleware` era el único sin ese fallback: un servicio que es **punto de entrada** (no eslabón intermedio) emitía logs sin `root_id`, y por efecto cascada sin `source` (campo mandatorio del estándar Wispro).
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
sequenceDiagram
|
|
30
|
+
participant Cliente
|
|
31
|
+
participant HM as HttpMiddleware
|
|
32
|
+
participant T as ExisRay::Tracer
|
|
33
|
+
participant App as Rack app / Controller
|
|
34
|
+
participant JF as JsonFormatter
|
|
35
|
+
|
|
36
|
+
Cliente->>HM: request (con o sin trace_header)
|
|
37
|
+
HM->>T: hydrate(trace_id: env[trace_header], source: "http")
|
|
38
|
+
T->>T: created_at, source="http", trace_id=...
|
|
39
|
+
T->>T: parse_trace_id
|
|
40
|
+
alt trace_header presente (eslabón intermedio)
|
|
41
|
+
T->>T: root_id = data["Root"] (parseado del header)
|
|
42
|
+
else sin trace_header (servicio entrypoint)
|
|
43
|
+
T-->>HM: root_id queda nil
|
|
44
|
+
HM->>T: root_id ||= generate_new_root %% fix issue #9 Gap A
|
|
45
|
+
T->>T: root_id = "Root=1-<ts>-<rand>" (fresco)
|
|
46
|
+
end
|
|
47
|
+
HM->>T: request_id = env["action_dispatch.request_id"]
|
|
48
|
+
HM->>HM: ExisRay.sync_correlation_id
|
|
49
|
+
HM->>App: @app.call(env)
|
|
50
|
+
App->>JF: Rails.logger.info "event=..."
|
|
51
|
+
JF->>T: request_id? %% issue #9 Gap C: fuera del guard de root_id
|
|
52
|
+
JF->>JF: payload[:request_id] = T.request_id (si presente)
|
|
53
|
+
JF->>T: root_id?
|
|
54
|
+
alt root_id presente (siempre en entrypoint, post-fix)
|
|
55
|
+
JF->>JF: payload += root_id, trace_id, source, ...
|
|
56
|
+
end
|
|
57
|
+
JF-->>App: línea JSON con source + correlación
|
|
58
|
+
Note over HM,App: rescue StandardError ⇒ @app.call(env) igual<br/>(logging nunca rompe el flujo principal)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Contexto: `JsonFormatter#inject_tracer_context` corta todo su bloque con `return unless root_id` (Gap B — efecto cascada de Gap A). El fix de Gap A (root fresco en `HttpMiddleware`) des-gatea ese bloque y `source` vuelve a emitirse. `request_id` se emite **fuera** de ese guard (Gap C): tiene distinto ciclo de vida que `root_id` (UUID v4 de Rails vs formato X-Ray) y un servicio puede querer correlación por request aunque no haya trace context.
|
|
62
|
+
|
|
63
|
+
## 3. Inferencias
|
|
64
|
+
|
|
65
|
+
| Afirmación | confidence | a verificar por humano |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Los 4 entrypoints comparten la invariante "siempre hay root_id" | `declared` | código de los 4 middlewares lo confirma (issue #9 lo tabula) |
|
|
68
|
+
| `request_id` no se emitía en ninguna línea pre-fix | `declared` | confirmado: `JsonFormatter` solo lo usaba interno vía `clean_request_id` |
|
|
69
|
+
| Orden de emisión (request_id antes del guard de root_id) | `declared` | `json_formatter.rb#inject_tracer_context` post-fix |
|
|
70
|
+
|
|
71
|
+
## 4. Cobertura y fronteras
|
|
72
|
+
|
|
73
|
+
- **Incremental:** solo el flujo de entrypoint HTTP está diagramado. Sidekiq/BugBunny/TaskMonitor referenciados por paridad pero no diagramados (acretan cuando se toquen).
|
|
74
|
+
- **Fuera de alcance:** estructura de datos (gema sin DB → no aplica), interfaz Ruby pública / topología (capas F2 de `dev-structure`, no implementadas).
|
|
75
|
+
- **Significado de términos** (`root_id`, `source`, etc.) → `docs/glossary/glossary.md`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Glosario — exis_ray
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-009 (DDD ubiquitous language / bounded context + ISO 11179) · generado dev-enrich · anclado a commit `00cf803` · cobertura **parcial — sembrado inicial, acreta por PR**
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
Lenguaje ubicuo del bounded context **exis_ray** (observabilidad/trazabilidad del ecosistema Wispro). Significado de cada término **en esta gema**. Gema sin capa de datos (`docs/data` = n/a): el `Binding` apunta al símbolo público estable que *es* el concepto (ISO 11179, materialización no-tabular), no a tablas.
|
|
8
|
+
|
|
9
|
+
## 2. Cuerpo
|
|
10
|
+
|
|
11
|
+
## entrypoint
|
|
12
|
+
|
|
13
|
+
Punto donde la ejecución entra al servicio y nace (o se continúa) un contexto de trazabilidad. Hay exactamente 4: HTTP, Sidekiq, BugBunny consumer, TaskMonitor. **Invariante:** todo entrypoint garantiza un `root_id` — si no llega trace header entrante, genera uno fresco. Un servicio que es entrypoint sin trace upstream (ej. collector/ingest) igual emite logs correlacionables y con `source`.
|
|
14
|
+
|
|
15
|
+
**Binding:** `ExisRay::HttpMiddleware`, `ExisRay::Sidekiq::ServerMiddleware`, `ExisRay::BugBunny::ConsumerTracingMiddleware`, `ExisRay::TaskMonitor`.
|
|
16
|
+
|
|
17
|
+
## root_id
|
|
18
|
+
|
|
19
|
+
Identificador raíz de un trace distribuido (formato AWS X-Ray: `1-<ts_hex>-<rand_hex>`, prefijado `Root=`). **Constante a lo largo de toda la cadena** de servicios: nace en el primer entrypoint y se propaga sin cambiar. Es el campo de correlación primario de logs.
|
|
20
|
+
|
|
21
|
+
**Binding:** `ExisRay::Tracer.root_id`.
|
|
22
|
+
|
|
23
|
+
## trace_id
|
|
24
|
+
|
|
25
|
+
Header de traza entrante completo, sin parsear: `Root=...;Self=...;CalledFrom=...;TotalTimeSoFar=...ms`. Presente solo cuando el servicio es **eslabón intermedio** (recibió header upstream). Un entrypoint sin trace upstream tiene `root_id` (fresco) pero no `trace_id`.
|
|
26
|
+
|
|
27
|
+
**Binding:** `ExisRay::Tracer.trace_id`.
|
|
28
|
+
|
|
29
|
+
## self_id
|
|
30
|
+
|
|
31
|
+
Identificador del span del servicio actual dentro del trace. Se genera por servicio y viaja en el header saliente como `Self=`.
|
|
32
|
+
|
|
33
|
+
**Binding:** `ExisRay::Tracer.self_id`.
|
|
34
|
+
|
|
35
|
+
## source
|
|
36
|
+
|
|
37
|
+
Entrypoint de ejecución que originó la línea de log. Valores válidos: `http`, `sidekiq`, `task`, `system`. **Campo mandatorio** del estándar Wispro de logging — toda línea debe tenerlo. Se emite dentro del bloque de tracer context, que está gateado por `root_id`; por eso la invariante "todo entrypoint garantiza root_id" es lo que hace que `source` nunca falte.
|
|
38
|
+
|
|
39
|
+
**Binding:** `ExisRay::Tracer.source`.
|
|
40
|
+
|
|
41
|
+
## request_id
|
|
42
|
+
|
|
43
|
+
UUID v4 del request HTTP, provisto por `ActionDispatch::RequestId` (`env["action_dispatch.request_id"]`). **Distinto ciclo de vida que `root_id`**: identificador por-request de Rails, no formato X-Ray, no constante a lo largo de la cadena. Se emite en logs fuera del guard de `root_id` — un servicio puede querer correlación por request aunque no haya trace context activo.
|
|
44
|
+
|
|
45
|
+
**Binding:** `ExisRay::Tracer.request_id`.
|
|
46
|
+
|
|
47
|
+
## correlation_id
|
|
48
|
+
|
|
49
|
+
ID de correlación de negocio compuesto `ServiceName;RootID`, sincronizado al `Current` configurado por la app host. Liga logs de negocio con el trace.
|
|
50
|
+
|
|
51
|
+
**Binding:** `ExisRay::Tracer.correlation_id`, `ExisRay::Current#correlation_id`.
|
|
52
|
+
|
|
53
|
+
## trace context
|
|
54
|
+
|
|
55
|
+
Conjunto thread-safe de atributos de trazabilidad activos en la request/job actual (`root_id`, `trace_id`, `self_id`, `source`, `request_id`, `created_at`, ...). Vive en `ActiveSupport::CurrentAttributes`; se hidrata en el entrypoint y se resetea al finalizar. "Hay trace context activo" ≡ `root_id` presente.
|
|
56
|
+
|
|
57
|
+
**Binding:** `ExisRay::Tracer` (subclase de `ActiveSupport::CurrentAttributes`).
|
|
58
|
+
|
|
59
|
+
## propagation header
|
|
60
|
+
|
|
61
|
+
Header saliente que lleva el trace al siguiente servicio (`X-Amzn-Trace-Id` por default): `Root=...;Self=...;CalledFrom=<service>;TotalTimeSoFar=<acc>ms`. Lo genera `Tracer.generate_trace_header`. Distinto del header **entrante** (formato Rack `HTTP_X_AMZN_TRACE_ID`).
|
|
62
|
+
|
|
63
|
+
**Binding:** `ExisRay::Tracer.generate_trace_header`, `ExisRay.configuration.propagation_trace_header`.
|
|
64
|
+
|
|
65
|
+
## 3. Inferencias
|
|
66
|
+
|
|
67
|
+
| Término | confidence | a verificar |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Significado de negocio de cada término | `inferred` (LLM ancló al código; significado lo confirma humano) | revisar que la prosa refleje el uso real en servicios consumidores |
|
|
70
|
+
| `trace context activo ≡ root_id presente` | `declared` | `JsonFormatter#inject_tracer_context` lo implementa literal |
|
|
71
|
+
|
|
72
|
+
## 4. Cobertura y fronteras
|
|
73
|
+
|
|
74
|
+
- **Sembrado inicial** disparado por PR issue #9; cubre los términos del núcleo trace/log. Acreta por PR (ausencia ≠ inexistencia).
|
|
75
|
+
- **Términos no cubiertos aún:** `Reporter`/Sentry context, `total_time_so_far`, `called_from`, `log_fields`, `sidekiq_job`, `task`.
|
|
76
|
+
- **Frontera:** comportamiento/secuencias → `docs/behavior/behavior.md`. Estructura de datos → n/a (gema sin DB).
|
|
@@ -13,6 +13,13 @@ module ExisRay
|
|
|
13
13
|
trace_id: env[ExisRay.configuration.trace_header],
|
|
14
14
|
source: "http"
|
|
15
15
|
)
|
|
16
|
+
# Si la request no trae trace header entrante (servicio que es punto de
|
|
17
|
+
# entrada, no eslabón intermedio de un trace distribuido), generamos un
|
|
18
|
+
# root_id fresco igual que el resto de entrypoints
|
|
19
|
+
# (Sidekiq::ServerMiddleware, BugBunny::ConsumerTracingMiddleware,
|
|
20
|
+
# TaskMonitor). Sin esto root_id queda nil y JsonFormatter dropea todo
|
|
21
|
+
# el bloque de tracer, incluido `source` (campo mandatorio). Ver issue #9.
|
|
22
|
+
ExisRay::Tracer.root_id ||= ExisRay::Tracer.send(:generate_new_root)
|
|
16
23
|
ExisRay::Tracer.request_id = env["action_dispatch.request_id"]
|
|
17
24
|
ExisRay.sync_correlation_id
|
|
18
25
|
|
|
@@ -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
|
|
@@ -102,6 +118,12 @@ module ExisRay
|
|
|
102
118
|
# @param payload [Hash] El diccionario del log donde se insertarán los datos.
|
|
103
119
|
# @return [void]
|
|
104
120
|
def inject_tracer_context(payload)
|
|
121
|
+
# `request_id` (UUID v4 de Rails) tiene distinto ciclo de vida que
|
|
122
|
+
# `root_id` (formato X-Ray). Se emite fuera del guard de `root_id`:
|
|
123
|
+
# un servicio puede querer correlación por request_id aunque no haya
|
|
124
|
+
# trace context activo. Ver issue #9 (Gap C).
|
|
125
|
+
payload[:request_id] = ExisRay::Tracer.request_id if ExisRay::Tracer.request_id
|
|
126
|
+
|
|
105
127
|
return unless ExisRay::Tracer.root_id
|
|
106
128
|
|
|
107
129
|
payload[:root_id] = ExisRay::Tracer.root_id
|
|
@@ -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,14 +38,14 @@ 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
50
|
"error_class=#{e.class} error_message=#{e.message.inspect} " \
|
|
51
51
|
"exception.type=#{e.class} exception.message=#{e.message.inspect} " \
|
data/lib/exis_ray/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: exis-ray
|
|
3
|
-
description:
|
|
3
|
+
description: Conocimiento completo de ExisRay, la capa de observabilidad y trazabilidad distribuida del ecosistema Wispro (logging JSON estructurado, trace context AWS X-Ray, propagación entre servicios). ACTIVAR cuando un servicio o gema Rails integra/configura ExisRay, emite o debuggea logs JSON estructurados, propaga trace context (header X-Amzn-Trace-Id) entre HTTP/Sidekiq/BugBunny, usa ExisRay::Tracer/Current/Reporter/JsonFormatter/TaskMonitor, edita el initializer de ExisRay, o resuelve errores/antipatrones de logging-trazabilidad Wispro.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ExisRay Expert
|
|
@@ -9,6 +9,14 @@ Observabilidad y trazabilidad distribuida para microservicios Rails (AWS X-Ray c
|
|
|
9
9
|
|
|
10
10
|
Para el complemento del estándar de logging Wispro (regla Data First, mapeo OpenTelemetry, ciclo de vida de jobs/requests), ver `references/standard.md`.
|
|
11
11
|
|
|
12
|
+
### Artefactos de detalle (RFC-008)
|
|
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.10.0** (post fix issues #9, #11, #12):
|
|
15
|
+
|
|
16
|
+
- [`docs/behavior/behavior.md`](../docs/behavior/behavior.md) — secuencias de hidratación de trace por entrypoint + emisión en logs (parcial, incremental).
|
|
17
|
+
- [`docs/glossary/glossary.md`](../docs/glossary/glossary.md) — lenguaje ubicuo del bounded context (`root_id`, `trace_id`, `source`, `request_id`, `entrypoint`, ...).
|
|
18
|
+
- Datos = n/a (gema sin DB). Operaciones/Interfaz/Topología = F2 `dev-structure`, no implementado: contrato Ruby permanece embebido abajo (coexistencia transitoria RFC-008 §2).
|
|
19
|
+
|
|
12
20
|
---
|
|
13
21
|
|
|
14
22
|
## Glosario
|
|
@@ -67,11 +75,11 @@ ExisRay unifica trazabilidad distribuida, logging estructurado JSON, contexto de
|
|
|
67
75
|
|
|
68
76
|
### Flujo runtime (HTTP request)
|
|
69
77
|
|
|
70
|
-
1. `HttpMiddleware` lee `trace_header` del env Rack, llama `Tracer.hydrate(trace_id:, source: "http")`
|
|
78
|
+
1. `HttpMiddleware` lee `trace_header` del env Rack, llama `Tracer.hydrate(trace_id:, source: "http")`. **Si no llega header** (servicio entrypoint, no eslabón intermedio) genera un `root_id` fresco — paridad con `Sidekiq::ServerMiddleware`/`BugBunny::ConsumerTracingMiddleware`/`TaskMonitor`. Captura `request_id` de `action_dispatch.request_id`. Secuencia detallada: `docs/behavior/behavior.md`.
|
|
71
79
|
2. `Tracer.parse_trace_id` extrae `root_id`, `self_id`, `called_from`, `total_time_so_far`
|
|
72
80
|
3. `ExisRay.sync_correlation_id` asigna `Tracer.correlation_id` a `Current.correlation_id`
|
|
73
81
|
4. Controller ejecuta `before_action` para setear `Current.user_id`, `Current.isp_id`
|
|
74
|
-
5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta
|
|
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.
|
|
75
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`/`exception.*`).
|
|
76
84
|
7. En llamadas salientes, `FaradayMiddleware`/`ActiveResourceInstrumentation` inyectan `propagation_trace_header` con `Tracer.generate_trace_header`
|
|
77
85
|
8. Al finalizar, `ActiveSupport::CurrentAttributes` hace reset automatico
|
|
@@ -287,6 +295,26 @@ Casteo automatico: integers, floats, objetos JSON (`{...}`, `[...]`). Filtra cla
|
|
|
287
295
|
|
|
288
296
|
Por eso `component` y `event` jamas se auto-inyectan, aunque el estandar Wispro los exija.
|
|
289
297
|
|
|
298
|
+
#### Condiciones de emision por campo
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
| Campo | Condicion de emision | Entrypoint que la garantiza |
|
|
303
|
+
|:------|:---------------------|:----------------------------|
|
|
304
|
+
| `time`, `level`, `severity_number`, `service`, `service_version`, `deployment_environment` | Siempre (no depende de Tracer/Current) | — |
|
|
305
|
+
| `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. |
|
|
306
|
+
| `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). |
|
|
307
|
+
| `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. |
|
|
308
|
+
| `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). |
|
|
309
|
+
| `sidekiq_job` | `Tracer.sidekiq_job` presente **y** `root_id` presente. | Sidekiq `ServerMiddleware`. |
|
|
310
|
+
| `task` | `Tracer.task` presente **y** `root_id` presente. | `TaskMonitor.run`. |
|
|
311
|
+
| `correlation_id` | `Current.correlation_id` presente. | `ExisRay.sync_correlation_id` (HTTP middleware lo llama; otros entrypoints solo si la app lo invoca). |
|
|
312
|
+
| `user_id`, `isp_id` | `Current.<attr>` no nil. | Lo setea la app (login, before_actions, etc.). |
|
|
313
|
+
| `Current.log_fields` (cualquier key) | La subclass de `Current` overrideo el hook y retorno un Hash no vacio. | — |
|
|
314
|
+
| `tags` | Rails tagged logging activo. **Antipatron con JSON** (rompe el formato) — ver FAQ. | — |
|
|
315
|
+
|
|
316
|
+
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).
|
|
317
|
+
|
|
290
318
|
#### Ejemplos: KV vs Hash producen output equivalente
|
|
291
319
|
|
|
292
320
|
```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
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe ExisRay::HttpMiddleware do
|
|
6
|
+
subject(:middleware) { described_class.new(app) }
|
|
7
|
+
|
|
8
|
+
let(:app) { ->(_env) { [200, {}, ["ok"]] } }
|
|
9
|
+
let(:trace_header) { ExisRay.configuration.trace_header }
|
|
10
|
+
|
|
11
|
+
after { ExisRay::Tracer.reset }
|
|
12
|
+
|
|
13
|
+
describe "#call" do
|
|
14
|
+
context "cuando la request NO trae trace header entrante (entrypoint, issue #9 Gap A)" do
|
|
15
|
+
let(:env) { { "action_dispatch.request_id" => "req-uuid-123" } }
|
|
16
|
+
|
|
17
|
+
it "genera un root_id fresco igual que los demás entrypoints" do
|
|
18
|
+
middleware.call(env)
|
|
19
|
+
|
|
20
|
+
expect(ExisRay::Tracer.root_id).to match(/\ARoot=1-/)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "setea source=http (des-gatea el bloque de tracer en JsonFormatter, Gap B)" do
|
|
24
|
+
middleware.call(env)
|
|
25
|
+
|
|
26
|
+
expect(ExisRay::Tracer.source).to eq("http")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "setea request_id desde el env de Rails" do
|
|
30
|
+
middleware.call(env)
|
|
31
|
+
|
|
32
|
+
expect(ExisRay::Tracer.request_id).to eq("req-uuid-123")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context "cuando la request trae trace header entrante (eslabón intermedio)" do
|
|
37
|
+
let(:env) do
|
|
38
|
+
{
|
|
39
|
+
trace_header => "Root=1-5759b146-abc123;Self=1-5759b146-def456;CalledFrom=upstream",
|
|
40
|
+
"action_dispatch.request_id" => "req-uuid-456"
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "respeta el root_id entrante y no genera uno nuevo" do
|
|
45
|
+
middleware.call(env)
|
|
46
|
+
|
|
47
|
+
expect(ExisRay::Tracer.root_id).to eq("1-5759b146-abc123")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "setea source=http y request_id" do
|
|
51
|
+
middleware.call(env)
|
|
52
|
+
|
|
53
|
+
expect(ExisRay::Tracer.source).to eq("http")
|
|
54
|
+
expect(ExisRay::Tracer.request_id).to eq("req-uuid-456")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "invoca la app y retorna su respuesta" do
|
|
59
|
+
expect(middleware.call({})).to eq([200, {}, ["ok"]])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "no rompe el flujo principal si la hidratación falla" do
|
|
63
|
+
allow(ExisRay::Tracer).to receive(:hydrate).and_raise(StandardError)
|
|
64
|
+
|
|
65
|
+
expect(middleware.call({})).to eq([200, {}, ["ok"]])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -15,6 +15,7 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
15
15
|
def self.service_name = "test-service"
|
|
16
16
|
def self.root_id = nil
|
|
17
17
|
def self.trace_id = nil
|
|
18
|
+
def self.request_id = nil
|
|
18
19
|
end)
|
|
19
20
|
|
|
20
21
|
allow(ExisRay).to receive(:current_class).and_return(nil)
|
|
@@ -406,6 +407,128 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
406
407
|
end
|
|
407
408
|
end
|
|
408
409
|
|
|
410
|
+
describe "inyección de contexto de tracer (issue #9)" do
|
|
411
|
+
def stub_tracer(root_id:, request_id:, source:, trace_id: nil)
|
|
412
|
+
stub_const("ExisRay::Tracer", Module.new do
|
|
413
|
+
define_singleton_method(:service_name) { "test-service" }
|
|
414
|
+
define_singleton_method(:root_id) { root_id }
|
|
415
|
+
define_singleton_method(:trace_id) { trace_id }
|
|
416
|
+
define_singleton_method(:request_id) { request_id }
|
|
417
|
+
define_singleton_method(:source) { source }
|
|
418
|
+
define_singleton_method(:sidekiq_job) { nil }
|
|
419
|
+
define_singleton_method(:task) { nil }
|
|
420
|
+
end)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
context "cuando hay trace context activo (root_id presente)" do
|
|
424
|
+
before { stub_tracer(root_id: "1-abc-def", request_id: "uuid-1", source: "http", trace_id: "Root=1-abc-def") }
|
|
425
|
+
|
|
426
|
+
it "emite root_id, trace_id, source y request_id" do
|
|
427
|
+
result = call("event=acct.received")
|
|
428
|
+
|
|
429
|
+
expect(result).to include(
|
|
430
|
+
"root_id" => "1-abc-def",
|
|
431
|
+
"trace_id" => "Root=1-abc-def",
|
|
432
|
+
"source" => "http",
|
|
433
|
+
"request_id" => "uuid-1"
|
|
434
|
+
)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
context "cuando NO hay trace context (root_id nil) pero sí request_id (Gap C)" do
|
|
439
|
+
before { stub_tracer(root_id: nil, request_id: "uuid-2", source: "http") }
|
|
440
|
+
|
|
441
|
+
it "emite request_id aunque root_id esté ausente" do
|
|
442
|
+
result = call("event=acct.received")
|
|
443
|
+
|
|
444
|
+
expect(result["request_id"]).to eq("uuid-2")
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it "no emite root_id ni trace_id" do
|
|
448
|
+
result = call("event=acct.received")
|
|
449
|
+
|
|
450
|
+
expect(result).not_to have_key("root_id")
|
|
451
|
+
expect(result).not_to have_key("trace_id")
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
context "cuando no hay request_id" do
|
|
456
|
+
before { stub_tracer(root_id: "1-x-y", request_id: nil, source: "http") }
|
|
457
|
+
|
|
458
|
+
it "no incluye la clave request_id" do
|
|
459
|
+
result = call("event=acct.received")
|
|
460
|
+
|
|
461
|
+
expect(result).not_to have_key("request_id")
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
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
|
+
|
|
409
532
|
describe "#filter_sensitive_hash (privado)" do
|
|
410
533
|
it "filtra claves sensibles en el nivel raíz" do
|
|
411
534
|
result = formatter.send(:filter_sensitive_hash, { user: "gabriel", password: "secret" })
|
|
@@ -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
|
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.10.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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -51,6 +51,8 @@ files:
|
|
|
51
51
|
- LICENSE.txt
|
|
52
52
|
- README.md
|
|
53
53
|
- Rakefile
|
|
54
|
+
- docs/behavior/behavior.md
|
|
55
|
+
- docs/glossary/glossary.md
|
|
54
56
|
- lib/exis_ray.rb
|
|
55
57
|
- lib/exis_ray/active_resource_instrumentation.rb
|
|
56
58
|
- lib/exis_ray/bug_bunny/consumer_tracing_middleware.rb
|
|
@@ -75,6 +77,7 @@ files:
|
|
|
75
77
|
- skills.yml
|
|
76
78
|
- spec/exis_ray/configuration_spec.rb
|
|
77
79
|
- spec/exis_ray/current_spec.rb
|
|
80
|
+
- spec/exis_ray/http_middleware_spec.rb
|
|
78
81
|
- spec/exis_ray/json_formatter_spec.rb
|
|
79
82
|
- spec/exis_ray/log_subscriber_spec.rb
|
|
80
83
|
- spec/exis_ray/reporter_spec.rb
|
|
@@ -87,8 +90,8 @@ licenses:
|
|
|
87
90
|
metadata:
|
|
88
91
|
homepage_uri: https://github.com/gedera/exis_ray
|
|
89
92
|
source_code_uri: https://github.com/gedera/exis_ray
|
|
90
|
-
changelog_uri: https://github.com/gedera/exis_ray/blob/v0.
|
|
91
|
-
documentation_uri: https://github.com/gedera/exis_ray/blob/v0.
|
|
93
|
+
changelog_uri: https://github.com/gedera/exis_ray/blob/v0.10.0/CHANGELOG.md
|
|
94
|
+
documentation_uri: https://github.com/gedera/exis_ray/blob/v0.10.0/skill
|
|
92
95
|
post_install_message:
|
|
93
96
|
rdoc_options: []
|
|
94
97
|
require_paths:
|