exis_ray 0.5.11 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38ac0186226043d69ad60406f9aa32045a8d1192e223322735c76773a1fd41dd
4
- data.tar.gz: 5a37831fc8b34237723ca0ab929c79d469fe316241d97b859272f25b8349e6b6
3
+ metadata.gz: 2b8dea5e0b236f90177ce536cf1226b37b21c778ee6891dc0276756f9b972fc8
4
+ data.tar.gz: a8409a1e92f2494535f9e0512f1b47e8cc0bd841335e9b98d6b19ce20ebab08d
5
5
  SHA512:
6
- metadata.gz: a7f0c1f2e1629f593fd3af2608678568fae5890cd2eb6ed19450364e2a353725dd730baa4cc665498695d0062147728b68d45f3f0485acce93a2e370a956442d
7
- data.tar.gz: 84dc5252d489b2e28f5b9909d3928b49b8dfdc176019f4e2dcb066a7e25c1da460752625aca61b2be5c404c2eb7187da813de3fc34f02ba447a8e29902800129
6
+ metadata.gz: 3ad2bcc7d459c05efefd0e30b0701be99cf82477e843f233b4f62c36e56c4ba183a53d6097e6f461dd253bf07a8776da093c59396cbe0fc16948a0b0db813cc1
7
+ data.tar.gz: 297abed5f6adf6a799dd1b6a329833bd1433e1c7383aa364081677025f64b63a05d5b23fc6ae96bb2db0fa6a72d0544622a2820580568429f6433702d8aed0a6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.6.1] - 2026-04-05
2
+
3
+ ### Added
4
+ - **`exception.type` / `exception.message` / `exception.stacktrace`** emitidos junto a los legacy `error_class` / `error_message` en `TaskMonitor` y `LogSubscriber`. Inicia la transición a la convención OTel — los legacy se removerán en v1.0. El stacktrace se toma de `payload[:exception_object]` y se limita a las primeras 20 líneas para respetar el formato one-line KV del estándar Wispro.
5
+ - **`http_route`** en `LogSubscriber` con resolución en capas: (1) `payload[:request].route_uri_pattern` para Rails 7.1+, (2) fallback iterando `Rails.application.routes.routes` y matcheando por `defaults[:controller]`, `defaults[:action]` y verb HTTP. Normaliza el controller name CamelCase (incluyendo namespaces `Api::V1::Users`) al formato snake_case que Rails usa en `route.defaults`.
6
+
7
+ ### Fixed
8
+ - **`TaskMonitor.format_stacktrace`**: maneja `e.backtrace == nil` de forma segura y está envuelto en rescue para que un logger roto nunca afecte el flujo principal.
9
+ - **`private_class_method` restaurado** en `TaskMonitor`: `pod_identifier`, `setup_tracer`, `execute_with_optional_tags`, `log_event` y el nuevo `format_stacktrace` vuelven a ser privados (regresión de 0.6.0).
10
+ - **`extract_exception_data`**: las keys ahora son symbols (`:error_class`, `:"exception.type"`, etc) para ser consistentes con el resto del payload. Antes mezclaba strings y symbols, lo que causaba inconsistencias al serializar.
11
+ - **`extract_http_route`**: la implementación anterior matcheaba contra `route.requirements[:controller]`, pero Rails guarda controller/action en `route.defaults`. El método antes siempre retornaba `nil` en producción, aunque sus tests pasaban por stubear un contrato inventado. Tests reescritos para stubear la API real (`route.defaults`, `route.path.spec`, `route.verb`).
12
+
1
13
  ## [0.5.11] - 2026-04-04
2
14
 
3
15
  ### Documentación
data/CLAUDE.md CHANGED
@@ -4,9 +4,14 @@
4
4
 
5
5
  ExisRay es la capa de observabilidad y trazabilidad distribuida del ecosistema Wispro. Se integra con Rails para emitir logs estructurados en JSON, propagar trace context entre servicios (HTTP, Sidekiq, RabbitMQ), y mantener identidad de negocio (user_id, isp_id, correlation_id) en cada línea de log.
6
6
 
7
- El estándar de logging que implementa está definido en `MANIFEST.md`. Ese documento es la fuente de verdad — cualquier duda sobre formato, campos, o semántica de niveles se resuelve ahí.
7
+ El estándar de logging que implementa está definido en `skill/SKILL.md` (API, arquitectura, reglas generales) y `skill/references/standard.md` (Data First, mapeo OpenTelemetry, ciclo de vida). Son la fuente de verdad — cualquier duda sobre formato, campos, o semántica de niveles se resuelve ahí.
8
+
9
+ ## Documentación
10
+
11
+ - **Para humanos**: `docs/` (5 archivos) + `README.md`. Ver README para índice.
12
+ - **Para agentes AI**: `skill/SKILL.md` + `skill/references/`. Es la skill empaquetada que otros proyectos consumen via `skill-manager sync`.
13
+ - **Nunca referenciar `skill/` desde `docs/` o `README.md`** — son audiencias distintas.
8
14
 
9
- ---
10
15
  ## Knowledge Base
11
16
  - Las skills en `.agents/skills/` incluyen conocimiento de dependencias.
12
17
  - Leer la skill de una dependencia ANTES de responder sobre ella.
@@ -34,8 +39,8 @@ El estándar de logging que implementa está definido en `MANIFEST.md`. Ese docu
34
39
  - Todo código nuevo debe tener tests.
35
40
 
36
41
  ### Releases o Nuevas versiones
37
- - Gemas: `/gem-release`
38
- - Servicios: `/service-release build` o `/service-release deploy`
42
+ - Usar `/gem-release` para publicar nuevas versiones.
43
+ - El GitHub Action publica a RubyGems automáticamente al pushear un tag `v*`.
39
44
 
40
45
  ---
41
46
 
@@ -96,7 +101,10 @@ ExisRay.configuration.json_logs? # => true/false
96
101
  |:------|:----------|
97
102
  | `time` | Siempre |
98
103
  | `level` | Siempre |
104
+ | `severity_number` | Siempre (OTel: DEBUG=5, INFO=9, WARN=13, ERROR=17, FATAL=21) |
99
105
  | `service` | Siempre |
106
+ | `service_version` | Siempre (de `config.version` o `config.x.version`) |
107
+ | `deployment_environment` | Siempre (de `Rails.env`) |
100
108
  | `root_id` | Cuando hay trace context activo |
101
109
  | `trace_id` | Cuando hay trace context activo |
102
110
  | `source` | Cuando hay trace context activo |
data/README.md CHANGED
@@ -242,14 +242,14 @@ task generate_invoices: :environment do
242
242
  end
243
243
  ```
244
244
 
245
- `TaskMonitor` genera un `root_id` nuevo, configura Reporter y Current, loguea `task_started`/`task_finished` con `duration_s` y `status`, y limpia el contexto al finalizar. Si el bloque lanza una excepción, la registra como `status=failed` y la re-lanza.
245
+ `TaskMonitor` genera un `root_id` nuevo, configura Reporter y Current, loguea `task_started`/`task_finished` con `duration_s` y `outcome`, y limpia el contexto al finalizar. Si el bloque lanza una excepción, la registra como `outcome=failed` con `exception.type`, `exception.message` y `exception.stacktrace` (OTel) y la re-lanza.
246
246
 
247
247
  ## JSON Logging
248
248
 
249
249
  Con `log_format: :json`, `ExisRay::JsonFormatter` reemplaza el formatter de Rails y emite cada línea como JSON single-line con contexto inyectado automáticamente:
250
250
 
251
251
  ```json
252
- {"time":"2026-04-01T14:30:00Z","level":"INFO","service":"wispro_agent","root_id":"1-65f...abc","trace_id":"Root=1-65f...;Self=...","source":"http","user_id":42,"isp_id":10,"component":"exis_ray","event":"http_request","method":"GET","path":"/api/v1/users","status":200,"duration_s":0.0452}
252
+ {"time":"2026-04-01T14:30:00Z","level":"INFO","severity_number":9,"service":"wispro_agent","service_version":"1.2.3","deployment_environment":"production","root_id":"1-65f...abc","trace_id":"Root=1-65f...;Self=...","source":"http","user_id":42,"isp_id":10,"component":"exis_ray","event":"http_request","method":"GET","path":"/api/v1/users","http_route":"/api/v1/users","http_status":200,"duration_s":0.0452,"user_agent_original":"Mozilla/5.0","server_address":"api.example.com"}
253
253
  ```
254
254
 
255
255
  Los mensajes con formato `key=value` se parsean y elevan al root del JSON. Los valores numéricos se castean automáticamente:
@@ -276,7 +276,10 @@ En modo `:text`, ExisRay inyecta el `trace_id` o `root_id` como tag de Rails (`c
276
276
  |:------|:----------|
277
277
  | `time` | Siempre (UTC ISO 8601) |
278
278
  | `level` | Siempre |
279
+ | `severity_number` | Siempre (OTel SeverityNumber: DEBUG=5, INFO=9, WARN=13, ERROR=17, FATAL=21) |
279
280
  | `service` | Siempre (nombre de la app Rails en snake_case) |
281
+ | `service_version` | Siempre (lee de `config.version` o `config.x.version`) |
282
+ | `deployment_environment` | Siempre (lee de `Rails.env`) |
280
283
  | `root_id` | Cuando hay trace context activo |
281
284
  | `trace_id` | Cuando hay trace context activo |
282
285
  | `source` | Cuando hay trace context activo (`http`, `sidekiq`, `task`, `system`) |
@@ -287,6 +290,18 @@ En modo `:text`, ExisRay inyecta el `trace_id` o `root_id` como tag de Rails (`c
287
290
  | `task` | Solo en procesos TaskMonitor |
288
291
  | `tags` | Solo si hay Rails tagged logging activo |
289
292
 
293
+ `LogSubscriber` inyecta además estos campos en los logs de requests HTTP:
294
+
295
+ | Campo | Descripción |
296
+ |:------|:------------|
297
+ | `http_status` | Código HTTP (Integer). Antes `status`, renombrado en v0.6.0 |
298
+ | `http_route` | Template de ruta (ej: `/users/:id`). Resolución via `route.defaults` |
299
+ | `user_agent_original` | Header `User-Agent` del request |
300
+ | `server_address` | Hostname sin puerto del header `Host` |
301
+ | `exception.type` | Clase de la excepción (cuando el request falla) |
302
+ | `exception.message` | Mensaje de la excepción |
303
+ | `exception.stacktrace` | Primeras 20 líneas del backtrace |
304
+
290
305
  ### Filtrado de claves sensibles
291
306
 
292
307
  Las claves que matcheen `/password|pass|passwd|secret|token|api_key|auth/i` se reemplazan automáticamente por `[FILTERED]`, tanto en strings KV como en Hashes (incluyendo anidados).
@@ -40,6 +40,18 @@ module ExisRay
40
40
  # @example 'MyLogSubscriber'
41
41
  attr_accessor :log_subscriber_class
42
42
 
43
+ # @!attribute [rw] service_version
44
+ # @return [String, nil] Versión del servicio. Equivale a `service.version` de OTel.
45
+ # Por defecto intenta leer `Rails.application.config.x.version` si Rails está disponible.
46
+ # @example '1.2.3'
47
+ attr_accessor :service_version
48
+
49
+ # @!attribute [rw] deployment_environment
50
+ # @return [String, nil] Entorno de despliegue. Equivale a `deployment.environment` de OTel.
51
+ # Por defecto lee `Rails.env` si Rails está disponible.
52
+ # @example 'production'
53
+ attr_accessor :deployment_environment
54
+
43
55
  # Inicializa la configuración con valores por defecto compatibles con AWS X-Ray.
44
56
  def initialize
45
57
  @trace_header = "HTTP_X_AMZN_TRACE_ID"
@@ -48,6 +60,39 @@ module ExisRay
48
60
  @current_class = "Current"
49
61
  @log_format = :text
50
62
  @log_subscriber_class = nil
63
+ @service_version = default_service_version
64
+ @deployment_environment = default_deployment_environment
65
+ end
66
+
67
+ # Lee la versión del servicio desde la configuración de Rails.
68
+ # Busca primero en `config.version` (atributo directo) y luego en `config.x.version`
69
+ # (custom config namespace). Retorna nil si ninguno está definido.
70
+ #
71
+ # @return [String, nil]
72
+ def default_service_version
73
+ return unless defined?(Rails) && Rails.application&.config
74
+
75
+ config = Rails.application.config
76
+
77
+ # Primero: config.version (atributo directo definido en Application)
78
+ version = config.version if config.respond_to?(:version)
79
+ return version.to_s if version.present?
80
+
81
+ # Fallback: config.x.version (custom config namespace)
82
+ if config.respond_to?(:x) && config.x.respond_to?(:version)
83
+ x_version = config.x.version
84
+ return x_version.to_s if x_version.present?
85
+ end
86
+
87
+ nil
88
+ rescue StandardError
89
+ nil
90
+ end
91
+
92
+ def default_deployment_environment
93
+ return unless defined?(Rails) && Rails.respond_to?(:env)
94
+
95
+ Rails.env.to_s
51
96
  end
52
97
 
53
98
  # Indica si la aplicación está configurada para emitir logs en formato estructurado (JSON).
@@ -28,6 +28,16 @@ module ExisRay
28
28
  # Claves sensibles que deben filtrarse automáticamente según el estándar de Gabriel.
29
29
  SENSITIVE_KEYS = /password|pass|passwd|secret|token|api_key|auth/i
30
30
 
31
+ # Mapeo de severity_text OTel a SeverityNumber del Log Data Model.
32
+ # https://opentelemetry.io/docs/specs/otel/log/data-model/#field-severitynumber
33
+ SEVERITY_NUMBER = {
34
+ "DEBUG" => 5,
35
+ "INFO" => 9,
36
+ "WARN" => 13,
37
+ "ERROR" => 17,
38
+ "FATAL" => 21
39
+ }.freeze
40
+
31
41
  # Procesa un mensaje de log y lo formatea como una cadena estructurada en JSON.
32
42
  #
33
43
  # @param severity [String] El nivel de severidad del log (ej. "INFO", "ERROR", "DEBUG").
@@ -39,7 +49,10 @@ module ExisRay
39
49
  payload = {
40
50
  time: timestamp&.utc&.iso8601 || Time.now.utc.iso8601,
41
51
  level: severity,
42
- service: ExisRay::Tracer.service_name
52
+ severity_number: SEVERITY_NUMBER[severity],
53
+ service: ExisRay::Tracer.service_name,
54
+ service_version: service_version,
55
+ deployment_environment: deployment_environment
43
56
  }
44
57
 
45
58
  inject_tracer_context(payload)
@@ -54,6 +67,16 @@ module ExisRay
54
67
 
55
68
  private
56
69
 
70
+ # @return [String, nil]
71
+ def service_version
72
+ ExisRay.configuration.service_version
73
+ end
74
+
75
+ # @return [String, nil]
76
+ def deployment_environment
77
+ ExisRay.configuration.deployment_environment
78
+ end
79
+
57
80
  # Genera un JSON de fallback cuando el formateo principal falla.
58
81
  # Nunca debe lanzar una excepción.
59
82
  #
@@ -32,7 +32,7 @@ module ExisRay
32
32
  # @return [void]
33
33
  def process_action(event)
34
34
  payload = build_payload(event)
35
- if payload[:status] && payload[:status] >= 500
35
+ if payload[:http_status] && payload[:http_status] >= 500
36
36
  logger.error(payload)
37
37
  else
38
38
  logger.info(payload)
@@ -90,25 +90,183 @@ module ExisRay
90
90
  view_s = payload[:view_runtime] ? (payload[:view_runtime] / 1000.0).round(4) : nil
91
91
  db_s = payload[:db_runtime] ? (payload[:db_runtime] / 1000.0).round(4) : nil
92
92
 
93
+ headers = payload[:headers] || {}
94
+
95
+ exception_data = extract_exception_data(payload)
96
+
93
97
  data = {
94
98
  component: "exis_ray",
95
99
  event: "http_request",
96
100
  method: payload[:method],
97
101
  path: payload[:path],
102
+ http_route: extract_http_route(payload),
98
103
  format: payload[:format],
99
104
  controller: payload[:controller],
100
105
  action: payload[:action],
101
- status: status,
106
+ http_status: status,
102
107
  duration_s: duration_s,
103
108
  duration_human: ExisRay::Tracer.format_duration(duration_s),
104
109
  view_runtime_s: view_s,
105
- db_runtime_s: db_s
110
+ db_runtime_s: db_s,
111
+ user_agent_original: headers["HTTP_USER_AGENT"],
112
+ server_address: extract_server_address(headers["HTTP_HOST"])
106
113
  }
107
114
 
115
+ data.merge!(exception_data)
108
116
  data.merge!(self.class.extra_fields(event))
109
117
  data.compact
110
118
  end
111
119
 
120
+ # Extrae el hostname del valor de HTTP_HOST, descartando el puerto si está presente.
121
+ # OTel `server.address` espera solo hostname; el puerto va en `server.port` por separado.
122
+ #
123
+ # @param http_host [String, nil] Valor del header HTTP_HOST (ej: "api.example.com:3000").
124
+ # @return [String, nil] Hostname sin puerto, o nil si http_host es nil.
125
+ def extract_server_address(http_host)
126
+ return nil unless http_host
127
+
128
+ http_host.split(":").first
129
+ end
130
+
131
+ # 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".
135
+ #
136
+ # El stacktrace se toma de `payload[:exception_object]` (expuesto por Rails), no
137
+ # de `payload[:exception]` que es solo `[class_name, message]`.
138
+ #
139
+ # @param payload [Hash] Payload completo del notification de Rails.
140
+ # @return [Hash] Campos exception.* y error_class/error_message con keys symbol.
141
+ def extract_exception_data(payload)
142
+ exception_info = payload[:exception]
143
+ return {} unless exception_info
144
+
145
+ exception_class = exception_info.first
146
+ exception_message = exception_info.last
147
+
148
+ data = {
149
+ error_class: exception_class,
150
+ error_message: exception_message,
151
+ "exception.type": exception_class,
152
+ "exception.message": exception_message
153
+ }
154
+
155
+ exception_object = payload[:exception_object]
156
+ if exception_object.respond_to?(:backtrace) && exception_object.backtrace
157
+ data[:"exception.stacktrace"] = exception_object.backtrace.take(20).join("\n")
158
+ end
159
+
160
+ data
161
+ rescue StandardError
162
+ {}
163
+ end
164
+
165
+ # Extrae la plantilla de ruta (http.route) de una URL concreta.
166
+ # OTel espera la ruta templated (ej: `/users/:id`) en lugar de la URL concreta
167
+ # (ej: `/users/42`), para bajar cardinalidad en dashboards.
168
+ #
169
+ # Estrategia en capas:
170
+ # 1. Rails 7.1+: `payload[:request].route_uri_pattern` si está presente.
171
+ # 2. Fallback: iterar `Rails.application.routes.routes` buscando el match por
172
+ # `defaults[:controller]` + `defaults[:action]` + verb HTTP, y devolver el
173
+ # pattern limpio de la primera ruta que matchee.
174
+ #
175
+ # @param payload [Hash] Payload del evento de Rails.
176
+ # @return [String, nil] Ruta plantilla o nil si no se puede resolver.
177
+ def extract_http_route(payload)
178
+ return nil unless defined?(Rails) && Rails.application&.routes
179
+
180
+ from_request = extract_from_route_uri_pattern(payload)
181
+ return from_request if from_request
182
+
183
+ controller = payload[:controller]
184
+ action = payload[:action]
185
+ method = payload[:method]
186
+ return nil unless controller && action && method
187
+
188
+ find_route_template(controller, action, method)
189
+ rescue StandardError
190
+ nil
191
+ end
192
+
193
+ # Rails 7.1+ expone `route_uri_pattern` en el request. Algunas versiones de
194
+ # `ActionController::Instrumentation` incluyen el request en el payload; si no,
195
+ # esta rama simplemente retorna nil y el caller cae al fallback.
196
+ #
197
+ # @param payload [Hash]
198
+ # @return [String, nil]
199
+ def extract_from_route_uri_pattern(payload)
200
+ request = payload[:request]
201
+ return nil unless request.respond_to?(:route_uri_pattern)
202
+
203
+ pattern = request.route_uri_pattern
204
+ clean_route_pattern(pattern.to_s) if pattern
205
+ rescue StandardError
206
+ nil
207
+ end
208
+
209
+ # Busca en la RouteSet de Rails la ruta que matchea controller + action + verb
210
+ # y devuelve su pattern template (ej: `/users/:id`).
211
+ #
212
+ # Rails guarda controller/action en `route.defaults`, no en `route.requirements`.
213
+ # El formato típico es `defaults[:controller] == "users"` (snake_case sin el sufijo
214
+ # `Controller`), mientras que `payload[:controller]` viene como `"UsersController"`.
215
+ #
216
+ # @param controller [String] Class name del controller (ej: "UsersController").
217
+ # @param action [String] Nombre del action (ej: "show").
218
+ # @param method [String] Método HTTP (ej: "GET").
219
+ # @return [String, nil] Pattern template o nil.
220
+ def find_route_template(controller, action, method)
221
+ normalized = controller.to_s.sub(/Controller$/, "").gsub("::", "/")
222
+ normalized = normalized.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
223
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
224
+ .downcase
225
+ action_s = action.to_s
226
+ method_s = method.to_s.upcase
227
+
228
+ Rails.application.routes.routes.each do |route|
229
+ defaults = route.defaults
230
+ next unless defaults[:controller] == normalized
231
+ next unless defaults[:action].to_s == action_s
232
+ next unless route_matches_verb?(route, method_s)
233
+
234
+ return clean_route_pattern(route.path.spec.to_s)
235
+ end
236
+ nil
237
+ rescue StandardError
238
+ nil
239
+ end
240
+
241
+ # Verifica que el verb HTTP de la ruta matchee con el método del request.
242
+ # `route.verb` en Rails 5+ es un String (ej: "GET"); en versiones antes era un
243
+ # Regexp. Manejamos ambos y además el caso de rutas multi-verb (empty verb).
244
+ #
245
+ # @param route [ActionDispatch::Journey::Route]
246
+ # @param method [String] Método HTTP en mayúsculas.
247
+ # @return [Boolean]
248
+ def route_matches_verb?(route, method)
249
+ verb = route.verb
250
+ return true if verb.nil? || verb.to_s.empty?
251
+
252
+ if verb.is_a?(Regexp)
253
+ verb.match?(method)
254
+ else
255
+ verb.to_s.upcase == method
256
+ end
257
+ rescue StandardError
258
+ true
259
+ end
260
+
261
+ # Limpia el sufijo `(.:format)` típico de las rutas de Rails, que no es parte
262
+ # del template semántico que queremos emitir en el log.
263
+ #
264
+ # @param pattern [String] Pattern crudo de `route.path.spec.to_s`.
265
+ # @return [String]
266
+ def clean_route_pattern(pattern)
267
+ pattern.to_s.sub(/\(\.:format\)\z/, "")
268
+ end
269
+
112
270
  # Infiere el status HTTP desde el nombre de la excepción cuando el request
113
271
  # terminó con una excepción no rescatada (payload[:status] es nil).
114
272
  #
@@ -29,7 +29,7 @@ module ExisRay
29
29
  curr.correlation_id = ExisRay::Tracer.correlation_id
30
30
  end
31
31
 
32
- log_event(:info, "component=exis_ray event=task_started task=#{task_name} status=started")
32
+ log_event(:info, "component=exis_ray event=task_started task=#{task_name} outcome=started")
33
33
 
34
34
  # Bloque de ejecución con o sin tags dependiendo de la configuración
35
35
  execute_with_optional_tags(&block)
@@ -39,15 +39,17 @@ module ExisRay
39
39
 
40
40
  log_event(:info,
41
41
  "component=exis_ray event=task_finished task=#{task_name} " \
42
- "status=success duration_s=#{duration_s} duration_human=\"#{human_time}\"")
42
+ "outcome=success duration_s=#{duration_s} duration_human=\"#{human_time}\"")
43
43
  rescue StandardError => e
44
44
  duration_s = ExisRay::Tracer.current_duration_s
45
45
  human_time = ExisRay::Tracer.format_duration(duration_s)
46
46
 
47
47
  log_event(:error,
48
48
  "component=exis_ray event=task_finished task=#{task_name} " \
49
- "status=failed duration_s=#{duration_s} duration_human=\"#{human_time}\" " \
50
- "error_class=#{e.class} error_message=#{e.message.inspect}")
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)}")
51
53
  raise e
52
54
  ensure
53
55
  # Limpieza centralizada obligatoria para evitar filtraciones de memoria o contexto
@@ -107,6 +109,22 @@ module ExisRay
107
109
  rescue StandardError
108
110
  end
109
111
 
110
- private_class_method :pod_identifier, :setup_tracer, :execute_with_optional_tags, :log_event
112
+ # Formatea el backtrace para logging:
113
+ # - Limita a las primeras 20 líneas (evita líneas de MB)
114
+ # - Maneja nil backtrace (excepciones sin stacktrace)
115
+ #
116
+ # @param backtrace [Array<String>, nil]
117
+ # @return [String]
118
+ def self.format_stacktrace(backtrace)
119
+ return '""' unless backtrace
120
+
121
+ lines = backtrace.take(20).join("\n")
122
+ lines.inspect
123
+ rescue StandardError
124
+ '""'
125
+ end
126
+
127
+ private_class_method :pod_identifier, :setup_tracer, :execute_with_optional_tags,
128
+ :log_event, :format_stacktrace
111
129
  end
112
130
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.5.11"
5
+ VERSION = "0.6.1"
6
6
  end
data/skill/SKILL.md CHANGED
@@ -7,6 +7,8 @@ description: Skill de conocimiento completo sobre ExisRay, la capa de observabil
7
7
 
8
8
  Observabilidad y trazabilidad distribuida para microservicios Rails (AWS X-Ray compatible).
9
9
 
10
+ Para el complemento del estándar de logging Wispro (regla Data First, mapeo OpenTelemetry, ciclo de vida de jobs/requests), ver `references/standard.md`.
11
+
10
12
  ---
11
13
 
12
14
  ## Glosario
@@ -69,8 +71,8 @@ ExisRay unifica trazabilidad distribuida, logging estructurado JSON, contexto de
69
71
  2. `Tracer.parse_trace_id` extrae `root_id`, `self_id`, `called_from`, `total_time_so_far`
70
72
  3. `ExisRay.sync_correlation_id` asigna `Tracer.correlation_id` a `Current.correlation_id`
71
73
  4. Controller ejecuta `before_action` para setear `Current.user_id`, `Current.isp_id`
72
- 5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta automaticamente: `time`, `level`, `service`, `root_id`, `trace_id`, `source`, `user_id`, `isp_id`, `correlation_id`
73
- 6. `LogSubscriber` emite un unico Hash al finalizar el request (method, path, status, duration_s, etc.)
74
+ 5. `JsonFormatter` intercepta cada `Rails.logger.*` e inyecta automaticamente: `time`, `level`, `severity_number`, `service`, `service_version`, `deployment_environment`, `root_id`, `trace_id`, `source`, `user_id`, `isp_id`, `correlation_id`
75
+ 6. `LogSubscriber` emite un unico Hash al finalizar el request (method, path, http_status, http_route, duration_s, user_agent_original, server_address, etc.)
74
76
  7. En llamadas salientes, `FaradayMiddleware`/`ActiveResourceInstrumentation` inyectan `propagation_trace_header` con `Tracer.generate_trace_header`
75
77
  8. Al finalizar, `ActiveSupport::CurrentAttributes` hace reset automatico
76
78
 
@@ -88,6 +90,8 @@ ExisRay.configure do |config|
88
90
  config.reporter_class = "Reporter" # String, default "Reporter"
89
91
  config.log_format = :json # Symbol, :text (default) | :json
90
92
  config.log_subscriber_class = "MyLogSubscriber" # String|nil, default nil
93
+ config.service_version = "1.2.3" # String|nil, default: Rails config.version o config.x.version
94
+ config.deployment_environment = "production" # String|nil, default: Rails.env
91
95
  end
92
96
 
93
97
  ExisRay.configuration.json_logs? # => true si log_format == :json
@@ -199,7 +203,9 @@ Soporta Sentry moderno (`Sentry.capture_exception`) y legacy (`Session`/`Raven`)
199
203
  ExisRay::TaskMonitor.run("billing:generate_invoices") do
200
204
  InvoiceService.process_all
201
205
  end
202
- # Genera root_id propio, loguea task_started/task_finished con duration_s.
206
+ # Genera root_id propio, loguea task_started/task_finished con outcome y duration_s.
207
+ # En caso de error emite: error_class, error_message (legacy) + exception.type,
208
+ # exception.message, exception.stacktrace (OTel, limitado a 20 lineas).
203
209
  # Re-lanza excepciones despues de loguearlas.
204
210
  # Hace reset de Tracer, Current y Reporter en ensure.
205
211
  ```