exis_ray 0.7.2 → 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 +8 -0
- data/CLAUDE.md +1 -0
- data/README.md +33 -0
- 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 +21 -0
- 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,11 @@
|
|
|
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
|
+
|
|
1
9
|
## [0.7.2] - 2026-05-11
|
|
2
10
|
|
|
3
11
|
### Documentación
|
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).
|
|
@@ -310,6 +342,7 @@ config.logger.formatter = ExisRay::JsonFormatter
|
|
|
310
342
|
| `correlation_id` | Cuando `Current.correlation_id` está presente |
|
|
311
343
|
| `user_id` | Cuando `Current.user_id` no es nil |
|
|
312
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 |
|
|
313
346
|
| `sidekiq_job` | Solo en procesos Sidekiq |
|
|
314
347
|
| `task` | Solo en procesos TaskMonitor |
|
|
315
348
|
| `tags` | Solo si hay Rails tagged logging activo |
|
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
|
@@ -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
|
|
@@ -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-05-
|
|
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:
|