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 +4 -4
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +12 -4
- data/README.md +17 -2
- data/lib/exis_ray/configuration.rb +45 -0
- data/lib/exis_ray/json_formatter.rb +24 -1
- data/lib/exis_ray/log_subscriber.rb +161 -3
- data/lib/exis_ray/task_monitor.rb +23 -5
- data/lib/exis_ray/version.rb +1 -1
- data/skill/SKILL.md +9 -3
- data/skill/references/standard.md +155 -0
- data/skills.lock +10 -1
- data/skills.yml +34 -9
- data/spec/exis_ray/configuration_spec.rb +69 -0
- data/spec/exis_ray/json_formatter_spec.rb +72 -0
- data/spec/exis_ray/log_subscriber_spec.rb +326 -0
- data/spec/exis_ray/task_monitor_spec.rb +108 -0
- metadata +7 -15
- data/.agents/skills/agent-review/SKILL.md +0 -213
- data/.agents/skills/ai-reports/SKILL.md +0 -211
- data/.agents/skills/documentation-writer/SKILL.md +0 -45
- data/.agents/skills/gem-release/SKILL.md +0 -116
- data/.agents/skills/quality-code/SKILL.md +0 -51
- data/.agents/skills/skill-builder/SKILL.md +0 -293
- data/.agents/skills/skill-manager/SKILL.md +0 -172
- data/.agents/skills/skill-manager/scripts/sync.rb +0 -310
- data/.agents/skills/yard/SKILL.md +0 -311
- data/.agents/skills/yard/references/tipos.md +0 -144
- data/MANIFEST.md +0 -222
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b8dea5e0b236f90177ce536cf1226b37b21c778ee6891dc0276756f9b972fc8
|
|
4
|
+
data.tar.gz: a8409a1e92f2494535f9e0512f1b47e8cc0bd841335e9b98d6b19ce20ebab08d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 `
|
|
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
|
-
-
|
|
38
|
-
-
|
|
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 `
|
|
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","
|
|
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
|
-
|
|
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[:
|
|
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
|
-
|
|
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}
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
data/lib/exis_ray/version.rb
CHANGED
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,
|
|
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
|
```
|