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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dd52ddf30f1d4eaaaad95ed4acde4bd94c8dd4a01b20853f42ddd71887c0876
4
- data.tar.gz: 329163a0bbbd0473e8da70971822c645de763f6a2c99eb4ccf1c973603cd700e
3
+ metadata.gz: 95101aa6f502c3c839070a556ab984244493d9f8bbabaad2b3b4e2e83b19cc67
4
+ data.tar.gz: af2097e279144370005dd457effbeaf961a11938f191a9aa30053efe717f9b12
5
5
  SHA512:
6
- metadata.gz: 9eb8bb340dae5b7f423e206ed1fa708caf0b599be0bfc3dd9f1d71209485cf6f2a17a06c9805167cd4988cae20b751f684fda2253d5983efa3c3d9e456e51849
7
- data.tar.gz: e18cf7eaa0962d39591072eb9241df8c950c0d1d1c1786aaff4d84ebc4acd1b6ff993b280f5bedc6175bbabd56a0a75f0e8f92a50b899051a1942d21e91888fc
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 |
@@ -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
- return unless curr.respond_to?(:correlation_id) && curr.correlation_id
125
+ payload[:correlation_id] = curr.correlation_id if curr.respond_to?(:correlation_id) && curr.correlation_id
126
126
 
127
- payload[:correlation_id] = curr.correlation_id
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.
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.7.2"
5
+ VERSION = "0.8.0"
6
6
  end
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.7.2
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 00:00:00.000000000 Z
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.7.2/CHANGELOG.md
91
- documentation_uri: https://github.com/gedera/exis_ray/blob/v0.7.2/skill
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: