exis_ray 0.10.0 → 0.11.1
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/AGENTS.md +238 -0
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +9 -183
- data/README.md +36 -6
- data/docs/config/configuracion.md +144 -0
- data/docs/interface/interface.md +120 -0
- data/docs/test/testing.md +156 -0
- data/lib/exis_ray/configuration.rb +21 -0
- data/lib/exis_ray/log_subscriber.rb +13 -7
- data/lib/exis_ray/task_monitor.rb +21 -4
- data/lib/exis_ray/version.rb +1 -1
- data/skill/SKILL.md +15 -8
- data/skills.yml +6 -24
- data/spec/exis_ray/configuration_spec.rb +22 -0
- data/spec/exis_ray/log_subscriber_spec.rb +43 -0
- data/spec/exis_ray/task_monitor_spec.rb +15 -0
- metadata +8 -5
- data/.gemini/docs/TELEMETRY_GUIDELINES.md +0 -32
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Configuración — exis_ray
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-012 (12-Factor App §III, shape v2.2) · generado arch-structure + arch-enrich · anclado a commit `7db39ca` (v0.11.0) · cobertura **inventario base completa · §f enriquecido 11/11 · §j mapeado 2/2**
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
Gema de tracing/observabilidad. Configuración pública vía `ExisRay.configure do |config| ... end` (clase `ExisRay::Configuration`, 10 opciones con defaults compatibles AWS X-Ray) + 1 env var de runtime (`HOSTNAME`). Sin scheduler propio; el `Railtie` inyecta middlewares, formatter de logs e instrumentación de Sidekiq/BugBunny/ActiveResource al host durante el boot.
|
|
8
|
+
|
|
9
|
+
## 2. Cuerpo
|
|
10
|
+
|
|
11
|
+
### §a Hecho verificable
|
|
12
|
+
|
|
13
|
+
| métrica | conteo |
|
|
14
|
+
|---|---|
|
|
15
|
+
| total vars/opciones | 11 |
|
|
16
|
+
| requeridas | 0 |
|
|
17
|
+
| con default | 11 |
|
|
18
|
+
| derivadas | 2 |
|
|
19
|
+
| secretas | 0 |
|
|
20
|
+
|
|
21
|
+
### §b Inventario base
|
|
22
|
+
|
|
23
|
+
| nombre | tipo | requerida | default | origen | consumidor (file:line) | secret? |
|
|
24
|
+
|---|---|---|---|---|---|---|
|
|
25
|
+
| `trace_header` | String | no | `"HTTP_X_AMZN_TRACE_ID"` | code-default | `configuration.rb:76` | no |
|
|
26
|
+
| `propagation_trace_header` | String | no | `"X-Amzn-Trace-Id"` | code-default | `configuration.rb:77` | no |
|
|
27
|
+
| `reporter_class` | String/Class/nil | no | `nil` | code-default | `configuration.rb:78` | no |
|
|
28
|
+
| `current_class` | String/Class/nil | no | `nil` | code-default | `configuration.rb:79` | no |
|
|
29
|
+
| `log_format` | Symbol | no | `:text` | code-default | `configuration.rb:80` | no |
|
|
30
|
+
| `log_subscriber_class` | String/nil | no | `nil` | code-default | `configuration.rb:81` | no |
|
|
31
|
+
| `service_version` | String/nil | no | `derived(Rails config.version ∥ config.x.version)` | derived | `configuration.rb:82,93-111` | no |
|
|
32
|
+
| `deployment_environment` | String/nil | no | `derived(Rails.env)` | derived | `configuration.rb:83,113-117` | no |
|
|
33
|
+
| `emit_legacy_exception_keys` | Boolean | no | `true` | code-default | `configuration.rb:84` | no |
|
|
34
|
+
| `emit_legacy_path_key` | Boolean | no | `true` | code-default | `configuration.rb:85` | no |
|
|
35
|
+
| `HOSTNAME` | String | no | `"local"` (fallback) | env | `task_monitor.rb:81` | no |
|
|
36
|
+
|
|
37
|
+
> Solo shape — sin valores reales (RFC-012 §3). `requerida=no` en las 10 opciones: el `initialize` siembra default a todas; `HOSTNAME` cae a `"local"` si ausente.
|
|
38
|
+
|
|
39
|
+
### §c Meta-templates
|
|
40
|
+
|
|
41
|
+
`n/a` — sin patrón de sufijo/prefijo repetido ≥3 (no hay service discovery estática `_HOST/_PORT/_PROTOCOL`).
|
|
42
|
+
|
|
43
|
+
### §d Derivaciones simples
|
|
44
|
+
|
|
45
|
+
| var derivada | fórmula | fuente |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `service_version` | `Rails.application.config.version` → fallback `config.x.version` → `nil` | `configuration.rb:93-111` |
|
|
48
|
+
| `deployment_environment` | `Rails.env.to_s` | `configuration.rb:113-117` |
|
|
49
|
+
| `pod_id` (no-config) | `(ENV["HOSTNAME"] ∥ "local").split("-").last` | `task_monitor.rb:81` |
|
|
50
|
+
|
|
51
|
+
### §e Scheduling
|
|
52
|
+
|
|
53
|
+
`n/a` — la gema no define `sidekiq.yml`/`queue.yml`/`recurring.yml` ni queues/cron propios. **Instrumenta** el Sidekiq del host (middlewares) pero no agenda trabajo — ver §i.
|
|
54
|
+
|
|
55
|
+
### §i Inyecciones al host (Railtie)
|
|
56
|
+
|
|
57
|
+
| inyección | gatillo | efecto | file:line |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `exis_ray.configure_middleware` | initializer | inserta `ExisRay::HttpMiddleware` after `ActionDispatch::RequestId` (fallback `use`) | `railtie.rb:14-23` |
|
|
60
|
+
| `exis_ray.configure_logging` | initializer (after `:load_config_initializers`) | en modo texto: `app.config.log_tags << proc { trace_id ∥ root_id }` | `railtie.rb:28-36` |
|
|
61
|
+
| validación de clases | `config.after_initialize` | `raise` si `current_class`/`reporter_class` no heredan de base | `railtie.rb:45-53` |
|
|
62
|
+
| formatter JSON global | `config.after_initialize` (modo json) | `Rails.logger.formatter = ExisRay::JsonFormatter.new` | `railtie.rb:57` |
|
|
63
|
+
| LogSubscriber HTTP | `config.after_initialize` (modo json) | `ExisRay::LogSubscriber.install!` (reemplaza Lograge/defaults Rails) | `railtie.rb:62` |
|
|
64
|
+
| instrumentación BugBunny | `config.after_initialize` (si `defined?(::BugBunny)`) | consumer middleware + `rpc_reply_headers` + `on_rpc_reply` | `railtie.rb:68-90` |
|
|
65
|
+
| instrumentación ActiveResource | `config.after_initialize` (si `defined?(ActiveResource::Base)`) | `prepend ExisRay::ActiveResourceInstrumentation` | `railtie.rb:93-99` |
|
|
66
|
+
| instrumentación Sidekiq | `config.after_initialize` (si `defined?(::Sidekiq)`) | client/server middleware + `Sidekiq.logger.formatter` (modo json) | `railtie.rb:102-123` |
|
|
67
|
+
|
|
68
|
+
### §j Inyección a gemas configuradas
|
|
69
|
+
|
|
70
|
+
| gema configurada | superficie tocada | file:line | mapeo opción local → gema | ancla |
|
|
71
|
+
|---|---|---|---|---|
|
|
72
|
+
| `::BugBunny` | `consumer_middlewares.use ConsumerTracingMiddleware`; `configuration.rpc_reply_headers`; `configuration.on_rpc_reply` | `railtie.rb:71-88` | `propagation_trace_header` → key del header en `rpc_reply_headers` (`railtie.rb:75`) y lectura en `on_rpc_reply` (`railtie.rb:79`). El middleware/consumer no mapea opción de valor. | **integración opcional** (guard `defined?(::BugBunny)`), **no es dependencia declarada** (ausente de gemspec/Gemfile.lock) → sin ancla cross-repo, no aplica `skills.yml` |
|
|
73
|
+
| `::Sidekiq` | `configure_client` / `configure_server` (middleware chains); `Sidekiq.logger.formatter` | `railtie.rb:106-121` | `log_format=:json` (vía `json_logs?`) → `Sidekiq.logger.formatter = ExisRay::JsonFormatter.new` (`railtie.rb:121`). Las chains de middleware no mapean opción de valor. | **integración opcional** (guard `defined?(::Sidekiq)`), no es dependencia declarada → sin `docs/config/` a linkear |
|
|
74
|
+
|
|
75
|
+
## f. Enriquecimiento semántico
|
|
76
|
+
|
|
77
|
+
> cobertura: 11/11 vars enriquecidas; ausencia ≠ "no aplica".
|
|
78
|
+
|
|
79
|
+
### f.1 Propagación de trace context (`*trace_header`)
|
|
80
|
+
|
|
81
|
+
| var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
|
|
82
|
+
|---|---|---|---|---|---|
|
|
83
|
+
| `trace_header` | integration | silent-default (cae al header AWS X-Ray si no se setea) | per-request (leído por request) | boot-only | key del header **Rack entrante** que `HttpMiddleware` lee para hidratar el Tracer (`http_middleware.rb:13`); cambiarla solo si el edge/LB usa otro header de traza |
|
|
84
|
+
| `propagation_trace_header` | integration | silent-default | per-request | boot-only | key del header **saliente** que inyectan Faraday/ActiveResource/BugBunny-publisher + RPC reply (`faraday_middleware.rb:20`, `active_resource_instrumentation.rb:31`, `bug_bunny/publisher_tracing.rb:33`, `railtie.rb:75,79`); debe coincidir con el `trace_header` (forma HTTP) del downstream o la cadena se corta |
|
|
85
|
+
|
|
86
|
+
### f.2 Wiring de clases del host (`*_class`)
|
|
87
|
+
|
|
88
|
+
| var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
|
|
89
|
+
|---|---|---|---|---|---|
|
|
90
|
+
| `current_class` | orchestration | boot-crash @ after-initialize si la clase no hereda de `ExisRay::Current` (`railtie.rb:45-47`); silent-default (`nil` → contexto de negocio no-op) si no existe | restart (prod: memoizado `@current_class_cache`, `exis_ray.rb:78`) · per-request (dev: re-resuelto para Zeitwerk reload, `exis_ray.rb:80`) | boot-only en prod · mutable-singleton en dev | clase del host que provee `user_id`/`isp_id`/`correlation_id`; se pasa como String para evitar `uninitialized constant` en boot |
|
|
91
|
+
| `reporter_class` | orchestration | boot-crash @ after-initialize si no hereda de `ExisRay::Reporter` (`railtie.rb:50-52`); silent-default (`nil` → no se reporta a Sentry) | restart · per-request (idem `current_class`, `exis_ray.rb:95-97`) | boot-only en prod · mutable-singleton en dev | clase del host que envía errores a Sentry con trace context; consumida por TaskMonitor/middlewares al rescatar |
|
|
92
|
+
| `log_subscriber_class` | observability | silent-default (`nil` → `ExisRay::LogSubscriber` base sin `extra_fields`) | restart (`install!` en after_initialize, `log_subscriber.rb:295`) | boot-only | subclase para inyectar `extra_fields` en el log de cierre HTTP; **solo aplica si `json_logs?`** (ver §g) |
|
|
93
|
+
|
|
94
|
+
### f.3 Estrategia de logging (`log_format`)
|
|
95
|
+
|
|
96
|
+
| var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
|
|
97
|
+
|---|---|---|---|---|---|
|
|
98
|
+
| `log_format` | observability | silent-default (`:text` → tags de Rails, sin JsonFormatter) | restart (decisión tomada en boot tras `load_config_initializers`, `railtie.rb:28-29`) | boot-only | conmuta **toda** la estrategia: `:json` instala JsonFormatter global + LogSubscriber + formatter de Sidekiq (`railtie.rb:56-65,121`); `:text` solo agrega `root_id` como `log_tag`. **Ramificador** (§g). |
|
|
99
|
+
|
|
100
|
+
### f.4 Metadata de recurso OTel
|
|
101
|
+
|
|
102
|
+
| var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
|
|
103
|
+
|---|---|---|---|---|---|
|
|
104
|
+
| `service_version` | observability | silent-default (`nil` → campo `service_version` omitido del log) | per-request (emitido por línea, `json_formatter.rb:89`) | boot-only (computado una vez de Rails config) | equivale a `service.version` de OTel |
|
|
105
|
+
| `deployment_environment` | observability | silent-default (`nil` → campo omitido) | per-request (`json_formatter.rb:94`) | boot-only (derivado de `Rails.env`) | equivale a `deployment.environment` de OTel |
|
|
106
|
+
|
|
107
|
+
### f.5 Feature-flags de migración OTel (transitorios)
|
|
108
|
+
|
|
109
|
+
| var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
|
|
110
|
+
|---|---|---|---|---|---|
|
|
111
|
+
| `emit_legacy_exception_keys` | feature-flag | silent-default (`true` → emite `error_class`/`error_message` además de `exception.*`) | per-request en error (`log_subscriber.rb:156`, `task_monitor.rb:138`) | boot-only | flag transitorio ventana OTel v1.0. **Ramp:** partial (default `true`). **Cleanup:** cuando dashboards/alertas/queries migren a `exception.*`. Roadmap: default `false` en v0.12.0, removido en v1.0 |
|
|
112
|
+
| `emit_legacy_path_key` | feature-flag | silent-default (`true` → emite `path` además de `url.path`) | per-request (`log_subscriber.rb:115`) | boot-only | flag transitorio. **Ramp:** partial. **Cleanup:** cuando consumers migren a `url.path`. Roadmap: default `false` en v0.12.0, removido en v1.0 |
|
|
113
|
+
|
|
114
|
+
### f.6 Identidad de pod (`HOSTNAME`)
|
|
115
|
+
|
|
116
|
+
| var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
|
|
117
|
+
|---|---|---|---|---|---|
|
|
118
|
+
| `HOSTNAME` | infra | silent-default (`"local"` si ausente) | per-task (leído al generar root en TaskMonitor, `task_monitor.rb:81`) | boot-only (env del contenedor) | sufijo del hostname (post último `-`) como `pod_id` en el `root_id` de tasks; en K8s/Docker lo inyecta el orquestador |
|
|
119
|
+
|
|
120
|
+
**Threading (§h):** los callbacks RPC de BugBunny (`on_rpc_reply`, `railtie.rb:77-88`) corren en el **thread del publisher** y resuelven `reporter_class`/`current_class` vía los helpers memoizados. `ExisRay::Tracer`/`Current` heredan `ActiveSupport::CurrentAttributes` (thread/fiber-local). Constraint: el valor de config es un singleton global; las clases resueltas se comparten entre threads — safe porque son class objects inmutables tras boot.
|
|
121
|
+
|
|
122
|
+
**Ramificadores intra-config (§g):**
|
|
123
|
+
|
|
124
|
+
- `log_format=:json` (vía `json_logs?`) ramifica la aplicabilidad de **`log_subscriber_class`** (inerte en `:text` — `install!` solo corre en modo json) y dispara `JsonFormatter` global + `Sidekiq.logger.formatter` (`railtie.rb:56-65,121`).
|
|
125
|
+
- `emit_legacy_exception_keys` / `emit_legacy_path_key` ramifican **qué keys se emiten** en el log, no la aplicabilidad de otras vars.
|
|
126
|
+
|
|
127
|
+
## 3. Inferencias
|
|
128
|
+
|
|
129
|
+
| ítem | confidence | a verificar |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `HOSTNAME` clasificada `requerida=no` | declared | fallback `"local"` explícito en `task_monitor.rb:81` — confirmado |
|
|
132
|
+
| `service_version`/`deployment_environment` como `derived` | declared | defaults computados en `initialize` vía `default_*` — confirmado |
|
|
133
|
+
| sin secretos | declared | ninguna opción matchea `*_KEY|*_SECRET|*_PASS|*_TOKEN` ni literal sensible — confirmado |
|
|
134
|
+
| `reporter_class`/`current_class` aceptan String o Class | declared | YARD `@return [String, Class, nil]`; el `safe_constantize` del Railtie asume String — verificar uso real |
|
|
135
|
+
| `::BugBunny`/`::Sidekiq` sin ancla cross-repo (§j) | declared | son **integraciones opcionales** (guard `defined?`), no dependencias declaradas (ausentes de gemspec/Gemfile.lock) — la gema inyecta hooks si el host las tiene. Por eso NO van en `skills.yml`: no hay relación de consumo que anclar |
|
|
136
|
+
| `scope-override` boot-only vs mutable-singleton de `*_class` (§f.2) | inferred | depende de `cache_classes?` (`exis_ray.rb:118-128`): prod memoiza (boot-only), dev re-resuelve por request (mutable). Verificar que el host no mute `configuration.*_class` post-boot |
|
|
137
|
+
|
|
138
|
+
## 4. Cobertura y fronteras
|
|
139
|
+
|
|
140
|
+
- **Inventario base completo** para v0.11.0 — las 11 vars/opciones del código están listadas.
|
|
141
|
+
- **Enriquecimiento §f completo 11/11** — categoría · failure-mode · side-effect · scope-override · business-reason por opción, + threading (§h) y ramificadores (§g). Anclado al consumidor real (`file:line`); las inferencias quedan en §3 para verificación humana.
|
|
142
|
+
- **Mapeo §j → gema completo 2/2** — `propagation_trace_header`→BugBunny RPC headers; `log_format`→Sidekiq formatter. Ancla cross-repo de BugBunny pendiente (no declarada en `skills.yml`, ver §3).
|
|
143
|
+
- **Fuera de alcance:** `ENV["TENANT_ID"]` en `current.rb:22` es **ejemplo en comentario YARD** (no consumo real) → no se lista. Vars de Rails core (`SECRET_KEY_BASE`, `RAILS_ENV`, etc.) las aporta la app host, no esta gema.
|
|
144
|
+
- **`config.x.version`** (lectura de `service_version`) depende del host — la gema solo lee, no define.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Interfaz — exis_ray
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-004 (RBS firmas como modelo conceptual) · generado arch-structure · anclado a commit `b7b96c7` (v0.11.0) · cobertura **superficie pública consumer-facing completa**
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
API Ruby pública de la gema: módulo `ExisRay` (config + resolución de clases host), 2 bases abstractas que el host subclasea (`Current`, `Reporter`), el contexto de trazabilidad (`Tracer`), el formatter/subscriber de logs, el monitor de tasks, y 7 middlewares de propagación (HTTP/Faraday/ActiveResource/Sidekiq×2/BugBunny×2). Las clases configurables (`Configuration`) se documentan en detalle en [`docs/config/configuracion.md`](../config/configuracion.md).
|
|
8
|
+
|
|
9
|
+
## 2. Superficie pública (símbolo · tipo · firma · nota)
|
|
10
|
+
|
|
11
|
+
### `ExisRay` (módulo raíz)
|
|
12
|
+
|
|
13
|
+
| símbolo | tipo | firma | nota |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `ExisRay::VERSION` | const | `String` | `"0.11.0"` (`version.rb:5`) |
|
|
16
|
+
| `ExisRay::Error` | class | `< StandardError` | error base de la gema (`exis_ray.rb:36`) |
|
|
17
|
+
| `ExisRay.configure` | method | `() { (Configuration) -> void } -> void` | bloque de configuración (`exis_ray.rb:60`) |
|
|
18
|
+
| `ExisRay.configuration` | method | `() -> Configuration` | instancia singleton (`exis_ray.rb:46`) |
|
|
19
|
+
| `ExisRay.configuration=` | writer | `(Configuration) -> void` | reemplaza la config — uso en tests (`exis_ray.rb:40`) |
|
|
20
|
+
| `ExisRay.current_class` | method | `() -> Class?` | resuelve `config.current_class`; memoizado en prod, re-resuelto en dev (`exis_ray.rb:71`) |
|
|
21
|
+
| `ExisRay.reporter_class` | method | `() -> Class?` | resuelve `config.reporter_class` (`exis_ray.rb:88`) |
|
|
22
|
+
| `ExisRay.sync_correlation_id` | method | `() -> void` | copia `Tracer.correlation_id` → `Current.correlation_id` (`exis_ray.rb:105`) |
|
|
23
|
+
|
|
24
|
+
### `ExisRay::Configuration` (clase)
|
|
25
|
+
|
|
26
|
+
| símbolo | tipo | firma | nota |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| `Configuration#<attr>` | accessor (rw) | — | 10 opciones (`trace_header`, `log_format`, `current_class`, ...) — inventario y semántica en [`docs/config/`](../config/configuracion.md) |
|
|
29
|
+
| `Configuration#json_logs?` | method | `() -> bool` | `true` si `log_format == :json` (`configuration.rb:122`) |
|
|
30
|
+
|
|
31
|
+
### `ExisRay::Tracer` (`< ActiveSupport::CurrentAttributes`)
|
|
32
|
+
|
|
33
|
+
| símbolo | tipo | firma | nota |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `Tracer.<attr>` | accessor (rw) | — | `trace_id · request_id · root_id · self_id · called_from · total_time_so_far · created_at · sidekiq_job · source · task` (`tracer.rb:15`) |
|
|
36
|
+
| `Tracer.hydrate` | method | `(trace_id:, source:) -> void` | inicializa el contexto desde header entrante (`tracer.rb:78`) |
|
|
37
|
+
| `Tracer.generate_trace_header` | method | `() -> String` | header para propagar al siguiente servicio (`tracer.rb:115`) |
|
|
38
|
+
| `Tracer.parse_trace_id` | method | `() -> void` | extrae `root_id`/`self_id`/`called_from`/`total_time_so_far` del `trace_id` (`tracer.rb:40`) |
|
|
39
|
+
| `Tracer.service_name` | method | `() -> String` | nombre del servicio (de `Rails.application`) (`tracer.rb:22`) |
|
|
40
|
+
| `Tracer.correlation_id` | method | `() -> String` | compuesto `service_name;root_id` (`tracer.rb:32`) |
|
|
41
|
+
| `Tracer.current_duration_s` | method | `() -> Float` | segundos desde `created_at` (monotónico) (`tracer.rb:89`) |
|
|
42
|
+
| `Tracer.current_duration_ms` | method | `() -> Integer` | milisegundos desde `created_at` (`tracer.rb:67`) |
|
|
43
|
+
| `Tracer.format_duration` | method | `(Float seconds) -> String` | formato humano (`"7.0ms"`, `"2 minutes 5 seconds"`) (`tracer.rb:102`) |
|
|
44
|
+
|
|
45
|
+
### `ExisRay::Current` (`< ActiveSupport::CurrentAttributes`, base abstracta — el host subclasea)
|
|
46
|
+
|
|
47
|
+
| símbolo | tipo | firma | nota |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `Current.<attr>` | accessor (rw) | — | `user_id · isp_id · correlation_id` (`current.rb:9`) |
|
|
50
|
+
| `Current.user_id=` | setter | `(Integer?) -> void` | usa `!nil?` (acepta `0`) + sincroniza PaperTrail/ActiveResource (`current.rb:54`) |
|
|
51
|
+
| `Current.isp_id=` | setter | `(Integer?) -> void` | ídem (`current.rb:63`) |
|
|
52
|
+
| `Current.correlation_id=` | setter | `(String?) -> void` | propaga a Session/Reporter (`current.rb:71`) |
|
|
53
|
+
| `Current.user=` / `Current.user` | accessor | `(obj) -> void` / `() -> User?` | asigna `user_id` de `obj.id` / lazy-load memoizado (`current.rb:84,88`) |
|
|
54
|
+
| `Current.isp=` / `Current.isp` | accessor | `(obj) -> void` / `() -> Isp?` | ídem (`current.rb:95,99`) |
|
|
55
|
+
| `Current.user?` / `Current.isp?` | predicate | `() -> bool` | true si el id `!nil?` (`current.rb:106,110`) |
|
|
56
|
+
| `Current.correlation_id?` | predicate | `() -> bool` | true si `present?` (`current.rb:116`) |
|
|
57
|
+
| `Current.log_fields` | hook | `() -> Hash` | **override** para inyectar campos custom en cada log; default `{}` (`current.rb:31`) |
|
|
58
|
+
|
|
59
|
+
### `ExisRay::Reporter` (`< ActiveSupport::CurrentAttributes`, base abstracta — el host subclasea)
|
|
60
|
+
|
|
61
|
+
| símbolo | tipo | firma | nota |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `Reporter.report` | method | `(String message, context: {}, tags: {}, fingerprint: [], transaction_name: nil) -> void` | reporta mensaje a Sentry (`reporter.rb:24`) |
|
|
64
|
+
| `Reporter.exception` | method | `(Exception excep, context: {}, tags: {}, fingerprint: [], transaction_name: nil) -> void` | reporta excepción (`reporter.rb:33`) |
|
|
65
|
+
| `Reporter.add_context` | method | `(Hash attrs) -> void` | acumula contexto (`reporter.rb:58`) |
|
|
66
|
+
| `Reporter.add_tags` | method | `(Hash attrs) -> void` | acumula tags (`reporter.rb:64`) |
|
|
67
|
+
| `Reporter.add_fingerprint` | method | `(value) -> void` | acumula fingerprint (`reporter.rb:52`) |
|
|
68
|
+
| `Reporter.build_custom_context` | hook | `() -> void` | **override** para contexto específico del servicio (`reporter.rb:70`) |
|
|
69
|
+
| `Reporter.sentry_user_context` | hook | `(current) -> Hash` | **override**; default `{ id: user_id }` (`reporter.rb:196`) |
|
|
70
|
+
| `Reporter.sentry_isp_context` | hook | `(current) -> Hash` | **override** (`reporter.rb:205`) |
|
|
71
|
+
|
|
72
|
+
### `ExisRay::JsonFormatter` (`< ::Logger::Formatter`)
|
|
73
|
+
|
|
74
|
+
| símbolo | tipo | firma | nota |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `JsonFormatter#call` | method | `(severity, timestamp, progname, msg) -> String` | interfaz `Logger::Formatter`; emite JSON single-line con contexto inyectado (`json_formatter.rb:49`) |
|
|
77
|
+
|
|
78
|
+
### `ExisRay::LogSubscriber` (`< ActiveSupport::LogSubscriber`)
|
|
79
|
+
|
|
80
|
+
| símbolo | tipo | firma | nota |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `LogSubscriber.install!` | method | `() -> void` | suscribe y suprime los subscribers default de Rails (`log_subscriber.rb:68`) |
|
|
83
|
+
| `LogSubscriber.attached?` | method | `() -> bool` | si ya está instalado (`log_subscriber.rb:76`) |
|
|
84
|
+
| `LogSubscriber.extra_fields` | hook | `(event) -> Hash` | **override** para campos HTTP extra; default `{}` (`log_subscriber.rb:58`) |
|
|
85
|
+
| `LogSubscriber#process_action` | method | `(event) -> void` | interfaz subscriber; emite el log de cierre de request (`log_subscriber.rb:33`) |
|
|
86
|
+
|
|
87
|
+
### `ExisRay::TaskMonitor` (módulo)
|
|
88
|
+
|
|
89
|
+
| símbolo | tipo | firma | nota |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `TaskMonitor.run` | method | `(String task_name) { () -> void } -> void` | genera `root_id`, loguea `task_started`/`task_finished`, re-lanza excepciones, resetea contexto (`task_monitor.rb:16`) |
|
|
92
|
+
|
|
93
|
+
### Middlewares de propagación
|
|
94
|
+
|
|
95
|
+
| símbolo | tipo | firma | nota |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `ExisRay::HttpMiddleware` | class (Rack) | `#initialize(app)` · `#call(env) -> Array` | auto-insertado tras `ActionDispatch::RequestId` (`http_middleware.rb`) |
|
|
98
|
+
| `ExisRay::FaradayMiddleware` | class (`< Faraday::Middleware`) | `#call(env)` | **manual** en el stack Faraday (`faraday_middleware.rb`) |
|
|
99
|
+
| `ExisRay::ActiveResourceInstrumentation` | module (prepend) | `#headers -> Hash` | auto-prepend a `ActiveResource::Base` (`active_resource_instrumentation.rb`) |
|
|
100
|
+
| `ExisRay::Sidekiq::ClientMiddleware` | class | `#call(worker_class, job, queue, redis_pool = nil) { }` | auto-registrado (client) (`sidekiq/client_middleware.rb`) |
|
|
101
|
+
| `ExisRay::Sidekiq::ServerMiddleware` | class | `#call(worker, job, queue) { }` | auto-registrado (server) (`sidekiq/server_middleware.rb`) |
|
|
102
|
+
| `ExisRay::BugBunny::PublisherTracing` | class (`< ::BugBunny::Middleware::Base`) | `#on_request(env)` | **manual** en el cliente (`bug_bunny/publisher_tracing.rb`) |
|
|
103
|
+
| `ExisRay::BugBunny::ConsumerTracingMiddleware` | class (`< ::BugBunny::ConsumerMiddleware::Base`) | `#call(delivery_info, properties, body)` | auto-registrado (consumer) (`bug_bunny/consumer_tracing_middleware.rb`) |
|
|
104
|
+
|
|
105
|
+
## 3. Inferencias
|
|
106
|
+
|
|
107
|
+
| ítem | confidence | a verificar |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `Tracer`/`Reporter`/`TaskMonitor` no declaran `private` | declared | métodos helper (`generate_new_root`, `clean_request_id`, `build_from_*`, `*_to_old/new_sentry`, `setup_tracer`, ...) quedan **reachable** Ruby-wise pero NO son contrato — ver §4 (Hyrum's Law) |
|
|
110
|
+
| accesores de `Current`/`Tracer`/`Reporter` como métodos de clase | declared | son `ActiveSupport::CurrentAttributes` → los `attribute :x` exponen `.x`/`.x=` a nivel de clase, thread/fiber-local |
|
|
111
|
+
| middlewares "manual" vs "auto" | declared | Faraday y BugBunny-publisher requieren registro manual; el resto los inyecta el Railtie (ver `docs/config` §i) |
|
|
112
|
+
|
|
113
|
+
## 4. Cobertura y fronteras
|
|
114
|
+
|
|
115
|
+
- **Superficie consumer-facing completa** para v0.11.0 — lo que un host app usa o subclasea.
|
|
116
|
+
- **Helpers públicos-Ruby pero NO contrato:** `Tracer.generate_new_root` / `.clean_request_id`; `Reporter.build_from_tracer` / `.build_from_current` / `.*_to_old_sentry` / `.*_to_new_sentry` / `.session_*`; `TaskMonitor.setup_tracer` / `.pod_identifier` / `.log_event` / `.format_*`; helpers privados de `Current` (`assign_session_request_id`, `sync_reporter_correlation_id`, `sanitize_header_value` — sí marcados `private`, `current.rb:120`). Reachable por falta de `private` (salvo Current), pero son detalle de implementación; cambiar su firma no es breaking de contrato.
|
|
117
|
+
- **Internos del formatter/subscriber:** los `inject_*`/`extract_*`/`parse_*` de `JsonFormatter` y `LogSubscriber` son privados de implementación — el contrato es `#call` / `#process_action` + los hooks de override.
|
|
118
|
+
- **Constantes:** `JsonFormatter::SENSITIVE_KEYS`, `::SEVERITY_NUMBER`, `::KV_DETECT_RE` son detalle interno, no contrato.
|
|
119
|
+
- **Configuración:** el shape de `Configuration` (defaults, tipos, failure-modes) vive en [`docs/config/configuracion.md`](../config/configuracion.md) — acá solo se lista la clase y `json_logs?`.
|
|
120
|
+
- **Comportamiento runtime** (secuencias de hidratación/emisión) → [`docs/behavior/behavior.md`](../behavior/behavior.md); **significado de términos** → [`docs/glossary/glossary.md`](../glossary/glossary.md).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Test — exis_ray
|
|
2
|
+
|
|
3
|
+
> meta: artefacto test · RFC-013 (shape v2.1, `accepted`) · generado
|
|
4
|
+
> manualmente (**piloto RFC-013** — suplemento de gema para validar §j/§k;
|
|
5
|
+
> predata el soporte de `test` en `arch-structure`, futura regeneración vía
|
|
6
|
+
> esa skill) · anclado a `spec/` · fecha 2026-06-01 · cobertura: completa para los
|
|
7
|
+
> componentes aislados; gap real en inyección del Railtie (sin dummy app) y
|
|
8
|
+
> matrix de versiones (CI mono-versión).
|
|
9
|
+
|
|
10
|
+
## 1. Resumen
|
|
11
|
+
|
|
12
|
+
Estrategia y cobertura de test de la gema (Railtie de observabilidad). Stack:
|
|
13
|
+
**RSpec**, 8 suites / **176** examples, **sin SimpleCov**. Cada componente se
|
|
14
|
+
testea **aislado con mocks de Rails** (`stub_const`), no contra una app real.
|
|
15
|
+
Filosofía **test-last + FDD**. Este artefacto es el **suplemento gema** del
|
|
16
|
+
piloto RFC-013: ejercita §j (inyección al host + matrix) y confirma que §k
|
|
17
|
+
(concurrencia) es **n/a** acá.
|
|
18
|
+
|
|
19
|
+
## 2. Cuerpo
|
|
20
|
+
|
|
21
|
+
### a. Hecho verificable
|
|
22
|
+
|
|
23
|
+
- **Suites:** 8 / **176** examples (todos unit aislados).
|
|
24
|
+
- **Modo:** `railtie`/`library` (no http/worker — es gema).
|
|
25
|
+
- **Factories:** 0 (Structs/doubles inline).
|
|
26
|
+
- **Regresiones post-incidente:** 2 (#9, #12) — **con** comentario de origen
|
|
27
|
+
(§h, convención ya practicada).
|
|
28
|
+
- **% coverage:** `n/a (sin tooling)`.
|
|
29
|
+
- **Inyecciones al host testeadas:** 0 de ~7 (§j) — sin dummy app.
|
|
30
|
+
- **Matrix:** declarado Ruby ≥2.6 / Rails ≥6; testeado CI **solo 3.4.4** (§j).
|
|
31
|
+
- **Tests de concurrencia:** 0 (§k n/a).
|
|
32
|
+
|
|
33
|
+
### b. Estrategia / pirámide
|
|
34
|
+
|
|
35
|
+
Todo **unit aislado**: cada clase (`Tracer`, `HttpMiddleware`, `JsonFormatter`,
|
|
36
|
+
`LogSubscriber`, `Configuration`, `Current`, `Reporter`, `TaskMonitor`) se
|
|
37
|
+
testea sola, mockeando `Rails`/`Logger`/`ActionDispatch`. Sin nivel
|
|
38
|
+
integration/system (no hay app real). Mapeo: la gema no mapea a capas de
|
|
39
|
+
servicio — los componentes son su propia superficie (RFC-004 interfaz).
|
|
40
|
+
Momento: **test-last**.
|
|
41
|
+
|
|
42
|
+
### c. Inventario de suites
|
|
43
|
+
|
|
44
|
+
| Suite | Nivel | Modo | Nº specs |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `spec/exis_ray/json_formatter_spec.rb` | unit | library | 54 |
|
|
47
|
+
| `spec/exis_ray/log_subscriber_spec.rb` | unit | railtie | 29 |
|
|
48
|
+
| `spec/exis_ray/tracer_spec.rb` | unit | library | 27 |
|
|
49
|
+
| `spec/exis_ray/configuration_spec.rb` | unit | library | 27 |
|
|
50
|
+
| `spec/exis_ray/current_spec.rb` | unit | library | 16 |
|
|
51
|
+
| `spec/exis_ray/reporter_spec.rb` | unit | library | 8 |
|
|
52
|
+
| `spec/exis_ray/task_monitor_spec.rb` | unit | library | 8 |
|
|
53
|
+
| `spec/exis_ray/http_middleware_spec.rb` | unit | railtie | 7 |
|
|
54
|
+
|
|
55
|
+
Invocación: `bundle exec rake` (RSpec + RuboCop), ~1.2s.
|
|
56
|
+
|
|
57
|
+
### d. Cobertura declarada
|
|
58
|
+
|
|
59
|
+
| Flujo crítico | Cubierto? | Spec | Fuente del gap |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| Trace propagation (parse X-Ray headers, root_id) | sí | `spec/exis_ray/tracer_spec.rb`, `http_middleware_spec.rb` | — |
|
|
62
|
+
| JSON log formatting + OTel + filtrado sensible | sí | `spec/exis_ray/json_formatter_spec.rb` | — |
|
|
63
|
+
| LogSubscriber (HTTP status → log level) | sí | `spec/exis_ray/log_subscriber_spec.rb` | — |
|
|
64
|
+
| **Inyección del Railtie en app real** | **no** | — (sin `spec/dummy/`) | inferido — componentes testeados aislados; la inyección (`lib/exis_ray/railtie.rb`) no se ejercita (§j) |
|
|
65
|
+
|
|
66
|
+
`% coverage: n/a`. Gap nominal: la **inyección** (lo que distingue a una gema
|
|
67
|
+
Railtie) no tiene test — solo la lógica de cada componente por separado.
|
|
68
|
+
|
|
69
|
+
### e. Fixtures / factories / test data
|
|
70
|
+
|
|
71
|
+
Sin FactoryBot, sin fixtures, sin PII. Datos sintéticos inline: Structs
|
|
72
|
+
(`build_event`, `build_route` en `log_subscriber_spec.rb`), `stub_const` de
|
|
73
|
+
`Rails`/`Tracer`/`Logger`. n/a la columna entidad (gema sin capa de datos).
|
|
74
|
+
|
|
75
|
+
### f. Topología de integration/system tests
|
|
76
|
+
|
|
77
|
+
**n/a** — no se levanta nada real (sin DB/broker/app). Todo mockeado:
|
|
78
|
+
`Rails`, `Rails.application`, `Logger`, `ActionDispatch::*` vía `stub_const`/
|
|
79
|
+
`instance_double`. Estado requerido para correr: **ninguno** (solo
|
|
80
|
+
`activesupport`). No es el sub-caso "gema-envuelve-infra" (exis_ray no envuelve
|
|
81
|
+
infra externa; instrumenta el host).
|
|
82
|
+
|
|
83
|
+
### g. Contract tests (cross RFC-018)
|
|
84
|
+
|
|
85
|
+
**n/a** — la gema no **consume** servicios (no tiene `docs/consumed/`). Nota: sí
|
|
86
|
+
**inyecta** hooks a otras gemas (`BugBunny.consumer_middlewares`, Sidekiq
|
|
87
|
+
client/server middleware, `ActiveResource::Base.prepend` —
|
|
88
|
+
`lib/exis_ray/railtie.rb:71,96,116`), pero **ninguno tiene test** (cae en §j,
|
|
89
|
+
no en §g — es inyección, no consumo).
|
|
90
|
+
|
|
91
|
+
### h. Tests de no-regresión / nacidos de incidentes
|
|
92
|
+
|
|
93
|
+
| Test | Incidente | Qué reproduce |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `spec/exis_ray/json_formatter_spec.rb:410` `describe "...(issue #9)"` | issue #9 | inyección de contexto de tracer; root_id nil pero request_id presente (Gap C) |
|
|
96
|
+
| `spec/exis_ray/json_formatter_spec.rb:466` `describe "...(issue #12)"` | issue #12 | dedup de claves Symbol/String (no duplicar en JSON) |
|
|
97
|
+
|
|
98
|
+
- **Convención §h ya practicada** ✅ — exis_ray ancla el incidente en el
|
|
99
|
+
`describe`. Valida que la convención v2.1 es factible y de bajo costo. (Forma
|
|
100
|
+
observada: `(issue #N)` en el describe; la norma sugiere `# Regresión: #N` en
|
|
101
|
+
comentario — ambas cumplen el objetivo de link recuperable sin `git blame`.)
|
|
102
|
+
|
|
103
|
+
### i. Ejecución, gates y flaky
|
|
104
|
+
|
|
105
|
+
- **Ejecución:** `bundle exec rake` (RSpec + RuboCop), ~1.2s. Sin gates de
|
|
106
|
+
suite (no hay integration que excluir).
|
|
107
|
+
- **Flaky:** 0 (176 pasan consistentes, sin `sleep`/`Thread`).
|
|
108
|
+
- **CI:** `.github/workflows/main.yml` — corre la suite, pero ver §j (matrix).
|
|
109
|
+
|
|
110
|
+
### j. Inyección al host + matrix de versiones
|
|
111
|
+
|
|
112
|
+
**Sección de alto valor para esta gema** (motivó §j en v2.1).
|
|
113
|
+
|
|
114
|
+
**Inyección al host** — `lib/exis_ray/railtie.rb` inyecta ~7 cosas; **ninguna
|
|
115
|
+
testeada en app real** (no hay `spec/dummy/`):
|
|
116
|
+
|
|
117
|
+
| Inyección | file:line | Testeada? |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `HttpMiddleware` (`insert_after ActionDispatch::RequestId`) | `railtie.rb:17` | **sin-test** de inyección (lógica del middleware sí, aislada — `mock-rails`) |
|
|
120
|
+
| `config.log_tags` (request_id proc) | `railtie.rb:31` | sin-test |
|
|
121
|
+
| `Rails.logger.formatter = JsonFormatter.new` | `railtie.rb:57` | sin-test (formatter sí, aislado) |
|
|
122
|
+
| `LogSubscriber` (subscribe a ActionController) | `railtie.rb:61` | sin-test (subscriber sí, aislado con `mock-rails`) |
|
|
123
|
+
| `BugBunny.consumer_middlewares.use` | `railtie.rb:71` | **sin-test** |
|
|
124
|
+
| `ActiveResource::Base.prepend` instrumentation | `railtie.rb:96` | **sin-test** |
|
|
125
|
+
| Sidekiq client/server middleware | `railtie.rb:107,116` | **sin-test** |
|
|
126
|
+
|
|
127
|
+
Gap: lo que **define** a la gema (inyectar al host) es lo único sin test.
|
|
128
|
+
Cruza [`docs/config/configuracion.md`](../config/configuracion.md) §i (mismo
|
|
129
|
+
inventario de inyecciones del Railtie, lado config). Un `spec/dummy/` cerraría
|
|
130
|
+
el gap.
|
|
131
|
+
|
|
132
|
+
**Matrix de versiones:**
|
|
133
|
+
|
|
134
|
+
| | Valor |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Declarado (gemspec) | Ruby `>= 2.6.0` · activesupport/railties `>= 6.0` (`exis_ray.gemspec:15,38`) |
|
|
137
|
+
| Testeado (CI) | **solo Ruby 3.4.4** (`.github/workflows/main.yml:17`) — sin Appraisal, sin matrix de Rails |
|
|
138
|
+
| Gap | rango Ruby 2.6–3.3 y Rails 6–7 **declarado pero no ejercitado** |
|
|
139
|
+
|
|
140
|
+
### k. Tests de concurrencia / async
|
|
141
|
+
|
|
142
|
+
**n/a** — exis_ray no tiene threads ni callbacks en reader-thread. Confirma que
|
|
143
|
+
§k es **específica de gemas que gestionan concurrencia** (`bug_bunny`), no
|
|
144
|
+
universal. (Input para §5 de la RFC: §k aplicó a 1 de 4 repos validados →
|
|
145
|
+
evaluar si va como sección top-level o sub-bloque condicional.)
|
|
146
|
+
|
|
147
|
+
## 3. Inferencias
|
|
148
|
+
|
|
149
|
+
- "Inyección sin test" se infiere de la ausencia de `spec/dummy/` + presencia
|
|
150
|
+
de hooks en `railtie.rb`. No medido (sin coverage).
|
|
151
|
+
|
|
152
|
+
## 4. Cobertura y fronteras
|
|
153
|
+
|
|
154
|
+
- Cobertura completa de los **componentes aislados**; gap en la **integración
|
|
155
|
+
Railtie↔Rails real** y en la **matrix de versiones**. Ambos son los hallazgos
|
|
156
|
+
que §j hace visibles — el valor del artefacto para una gema.
|
|
@@ -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.
|
|
@@ -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")
|
|
@@ -47,9 +47,7 @@ module ExisRay
|
|
|
47
47
|
log_event(:error,
|
|
48
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,11 +11,14 @@ 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.11.1** (capas de doc config/interface + Mapa de conocimiento; sin cambios de código desde v0.11.0):
|
|
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`, ...).
|
|
18
|
-
-
|
|
18
|
+
- [`docs/config/configuracion.md`](../docs/config/configuracion.md) — inventario de las 10 opciones de `ExisRay.configure` + `HOSTNAME` + inyecciones del Railtie al host (enriquecimiento semántico §f pendiente).
|
|
19
|
+
- [`docs/test/testing.md`](../docs/test/testing.md) — mapa de suites RSpec + fixtures + coverage.
|
|
20
|
+
- [`docs/interface/interface.md`](../docs/interface/interface.md) — superficie Ruby pública (símbolo · tipo · firma · nota); el detalle de uso de cada símbolo permanece abajo.
|
|
21
|
+
- Datos · Operaciones · Eventos · Consumidas = `n/a` (gema sin DB, no expone superficie propia ni consume servicios). Topología/Release/Errores = **pendiente**. Ver mapa de cobertura completo en `AGENTS.md`.
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
@@ -80,7 +83,7 @@ ExisRay unifica trazabilidad distribuida, logging estructurado JSON, contexto de
|
|
|
80
83
|
3. `ExisRay.sync_correlation_id` asigna `Tracer.correlation_id` a `Current.correlation_id`
|
|
81
84
|
4. Controller ejecuta `before_action` para setear `Current.user_id`, `Current.isp_id`
|
|
82
85
|
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`, `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
|
|
86
|
+
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
87
|
7. En llamadas salientes, `FaradayMiddleware`/`ActiveResourceInstrumentation` inyectan `propagation_trace_header` con `Tracer.generate_trace_header`
|
|
85
88
|
8. Al finalizar, `ActiveSupport::CurrentAttributes` hace reset automatico
|
|
86
89
|
|
|
@@ -100,6 +103,8 @@ ExisRay.configure do |config|
|
|
|
100
103
|
config.log_subscriber_class = "MyLogSubscriber" # String|nil, default nil
|
|
101
104
|
config.service_version = "1.2.3" # String|nil, default: Rails config.version o config.x.version
|
|
102
105
|
config.deployment_environment = "production" # String|nil, default: Rails.env
|
|
106
|
+
config.emit_legacy_exception_keys = true # Boolean, default true (ventana transicion OTel v1.0)
|
|
107
|
+
config.emit_legacy_path_key = true # Boolean, default true (ventana transicion `path` -> `url.path` OTel v1.0)
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
ExisRay.configuration.json_logs? # => true si log_format == :json
|
|
@@ -233,8 +238,9 @@ ExisRay::TaskMonitor.run("billing:generate_invoices") do
|
|
|
233
238
|
InvoiceService.process_all
|
|
234
239
|
end
|
|
235
240
|
# Genera root_id propio, loguea task_started/task_finished con outcome y duration_s.
|
|
236
|
-
# En caso de error emite:
|
|
237
|
-
#
|
|
241
|
+
# En caso de error emite: exception.type, exception.message, exception.stacktrace
|
|
242
|
+
# (OTel, limitado a 20 lineas) + error_class/error_message si
|
|
243
|
+
# config.emit_legacy_exception_keys es true (default durante ventana de transicion).
|
|
238
244
|
# Re-lanza excepciones despues de loguearlas.
|
|
239
245
|
# Hace reset de Tracer, Current y Reporter en ensure.
|
|
240
246
|
```
|
|
@@ -250,8 +256,9 @@ Reemplaza Lograge. Se suscribe a `process_action.action_controller` y emite un H
|
|
|
250
256
|
| `component` | String | Siempre `"exis_ray"` |
|
|
251
257
|
| `event` | String | Siempre `"http_request"` |
|
|
252
258
|
| `method` | String | Verbo HTTP |
|
|
253
|
-
| `path` | String | URL concreta del request |
|
|
254
|
-
| `
|
|
259
|
+
| `url.path` | String | URL concreta del request (OTel v1.0). Siempre presente |
|
|
260
|
+
| `path` | String | Alias legacy de `url.path`. Solo si `config.emit_legacy_path_key` (default `true`, deprecado) |
|
|
261
|
+
| `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
262
|
| `format` | Symbol/String | `html`, `json`, etc. |
|
|
256
263
|
| `controller` | String | Class name del controller |
|
|
257
264
|
| `action` | String | Nombre del action |
|
|
@@ -262,7 +269,7 @@ Reemplaza Lograge. Se suscribe a `process_action.action_controller` y emite un H
|
|
|
262
269
|
| `db_runtime_s` | Float\|nil | Solo si ActiveRecord lo reporta |
|
|
263
270
|
| `user_agent_original` | String | Header `User-Agent` |
|
|
264
271
|
| `server_address` | String | Hostname sin puerto (de `Host` header) |
|
|
265
|
-
| `error_class`, `error_message` | String | Solo en fallo (
|
|
272
|
+
| `error_class`, `error_message` | String | Solo en fallo y si `config.emit_legacy_exception_keys` (default `true`, deprecadas) |
|
|
266
273
|
| `exception.type`, `exception.message`, `exception.stacktrace` | String | Solo en fallo (OTel; stack limitado a 20 lineas) |
|
|
267
274
|
|
|
268
275
|
Para inyectar campos extra, sobreescribir `extra_fields`:
|