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.
@@ -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
- # Emite tanto los campos legacy (`error_class`/`error_message`) como los OTel
133
- # (`exception.type`/`exception.message`/`exception.stacktrace`) durante la ventana
134
- # de transición definida en el plan "Breaking changes OTel v1.0".
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 con keys symbol.
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
- "error_class=#{e.class} error_message=#{e.message.inspect} " \
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.10.0"
5
+ VERSION = "0.11.1"
6
6
  end
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.10.0** (post fix issues #9, #11, #12):
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
- - 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).
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`/`exception.*`).
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: error_class, error_message (legacy) + exception.type,
237
- # exception.message, exception.stacktrace (OTel, limitado a 20 lineas).
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
- | `http_route` | String | Template (ej: `/users/:id`). Baja cardinalidad para dashboards |
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 (legacy) |
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`: