exis_ray 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/CLAUDE.md +1 -0
- data/README.md +85 -16
- data/lib/exis_ray/current.rb +24 -0
- data/lib/exis_ray/json_formatter.rb +21 -2
- data/lib/exis_ray/railtie.rb +2 -6
- data/lib/exis_ray/version.rb +1 -1
- data/skill/SKILL.md +95 -5
- data/spec/exis_ray/current_spec.rb +34 -0
- data/spec/exis_ray/json_formatter_spec.rb +85 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95101aa6f502c3c839070a556ab984244493d9f8bbabaad2b3b4e2e83b19cc67
|
|
4
|
+
data.tar.gz: af2097e279144370005dd457effbeaf961a11938f191a9aa30053efe717f9b12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62e1bf2fe943f02976ba69b73ba9d76e698f93f6b9d1284a02354c01c4ab4d5b18411212da27ffe0ea315616184dfce0d3b66dc9a871c6cebbb50320656f5342
|
|
7
|
+
data.tar.gz: 10cf92356274c0da04446c96a109f298eb2c94a8eacb3787762e9785cf42915b877304161f70aeb6ac8478598b29c6aca127057a0db8d7f089f29b046a9bd954
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [0.8.0] - 2026-05-14
|
|
2
|
+
|
|
3
|
+
### Nuevas funcionalidades
|
|
4
|
+
- **`Current.log_fields` hook** (#6, #8): class method overridable en `ExisRay::Current` para inyectar campos custom en cada log line. Cubre con un solo mecanismo tanto constantes de proceso (frozen constants en la subclass) como valores dinámicos per-request (atributos de Current). `JsonFormatter` filtra claves sensibles del hash retornado y rescata silenciosamente si el override revienta. Útil para servicios multi-tenant donde se necesita inyectar `tenant_id`, `region`, `stack_id`, etc.
|
|
5
|
+
|
|
6
|
+
### Mejoras internas
|
|
7
|
+
- **`.rubocop.yml` relajado para destrabar CI**: los thresholds default venían bloqueando todos los PRs recientes. Excluido `Metrics/BlockLength` en specs y railtie, deshabilitado `Lint/SuppressedException` (el estándar Wispro requiere `rescue StandardError` vacíos en logging), thresholds más realistas para Metrics en lib/.
|
|
8
|
+
|
|
9
|
+
## [0.7.2] - 2026-05-11
|
|
10
|
+
|
|
11
|
+
### Documentación
|
|
12
|
+
- **Warning sobre `ActiveSupport::TaggedLogging` con JSON logging:** `TaggedLogging` antepone tags como texto raw antes del formatter, rompiendo el JSON (`[req_id] {...}`). `JsonFormatter` ya inyecta `request_id`/`trace_id` como campos JSON — usar ambos genera salida inválida. Documentada la config correcta del logger en `production.rb` (README + SKILL FAQ).
|
|
13
|
+
- **Skill improvements (issue #4):** (1) Clarificado el criterio auto-inyectado vs manual — el formatter solo conoce contexto de ejecución (Tracer/Current), por eso `component` y `event` deben aportarse manualmente. (2) Agregados ejemplos concretos de output JSON lado-a-lado con el input KV/Hash. (3) Completada la lista de campos default de `LogSubscriber` (17 campos con tipos y condiciones, antes truncado con "etc."). (4) Documentados los 3 modos de input del formatter (KV / Hash / string libre) con ejemplos de cada uno.
|
|
14
|
+
|
|
1
15
|
## [0.7.1] - 2026-04-06
|
|
2
16
|
|
|
3
17
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -111,6 +111,7 @@ ExisRay.configuration.json_logs? # => true/false
|
|
|
111
111
|
| `correlation_id` | Cuando `Current.correlation_id` está presente |
|
|
112
112
|
| `user_id` | Cuando `Current.user_id` está presente |
|
|
113
113
|
| `isp_id` | Cuando `Current.isp_id` está presente |
|
|
114
|
+
| `Current.log_fields` (cualquier key) | Si la subclass overrideó el hook (default `{}`) |
|
|
114
115
|
| `sidekiq_job` | Solo en procesos Sidekiq |
|
|
115
116
|
| `task` | Solo en procesos TaskMonitor |
|
|
116
117
|
| `tags` | Solo si hay Rails tagged logging activo |
|
data/README.md
CHANGED
|
@@ -151,6 +151,38 @@ end
|
|
|
151
151
|
| `Current.user?` / `Current.isp?` | Predicate: true si el ID no es nil |
|
|
152
152
|
| `Current.correlation_id?` | Predicate: true si está presente (no vacío) |
|
|
153
153
|
|
|
154
|
+
#### Hook `log_fields` — inyectar campos custom en cada log
|
|
155
|
+
|
|
156
|
+
`Current.log_fields` es un class method overridable que retorna un Hash de campos
|
|
157
|
+
extra a inyectar en cada log line, junto a `user_id`/`isp_id`/`correlation_id`.
|
|
158
|
+
Cubre tanto **constantes de proceso** (declaradas como frozen constants en la
|
|
159
|
+
subclass) como **valores dinámicos per-request** (leídos de atributos de
|
|
160
|
+
`Current`) — todo en un solo lugar.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class Current < ExisRay::Current
|
|
164
|
+
TENANT_ID = ENV.fetch("TENANT_ID").freeze # static, frozen al boot
|
|
165
|
+
attribute :region # dynamic, per-request
|
|
166
|
+
|
|
167
|
+
def self.log_fields
|
|
168
|
+
{ tenant_id: TENANT_ID, region: region }.compact
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# En un before_action / middleware:
|
|
173
|
+
Current.region = request.headers["X-Region"]
|
|
174
|
+
|
|
175
|
+
# Los logs salen automáticamente con tenant_id y region:
|
|
176
|
+
# {"...":"...", "tenant_id":"42", "region":"us-east-1", "event":"..."}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Reglas:**
|
|
180
|
+
|
|
181
|
+
- Default `{}` — cero overhead si no se override.
|
|
182
|
+
- `JsonFormatter` filtra claves sensibles del hash retornado (`api_key`, `token`, etc.).
|
|
183
|
+
- Si el override revienta, el formatter lo rescata silenciosamente (logging nunca debe afectar el flujo principal).
|
|
184
|
+
- **Precedencia**: los campos canónicos del Tracer (`trace_id`, `root_id`, etc.) y las keys del propio mensaje del developer (ej. `Rails.logger.info "tenant_id=99"`) pisan `log_fields`. No sirve para overrideear campos canónicos, solo para agregar nuevos.
|
|
185
|
+
|
|
154
186
|
### Reporter (reporte de errores)
|
|
155
187
|
|
|
156
188
|
`ExisRay::Reporter` es un wrapper de Sentry que enriquece automáticamente cada evento con el trace context del `Tracer` y el contexto de negocio del `Current`. Soporta Sentry SDK moderno y legacy (Raven/Session).
|
|
@@ -252,22 +284,46 @@ Con `log_format: :json`, `ExisRay::JsonFormatter` reemplaza el formatter de Rail
|
|
|
252
284
|
{"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
285
|
```
|
|
254
286
|
|
|
255
|
-
|
|
287
|
+
El formatter acepta tres tipos de mensaje. Los tres producen output JSON equivalente; elegí el que sea más legible para tu caso:
|
|
256
288
|
|
|
257
289
|
```ruby
|
|
290
|
+
# 1. String KV — one-liner rápido, valores numéricos se castean
|
|
258
291
|
Rails.logger.info "component=billing event=invoice_created invoice_id=123 total=45.50"
|
|
259
|
-
# => {"time":"...","level":"INFO","service":"...","root_id":"...","component":"billing","event":"invoice_created","invoice_id":123,"total":45.5}
|
|
260
|
-
```
|
|
261
292
|
|
|
262
|
-
|
|
293
|
+
# 2. Hash style — payloads complejos o con valores nested
|
|
294
|
+
Rails.logger.info(component: "billing", event: "invoice_created",
|
|
295
|
+
invoice: { id: 123, total: 45.50 })
|
|
263
296
|
|
|
264
|
-
|
|
297
|
+
# 3. String libre — fallback, va a la clave `body`
|
|
265
298
|
Rails.logger.info "Algo pasó sin formato KV"
|
|
266
299
|
# => {"time":"...","level":"INFO","service":"...","body":"Algo pasó sin formato KV"}
|
|
267
300
|
```
|
|
268
301
|
|
|
302
|
+
Los mensajes Hash también son la forma que usa internamente `LogSubscriber` para emitir el cierre de cada request HTTP.
|
|
303
|
+
|
|
304
|
+
> **Nota:** `component` (módulo de negocio) y `event` (qué pasó) **no** son auto-inyectados — los aporta el call site. El formatter solo conoce el contexto de **ejecución** (quién, de dónde, con qué identidad), no el contexto del **lugar del código** que loguea.
|
|
305
|
+
|
|
269
306
|
En modo `:text`, ExisRay inyecta el `trace_id` o `root_id` como tag de Rails (`config.log_tags`) y no modifica el formatter.
|
|
270
307
|
|
|
308
|
+
### Configuración del logger en producción
|
|
309
|
+
|
|
310
|
+
Para logs JSON limpios sin códigos ANSI ni texto extra, configurar el logger así:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
# config/environments/production.rb
|
|
314
|
+
config.colorize_logging = false
|
|
315
|
+
config.logger = ActiveSupport::Logger.new(STDOUT)
|
|
316
|
+
config.logger.formatter = ExisRay::JsonFormatter
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**No usar `ActiveSupport::TaggedLogging`** — agrega tags como texto al inicio de cada línea antes del formatter, lo cual rompe el JSON:
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
[request_id] {"time":"...","level":"INFO",...} # ← texto antes del JSON (INVALIDO)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`ExisRay::JsonFormatter` ya inyecta los tags (`request_id`, `trace_id`, etc.) como campos JSON, así que `TaggedLogging` es redundante e incompatible.
|
|
326
|
+
|
|
271
327
|
### Campos auto-inyectados
|
|
272
328
|
|
|
273
329
|
`JsonFormatter` inyecta estos campos automáticamente en cada línea. **Nunca** los incluyas manualmente en tus logs:
|
|
@@ -286,21 +342,34 @@ En modo `:text`, ExisRay inyecta el `trace_id` o `root_id` como tag de Rails (`c
|
|
|
286
342
|
| `correlation_id` | Cuando `Current.correlation_id` está presente |
|
|
287
343
|
| `user_id` | Cuando `Current.user_id` no es nil |
|
|
288
344
|
| `isp_id` | Cuando `Current.isp_id` no es nil |
|
|
345
|
+
| `Current.log_fields` (cualquier key) | Si la subclass overrideó el hook y retornó un Hash no vacío |
|
|
289
346
|
| `sidekiq_job` | Solo en procesos Sidekiq |
|
|
290
347
|
| `task` | Solo en procesos TaskMonitor |
|
|
291
348
|
| `tags` | Solo si hay Rails tagged logging activo |
|
|
292
349
|
|
|
293
|
-
`LogSubscriber` inyecta además estos campos en
|
|
294
|
-
|
|
295
|
-
| Campo |
|
|
296
|
-
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
350
|
+
`LogSubscriber` inyecta además estos campos en cada log de cierre de request HTTP. **Nunca duplicarlos** en logs manuales:
|
|
351
|
+
|
|
352
|
+
| Campo | Tipo | Notas |
|
|
353
|
+
|:------|:-----|:------|
|
|
354
|
+
| `component` | String | Siempre `"exis_ray"` |
|
|
355
|
+
| `event` | String | Siempre `"http_request"` |
|
|
356
|
+
| `method` | String | Verbo HTTP |
|
|
357
|
+
| `path` | String | URL concreta del request |
|
|
358
|
+
| `http_route` | String | Template (ej: `/users/:id`). Baja cardinalidad para dashboards |
|
|
359
|
+
| `format` | Symbol/String | `html`, `json`, etc. |
|
|
360
|
+
| `controller` | String | Class name del controller |
|
|
361
|
+
| `action` | String | Nombre del action |
|
|
362
|
+
| `http_status` | Integer | Status HTTP final. Antes `status`, renombrado en v0.6.0 |
|
|
363
|
+
| `duration_s` | Float | Segundos (Rails reporta ms, se convierte), redondeo 4 decimales |
|
|
364
|
+
| `duration_human` | String | Legible: `"42.5ms"`, `"1.25s"`, `"2 minutes 5 seconds"` |
|
|
365
|
+
| `view_runtime_s` | Float\|nil | Solo si Rails lo reporta |
|
|
366
|
+
| `db_runtime_s` | Float\|nil | Solo si ActiveRecord lo reporta |
|
|
367
|
+
| `user_agent_original` | String | Header `User-Agent` |
|
|
368
|
+
| `server_address` | String | Hostname sin puerto del header `Host` |
|
|
369
|
+
| `error_class`, `error_message` | String | Solo en fallo (legacy) |
|
|
370
|
+
| `exception.type`, `exception.message`, `exception.stacktrace` | String | Solo en fallo (OTel; stack limitado a 20 líneas) |
|
|
371
|
+
|
|
372
|
+
Severity del log: `ERROR` si `http_status >= 500`, sino `INFO`.
|
|
304
373
|
|
|
305
374
|
### Filtrado de claves sensibles
|
|
306
375
|
|
data/lib/exis_ray/current.rb
CHANGED
|
@@ -8,6 +8,30 @@ module ExisRay
|
|
|
8
8
|
class Current < ActiveSupport::CurrentAttributes
|
|
9
9
|
attribute :user_id, :isp_id, :correlation_id
|
|
10
10
|
|
|
11
|
+
# Hook overridable por la subclass de la app host. Retorna un Hash de campos
|
|
12
|
+
# extra a inyectar en cada log line, junto a `user_id`/`isp_id`/`correlation_id`.
|
|
13
|
+
#
|
|
14
|
+
# Pensado para cubrir tanto constantes de proceso (declaradas como `freeze`-d
|
|
15
|
+
# constants en la subclass) como valores dinámicos per-request (leídos de
|
|
16
|
+
# atributos de Current). El JsonFormatter invoca este método en cada log y
|
|
17
|
+
# mergea el resultado al payload — luego de los campos canónicos pero antes
|
|
18
|
+
# de las keys del propio mensaje del developer (que ganan por override).
|
|
19
|
+
#
|
|
20
|
+
# @example Constantes de proceso + valores per-request combinados
|
|
21
|
+
# class Current < ExisRay::Current
|
|
22
|
+
# TENANT_ID = ENV.fetch("TENANT_ID").freeze
|
|
23
|
+
# attribute :region
|
|
24
|
+
#
|
|
25
|
+
# def self.log_fields
|
|
26
|
+
# { tenant_id: TENANT_ID, region: region }.compact
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @return [Hash] Pares clave/valor a inyectar. Default `{}`.
|
|
31
|
+
def self.log_fields
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
34
|
+
|
|
11
35
|
# Callback nativo de Rails: Se ejecuta automáticamente al llamar a Current.reset
|
|
12
36
|
resets do
|
|
13
37
|
@user_object = nil
|
|
@@ -122,9 +122,28 @@ module ExisRay
|
|
|
122
122
|
payload[:user_id] = curr.user_id if curr.respond_to?(:user_id) && !curr.user_id.nil?
|
|
123
123
|
payload[:isp_id] = curr.isp_id if curr.respond_to?(:isp_id) && !curr.isp_id.nil?
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
payload[:correlation_id] = curr.correlation_id if curr.respond_to?(:correlation_id) && curr.correlation_id
|
|
126
126
|
|
|
127
|
-
payload
|
|
127
|
+
inject_log_fields(payload, curr)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Mergea el resultado de `Current.log_fields` al payload, aplicando el mismo
|
|
131
|
+
# filtrado de claves sensibles que el resto del formatter. Si la subclass del
|
|
132
|
+
# host overrideó el hook y revienta, el error se traga: logging nunca debe
|
|
133
|
+
# afectar el flujo principal.
|
|
134
|
+
#
|
|
135
|
+
# @param payload [Hash]
|
|
136
|
+
# @param curr [Class] La clase Current configurada por la app host.
|
|
137
|
+
# @return [void]
|
|
138
|
+
def inject_log_fields(payload, curr)
|
|
139
|
+
return unless curr.respond_to?(:log_fields)
|
|
140
|
+
|
|
141
|
+
fields = curr.log_fields
|
|
142
|
+
return if fields.nil? || fields.empty?
|
|
143
|
+
|
|
144
|
+
payload.merge!(filter_sensitive_hash(fields))
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
128
147
|
end
|
|
129
148
|
|
|
130
149
|
# Inyecta cualquier etiqueta nativa (tags) de Rails que esté presente en el hilo actual.
|
data/lib/exis_ray/railtie.rb
CHANGED
|
@@ -44,16 +44,12 @@ module ExisRay
|
|
|
44
44
|
# y safe_constantize puede fallar aún con eager_load=true.
|
|
45
45
|
if (name = ExisRay.configuration.current_class).present?
|
|
46
46
|
klass = name.safe_constantize
|
|
47
|
-
if klass && !klass.<=(ExisRay::Current)
|
|
48
|
-
raise "ExisRay: current_class '#{name}' does not inherit from ExisRay::Current"
|
|
49
|
-
end
|
|
47
|
+
raise "ExisRay: current_class '#{name}' does not inherit from ExisRay::Current" if klass && !klass.<=(ExisRay::Current)
|
|
50
48
|
end
|
|
51
49
|
|
|
52
50
|
if (name = ExisRay.configuration.reporter_class).present?
|
|
53
51
|
klass = name.safe_constantize
|
|
54
|
-
if klass && !klass.<=(ExisRay::Reporter)
|
|
55
|
-
raise "ExisRay: reporter_class '#{name}' does not inherit from ExisRay::Reporter"
|
|
56
|
-
end
|
|
52
|
+
raise "ExisRay: reporter_class '#{name}' does not inherit from ExisRay::Reporter" if klass && !klass.<=(ExisRay::Reporter)
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
# Aplicamos el formateador JSON globalmente al logger ya instanciado de Rails
|
data/lib/exis_ray/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -71,8 +71,8 @@ ExisRay unifica trazabilidad distribuida, logging estructurado JSON, contexto de
|
|
|
71
71
|
2. `Tracer.parse_trace_id` extrae `root_id`, `self_id`, `called_from`, `total_time_so_far`
|
|
72
72
|
3. `ExisRay.sync_correlation_id` asigna `Tracer.correlation_id` a `Current.correlation_id`
|
|
73
73
|
4. Controller ejecuta `before_action` para setear `Current.user_id`, `Current.isp_id`
|
|
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
|
|
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`. El developer aporta `component` (modulo de negocio) y `event` (que paso); estos NO son auto-inyectados porque dependen del call site, no del contexto de ejecucion.
|
|
75
|
+
6. `LogSubscriber` emite un unico Hash al finalizar el request con campos default (`component`, `event`, `method`, `path`, `http_route`, `format`, `controller`, `action`, `http_status`, `duration_s`, `duration_human`, `view_runtime_s`, `db_runtime_s`, `user_agent_original`, `server_address`, y en error `error_class`/`error_message`/`exception.*`).
|
|
76
76
|
7. En llamadas salientes, `FaradayMiddleware`/`ActiveResourceInstrumentation` inyectan `propagation_trace_header` con `Tracer.generate_trace_header`
|
|
77
77
|
8. Al finalizar, `ActiveSupport::CurrentAttributes` hace reset automatico
|
|
78
78
|
|
|
@@ -163,6 +163,27 @@ Current.correlation_id? # => true si correlation_id es present?
|
|
|
163
163
|
|
|
164
164
|
Los setters auto-sincronizan con `ActiveResource::Base.headers` y `PaperTrail.request` cuando estan definidos.
|
|
165
165
|
|
|
166
|
+
#### Hook `log_fields` — inyectar campos custom en cada log
|
|
167
|
+
|
|
168
|
+
Class method overridable que retorna un Hash de campos extra para JsonFormatter. Cubre tanto **constantes de proceso** (frozen constants en la subclass) como **valores dinámicos per-request** (atributos de Current) en un solo lugar. Default `{}`.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
class Current < ExisRay::Current
|
|
172
|
+
TENANT_ID = ENV.fetch("TENANT_ID").freeze # static, frozen al boot
|
|
173
|
+
attribute :region # dynamic, per-request
|
|
174
|
+
|
|
175
|
+
def self.log_fields
|
|
176
|
+
{ tenant_id: TENANT_ID, region: region }.compact
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Reglas:
|
|
182
|
+
|
|
183
|
+
- `JsonFormatter` filtra claves sensibles del hash retornado (mismo regex que el resto del formatter).
|
|
184
|
+
- Si el override revienta, el formatter rescata silenciosamente (logging no afecta flujo principal).
|
|
185
|
+
- Precedencia: campos canónicos del Tracer y keys del mensaje del developer pisan `log_fields` en colisión. Solo sirve para agregar fields nuevos, no para overrideear los canónicos.
|
|
186
|
+
|
|
166
187
|
### ExisRay::Reporter (clase base abstracta)
|
|
167
188
|
|
|
168
189
|
```ruby
|
|
@@ -212,6 +233,32 @@ end
|
|
|
212
233
|
|
|
213
234
|
### ExisRay::LogSubscriber
|
|
214
235
|
|
|
236
|
+
Reemplaza Lograge. Se suscribe a `process_action.action_controller` y emite un Hash por request HTTP. Severity es `ERROR` si `http_status >= 500`, sino `INFO`.
|
|
237
|
+
|
|
238
|
+
**Campos default emitidos** (mergeados al payload JSON; nunca duplicarlos manualmente):
|
|
239
|
+
|
|
240
|
+
| Campo | Tipo | Notas |
|
|
241
|
+
|:------|:-----|:------|
|
|
242
|
+
| `component` | String | Siempre `"exis_ray"` |
|
|
243
|
+
| `event` | String | Siempre `"http_request"` |
|
|
244
|
+
| `method` | String | Verbo HTTP |
|
|
245
|
+
| `path` | String | URL concreta del request |
|
|
246
|
+
| `http_route` | String | Template (ej: `/users/:id`). Baja cardinalidad para dashboards |
|
|
247
|
+
| `format` | Symbol/String | `html`, `json`, etc. |
|
|
248
|
+
| `controller` | String | Class name del controller |
|
|
249
|
+
| `action` | String | Nombre del action |
|
|
250
|
+
| `http_status` | Integer | Status HTTP final |
|
|
251
|
+
| `duration_s` | Float | Segundos (Rails reporta ms, se convierte), redondeo 4 decimales |
|
|
252
|
+
| `duration_human` | String | Legible: `"42.5ms"`, `"1.25s"`, `"2 minutes 5 seconds"` |
|
|
253
|
+
| `view_runtime_s` | Float\|nil | Solo si Rails lo reporta |
|
|
254
|
+
| `db_runtime_s` | Float\|nil | Solo si ActiveRecord lo reporta |
|
|
255
|
+
| `user_agent_original` | String | Header `User-Agent` |
|
|
256
|
+
| `server_address` | String | Hostname sin puerto (de `Host` header) |
|
|
257
|
+
| `error_class`, `error_message` | String | Solo en fallo (legacy) |
|
|
258
|
+
| `exception.type`, `exception.message`, `exception.stacktrace` | String | Solo en fallo (OTel; stack limitado a 20 lineas) |
|
|
259
|
+
|
|
260
|
+
Para inyectar campos extra, sobreescribir `extra_fields`:
|
|
261
|
+
|
|
215
262
|
```ruby
|
|
216
263
|
class MyLogSubscriber < ExisRay::LogSubscriber
|
|
217
264
|
def self.extra_fields(event)
|
|
@@ -227,12 +274,41 @@ ExisRay.configure { |c| c.log_subscriber_class = "MyLogSubscriber" }
|
|
|
227
274
|
|
|
228
275
|
Se asigna automaticamente a `Rails.logger.formatter` cuando `log_format: :json`. Acepta tres tipos de mensaje:
|
|
229
276
|
|
|
230
|
-
- **Hash**: merge directo al payload JSON
|
|
231
|
-
- **String KV** (`"event=foo bar=baz"`): parsea pares y los eleva al root del JSON
|
|
232
|
-
- **String libre**: asigna al campo `body`
|
|
277
|
+
- **Hash**: merge directo al payload JSON. Util para payloads complejos o con valores nested.
|
|
278
|
+
- **String KV** (`"event=foo bar=baz"`): parsea pares y los eleva al root del JSON. Util para one-liners rapidos.
|
|
279
|
+
- **String libre**: asigna al campo `body` (OTel log body).
|
|
233
280
|
|
|
234
281
|
Casteo automatico: integers, floats, objetos JSON (`{...}`, `[...]`). Filtra claves sensibles (`password|secret|token|api_key|auth`) a `[FILTERED]`. Fallback a JSON minimo si el formateo falla.
|
|
235
282
|
|
|
283
|
+
#### Criterio auto-inyectado vs manual
|
|
284
|
+
|
|
285
|
+
- **Auto-inyectado** (formatter conoce desde `Tracer`/`Current`): contexto de **ejecucion** — quien hace el request, de donde viene, en que servicio, con que identidad.
|
|
286
|
+
- **Manual** (lo aporta cada `Rails.logger.*`): contexto del **call site** — que modulo (`component`) y que paso (`event`). El formatter no puede saber esto sin recorrer el stack en cada log.
|
|
287
|
+
|
|
288
|
+
Por eso `component` y `event` jamas se auto-inyectan, aunque el estandar Wispro los exija.
|
|
289
|
+
|
|
290
|
+
#### Ejemplos: KV vs Hash producen output equivalente
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# KV string — one-liner rapido
|
|
294
|
+
Rails.logger.info("component=billing event=invoice_paid invoice_id=42 total=199.99")
|
|
295
|
+
|
|
296
|
+
# Hash style — payloads complejos / nested
|
|
297
|
+
Rails.logger.info(component: "billing", event: "invoice_paid",
|
|
298
|
+
invoice: { id: 42, total: 199.99 })
|
|
299
|
+
|
|
300
|
+
# String libre — fallback, va a `body`
|
|
301
|
+
Rails.logger.info("usuario hizo click")
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Output JSON resultante (mismo para KV y Hash del ejemplo)
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{"time":"2026-05-11T09:15:00.123Z","level":"INFO","severity_number":9,"service":"box_radius_manager","service_version":"1.2.3","deployment_environment":"production","root_id":"1-abc","trace_id":"Root=1-abc;Self=...","source":"http","user_id":42,"isp_id":10,"correlation_id":"box_radius_manager;1-abc","component":"billing","event":"invoice_paid","invoice_id":42,"total":199.99}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Los campos hasta `correlation_id` los inyecta el formatter automaticamente. De `component` en adelante son los campos del mensaje del developer.
|
|
311
|
+
|
|
236
312
|
### Middlewares de propagacion
|
|
237
313
|
|
|
238
314
|
```ruby
|
|
@@ -290,6 +366,20 @@ Agrega `f.use ExisRay::FaradayMiddleware` al stack de Faraday. Solo inyecta head
|
|
|
290
366
|
**P: Puedo usar ExisRay sin JSON logging?**
|
|
291
367
|
Si. Con `log_format: :text` (default), ExisRay inyecta el `root_id` como tag de Rails via `config.log_tags`. El JsonFormatter y LogSubscriber no se activan.
|
|
292
368
|
|
|
369
|
+
**P: Por que no usar `ActiveSupport::TaggedLogging` con JSON logging?**
|
|
370
|
+
`TaggedLogging` agrega tags como texto plano al inicio de cada línea **antes** del formatter, lo cual rompe el JSON:
|
|
371
|
+
```
|
|
372
|
+
[request_id] {"time":"...","level":"INFO",...} # ← texto antes del JSON
|
|
373
|
+
```
|
|
374
|
+
`ExisRay::JsonFormatter` ya inyecta los tags (`request_id`, `trace_id`, etc.) como campos JSON. Usar ambos genera JSON inválido.
|
|
375
|
+
|
|
376
|
+
Configuración correcta en production.rb:
|
|
377
|
+
```ruby
|
|
378
|
+
config.colorize_logging = false
|
|
379
|
+
config.logger = ActiveSupport::Logger.new(STDOUT)
|
|
380
|
+
config.logger.formatter = ExisRay::JsonFormatter
|
|
381
|
+
```
|
|
382
|
+
|
|
293
383
|
**P: Que pasa con los logs de Sidekiq (el propio logger de Sidekiq)?**
|
|
294
384
|
Si `json_logs?` es true, el Railtie asigna `Sidekiq.logger.formatter = ExisRay::JsonFormatter.new`, asi los logs internos de Sidekiq tambien salen en JSON.
|
|
295
385
|
|
|
@@ -112,4 +112,38 @@ RSpec.describe ExisRay::Current do
|
|
|
112
112
|
TestCurrent.correlation_id = "corr-1"
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
|
+
|
|
116
|
+
describe ".log_fields hook" do
|
|
117
|
+
it "retorna un Hash vacío por defecto" do
|
|
118
|
+
expect(TestCurrent.log_fields).to eq({})
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "permite que la subclass overridee el método para inyectar campos custom" do
|
|
122
|
+
stub_const("TenantCurrent", Class.new(ExisRay::Current) do
|
|
123
|
+
def self.log_fields
|
|
124
|
+
{ tenant_id: "42", region: "us-east-1" }
|
|
125
|
+
end
|
|
126
|
+
end)
|
|
127
|
+
|
|
128
|
+
expect(TenantCurrent.log_fields).to eq(tenant_id: "42", region: "us-east-1")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "soporta combinar constantes de proceso con atributos per-request" do
|
|
132
|
+
stub_const("MixedCurrent", Class.new(ExisRay::Current) do
|
|
133
|
+
attribute :region
|
|
134
|
+
|
|
135
|
+
STATIC_TENANT = "tenant-42"
|
|
136
|
+
|
|
137
|
+
def self.log_fields
|
|
138
|
+
{ tenant_id: STATIC_TENANT, region: region }.compact
|
|
139
|
+
end
|
|
140
|
+
end)
|
|
141
|
+
MixedCurrent.region = "us-east-1"
|
|
142
|
+
|
|
143
|
+
expect(MixedCurrent.log_fields).to eq(tenant_id: "tenant-42", region: "us-east-1")
|
|
144
|
+
|
|
145
|
+
MixedCurrent.reset
|
|
146
|
+
expect(MixedCurrent.log_fields).to eq(tenant_id: "tenant-42")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
115
149
|
end
|
|
@@ -261,6 +261,91 @@ RSpec.describe ExisRay::JsonFormatter do
|
|
|
261
261
|
expect(result).not_to have_key("user_id")
|
|
262
262
|
end
|
|
263
263
|
end
|
|
264
|
+
|
|
265
|
+
describe "inyeccion de Current.log_fields" do
|
|
266
|
+
# Mock estilo ActiveSupport::CurrentAttributes que combina los atributos canónicos
|
|
267
|
+
# con un hook log_fields. Por defecto retorna {}, los tests lo redefinen vía stub.
|
|
268
|
+
let(:current_class_double) do
|
|
269
|
+
Class.new do
|
|
270
|
+
class << self
|
|
271
|
+
attr_accessor :user_id, :isp_id, :correlation_id, :_log_fields
|
|
272
|
+
|
|
273
|
+
def respond_to_missing?(method, *)
|
|
274
|
+
%i[user_id isp_id correlation_id log_fields].include?(method) || super
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.log_fields
|
|
279
|
+
_log_fields || {}
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
before do
|
|
285
|
+
allow(ExisRay).to receive(:current_class).and_return(current_class_double)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "inyecta los campos retornados por log_fields en el payload" do
|
|
289
|
+
current_class_double._log_fields = { tenant_id: "42", region: "us-east-1" }
|
|
290
|
+
|
|
291
|
+
result = call("event=boot")
|
|
292
|
+
|
|
293
|
+
# tenant_id="42" se castea a Integer por filter_sensitive_hash (igual que cualquier otro KV)
|
|
294
|
+
expect(result).to include("tenant_id" => 42, "region" => "us-east-1", "event" => "boot")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it "no agrega keys cuando log_fields retorna hash vacío" do
|
|
298
|
+
current_class_double._log_fields = {}
|
|
299
|
+
|
|
300
|
+
result = call("event=boot")
|
|
301
|
+
|
|
302
|
+
expect(result).not_to have_key("tenant_id")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it "no agrega keys cuando log_fields retorna nil" do
|
|
306
|
+
current_class_double._log_fields = nil
|
|
307
|
+
|
|
308
|
+
result = call("event=boot")
|
|
309
|
+
|
|
310
|
+
expect(result.keys).to contain_exactly("time", "level", "severity_number", "service", "event")
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it "el mensaje del developer pisa log_fields (override por call site)" do
|
|
314
|
+
current_class_double._log_fields = { tenant_id: "from-current" }
|
|
315
|
+
|
|
316
|
+
result = call("event=override tenant_id=from-message")
|
|
317
|
+
|
|
318
|
+
expect(result["tenant_id"]).to eq("from-message")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "filtra claves sensibles" do
|
|
322
|
+
current_class_double._log_fields = { api_key: "leaked", tenant_id: "42" }
|
|
323
|
+
|
|
324
|
+
result = call("event=boot")
|
|
325
|
+
|
|
326
|
+
expect(result["api_key"]).to eq("[FILTERED]")
|
|
327
|
+
expect(result["tenant_id"]).to eq(42)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it "no rompe el formatter si la subclass overrideó log_fields con un método que revienta" do
|
|
331
|
+
allow(current_class_double).to receive(:log_fields).and_raise(StandardError, "boom")
|
|
332
|
+
|
|
333
|
+
expect { call("event=boot") }.not_to raise_error
|
|
334
|
+
result = call("event=boot")
|
|
335
|
+
expect(result["event"]).to eq("boot")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it "no falla si Current configurado no implementa log_fields (backwards compat)" do
|
|
339
|
+
legacy_current = Class.new do
|
|
340
|
+
class << self
|
|
341
|
+
attr_accessor :user_id, :isp_id, :correlation_id
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
allow(ExisRay).to receive(:current_class).and_return(legacy_current)
|
|
345
|
+
|
|
346
|
+
expect { call("event=boot") }.not_to raise_error
|
|
347
|
+
end
|
|
348
|
+
end
|
|
264
349
|
end
|
|
265
350
|
|
|
266
351
|
describe "#kv_string? (privado)" do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: exis_ray
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel Edera
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -87,8 +87,8 @@ licenses:
|
|
|
87
87
|
metadata:
|
|
88
88
|
homepage_uri: https://github.com/gedera/exis_ray
|
|
89
89
|
source_code_uri: https://github.com/gedera/exis_ray
|
|
90
|
-
changelog_uri: https://github.com/gedera/exis_ray/blob/v0.
|
|
91
|
-
documentation_uri: https://github.com/gedera/exis_ray/blob/v0.
|
|
90
|
+
changelog_uri: https://github.com/gedera/exis_ray/blob/v0.8.0/CHANGELOG.md
|
|
91
|
+
documentation_uri: https://github.com/gedera/exis_ray/blob/v0.8.0/skill
|
|
92
92
|
post_install_message:
|
|
93
93
|
rdoc_options: []
|
|
94
94
|
require_paths:
|