exis_ray 0.3.4 → 0.4.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: 368df7fa55a8dc849917a5854026d50752f3701afede7bc9418c83e229a1fe50
4
- data.tar.gz: 7998fda22e20966eae15450faf4c2e73e103c24e23b2decd3c41b76c605ec036
3
+ metadata.gz: c5162f50fce688356ff17e9adff2464f6bc7ba9c83ee2e3a1f662dc4568fb6d3
4
+ data.tar.gz: b2559412f57a487971edbf42cd0b24e18bbbe9fe33890096e8e6769647895e49
5
5
  SHA512:
6
- metadata.gz: 791eca1e5cd91703ebd7d43803b09a55f718918ac65808eecd9ae8d8141944297f9d06ab1f7b46519dcd0018e0174ebc307d14a297eefa9581440e2c6448cdb0
7
- data.tar.gz: e2c88c9e68556110a8ab757009e2f824849123202f692aa71d15015d3c4aaa385fc90cd093bb7657f112a1a217f98c7ed4385de11d0c6e1f9cf3c5a5b36abfee
6
+ metadata.gz: 9efba79f9584005d1beb8c1d749dc4efb54959aa47433a671b611cbc15ce93bb4f081f390047b736039a1fad1d621cc2d5661fb82c3f340c2f57a7bddc461f19
7
+ data.tar.gz: a2878c45410719109283a4a2b44201bc0b45fbe5a7ee2c70ba020a5b255392ab42af18fa52995f75e98dc4e32ae0727c718429b590daa3af797202e1488db897
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [0.4.0] - 2026-03-23
2
+
3
+ ### Breaking Changes
4
+ - **Lograge removido como dependencia.** ExisRay ya no depende de Lograge para el logging estructurado de requests HTTP. Si tu app usaba `config.lograge.custom_options`, migrá al nuevo mecanismo de subclase (ver más abajo).
5
+
6
+ ### Added
7
+ - **`ExisRay::LogSubscriber`:** Nuevo subscriber propio que reemplaza Lograge. Se suscribe a `process_action.action_controller` y emite un Hash estructurado directamente al logger, compatible con Rails 6, 7 y 8.
8
+ - Suprime `ActionController::LogSubscriber` y `ActionView::LogSubscriber` (Rails 3.0+, sin cambios en 6/7/8).
9
+ - Suprime `Rails::Rack::Logger` para eliminar las líneas "Started GET /..." (Rails 3.2+, firma `call_app(request, env)` desde Rails 5.0+).
10
+ - Usa `notifier.all_listeners_for` en Rails 7.1+ y `notifier.listeners_for` en Rails 6/7.0 para desuscribir los subscribers por defecto. Si `all_listeners_for` cambia en futuras versiones, revisar `ActiveSupport::Notifications::Fanout`.
11
+ - **`config.log_subscriber_class`:** Nueva opción de configuración para registrar una subclase de `ExisRay::LogSubscriber`. Permite inyectar campos extra en cada log de request HTTP sobreescribiendo `self.extra_fields(event)`. Si no se configura, se usa `ExisRay::LogSubscriber` directamente sin campos extra.
12
+
13
+ ### Migration Guide
14
+ Si usabas `config.lograge.custom_options`, el equivalente es:
15
+
16
+ ```ruby
17
+ # Antes
18
+ config.lograge.custom_options = ->(event) { { user_id: Current.user_id } }
19
+
20
+ # Después — app/models/my_log_subscriber.rb
21
+ class MyLogSubscriber < ExisRay::LogSubscriber
22
+ def self.extra_fields(event)
23
+ { user_id: Current.user_id }
24
+ end
25
+ end
26
+
27
+ # config/initializers/exis_ray.rb
28
+ ExisRay.configure do |config|
29
+ config.log_subscriber_class = "MyLogSubscriber"
30
+ end
31
+ ```
32
+
1
33
  ## [0.3.4] - 2026-03-23
2
34
 
3
35
  ### Fixed
data/README.md CHANGED
@@ -31,7 +31,7 @@ And then execute:
31
31
  $ bundle install
32
32
  ```
33
33
 
34
- *(Note: ExisRay automatically installs and configures `lograge` as a dependency to handle HTTP log condensation).*
34
+ *(Note: ExisRay handles HTTP log condensation internally via `ExisRay::LogSubscriber`, compatible with Rails 6, 7 and 8. Lograge is no longer required).*
35
35
 
36
36
  ---
37
37
 
@@ -242,26 +242,38 @@ config.log_tags = [
242
242
  {"time":"2026-03-12T14:30:00Z","service":"App-HTTP","tags":["abcd-1234-uuid","CustomValue"],"method":"GET","status":200}
243
243
  ```
244
244
 
245
- ### 3. Extending JSON with Custom Properties (Lograge)
245
+ ### 3. Extending JSON with Custom HTTP Fields
246
246
 
247
- If you prefer to inject key-value pairs directly into the root of the JSON (which is highly recommended for querying in Datadog, Kibana, etc.), you can leverage Lograge's `custom_options` in your host application.
247
+ To inject extra fields into each HTTP request log, create a subclass of `ExisRay::LogSubscriber` and override `self.extra_fields`:
248
248
 
249
- ExisRay will flawlessly merge these attributes with the core Trace ID and Business Context.
249
+ **File:** `app/models/my_log_subscriber.rb`
250
+ ```ruby
251
+ class MyLogSubscriber < ExisRay::LogSubscriber
252
+ def self.extra_fields(event)
253
+ {
254
+ ip_address: event.payload[:ip],
255
+ user_agent: event.payload[:headers]["HTTP_USER_AGENT"]
256
+ }
257
+ end
258
+ end
259
+ ```
250
260
 
251
- **File:** `config/environments/production.rb`
261
+ Then register it in your initializer:
262
+
263
+ **File:** `config/initializers/exis_ray.rb`
252
264
  ```ruby
253
- config.lograge.custom_options = lambda do |event|
254
- {
255
- ip_address: event.payload[:ip],
256
- browser: event.payload[:user_agent]
257
- }
265
+ ExisRay.configure do |config|
266
+ config.log_subscriber_class = "MyLogSubscriber"
258
267
  end
259
268
  ```
269
+
260
270
  **Output:**
261
271
  ```json
262
- {"time":"2026-03-12T14:30:00Z","service":"App-HTTP","root_id":"Root=1-65a...","method":"GET","ip_address":"192.168.1.1","browser":"Chrome","status":200}
272
+ {"time":"2026-03-12T14:30:00Z","service":"App-HTTP","root_id":"Root=1-65a...","method":"GET","ip_address":"192.168.1.1","user_agent":"Chrome","status":200}
263
273
  ```
264
274
 
275
+ If you don't need extra fields, skip this step — `ExisRay::LogSubscriber` is used by default with no additional fields.
276
+
265
277
  ---
266
278
 
267
279
  ## 🏗 Architecture
@@ -271,6 +283,7 @@ end
271
283
  * **`ExisRay::Reporter`**: The observability layer. Bridges the gap between your app and Sentry.
272
284
  * **`ExisRay::JsonFormatter`**: The central logging engine. Intercepts HTTP, Sidekiq, and Tasks to output clean JSON.
273
285
  * **KV String Parser:** It automatically detects if a log message (String) uses `key=value` format. If so, it parses the pairs and elevates them to the root of the JSON. For example, `Rails.logger.info "event=boot status=ok"` becomes `{"event":"boot","status":"ok",...}`. It supports quoted values with spaces: `message="something went wrong"`.
286
+ * **`ExisRay::LogSubscriber`**: Replaces Lograge for HTTP request logging. Subscribes to `process_action.action_controller` and suppresses Rails' default multi-line log subscribers. Compatible with Rails 6, 7, and 8. Subclass it and override `self.extra_fields(event)` to inject custom fields.
274
287
  * **`ExisRay::TaskMonitor`**: The entry point for non-HTTP processes.
275
288
 
276
289
  ## License
@@ -31,6 +31,13 @@ module ExisRay
31
31
  # @example :json
32
32
  attr_accessor :log_format
33
33
 
34
+ # @!attribute [rw] log_subscriber_class
35
+ # @return [String, nil] Nombre de la subclase de ExisRay::LogSubscriber de la app host.
36
+ # Usada para inyectar campos extra en cada log de request HTTP via `extra_fields`.
37
+ # Si es nil, se usa ExisRay::LogSubscriber directamente (sin campos extra).
38
+ # @example 'MyLogSubscriber'
39
+ attr_accessor :log_subscriber_class
40
+
34
41
  # Inicializa la configuración con valores por defecto compatibles con AWS X-Ray.
35
42
  def initialize
36
43
  @trace_header = 'HTTP_X_AMZN_TRACE_ID'
@@ -38,6 +45,7 @@ module ExisRay
38
45
  @reporter_class = 'Reporter'
39
46
  @current_class = 'Current'
40
47
  @log_format = :text
48
+ @log_subscriber_class = nil
41
49
  end
42
50
 
43
51
  # Indica si la aplicación está configurada para emitir logs en formato estructurado (JSON).
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/log_subscriber"
4
+
5
+ module ExisRay
6
+ # Reemplaza Lograge para el logging estructurado de requests HTTP en Rails 6, 7 y 8.
7
+ #
8
+ # Se suscribe a `process_action.action_controller` y emite un Hash al logger,
9
+ # que JsonFormatter convierte a JSON. Suprime los log subscribers por defecto
10
+ # de Rails (ActionController, ActionView, Rails::Rack::Logger) para evitar
11
+ # líneas de log duplicadas o en formato texto.
12
+ #
13
+ # Para inyectar campos extra en cada log de request, heredá esta clase y
14
+ # sobreescribí `extra_fields`:
15
+ #
16
+ # class MyLogSubscriber < ExisRay::LogSubscriber
17
+ # def self.extra_fields(event)
18
+ # { user_agent: event.payload[:headers]["HTTP_USER_AGENT"] }
19
+ # end
20
+ # end
21
+ #
22
+ # Luego configurá la subclase:
23
+ #
24
+ # ExisRay.configure do |config|
25
+ # config.log_subscriber_class = "MyLogSubscriber"
26
+ # end
27
+ #
28
+ class LogSubscriber < ActiveSupport::LogSubscriber
29
+ # Procesa el evento de finalización de un request HTTP y lo emite como Hash estructurado.
30
+ #
31
+ # @param event [ActiveSupport::Notifications::Event]
32
+ # @return [void]
33
+ def process_action(event)
34
+ payload = build_payload(event)
35
+ logger.info(payload)
36
+ rescue StandardError
37
+ # El logger nunca debe interrumpir el flujo del request.
38
+ end
39
+
40
+ # Hook para que las subclases inyecten campos extra en cada log de request.
41
+ # Por defecto retorna un Hash vacío.
42
+ #
43
+ # @param event [ActiveSupport::Notifications::Event]
44
+ # @return [Hash]
45
+ def self.extra_fields(_event)
46
+ {}
47
+ end
48
+
49
+ # --- Instalación y supresión de subscribers ---
50
+
51
+ # Activa el subscriber correcto (subclase configurada o ExisRay::LogSubscriber por defecto)
52
+ # y suprime los log subscribers por defecto de Rails.
53
+ #
54
+ # @return [void]
55
+ def self.install!
56
+ suppress_default_log_subscribers!
57
+ suppress_rack_logger!
58
+ subscriber_class.attach_to(:action_controller)
59
+ end
60
+
61
+ private
62
+
63
+ def build_payload(event)
64
+ payload = event.payload
65
+ status = payload[:status] || exception_status(payload[:exception])
66
+
67
+ data = {
68
+ method: payload[:method],
69
+ path: payload[:path],
70
+ format: payload[:format],
71
+ controller: payload[:controller],
72
+ action: payload[:action],
73
+ status: status,
74
+ duration: event.duration.round(2),
75
+ view: payload[:view_runtime]&.round(2),
76
+ db: payload[:db_runtime]&.round(2)
77
+ }
78
+
79
+ data.merge!(self.class.extra_fields(event))
80
+ data.compact
81
+ end
82
+
83
+ # Infiere el status HTTP desde el nombre de la excepción cuando el request
84
+ # terminó con una excepción no rescatada (payload[:status] es nil).
85
+ #
86
+ # @param exception_info [Array(String, String), nil] [nombre_clase, mensaje]
87
+ # @return [Integer]
88
+ def exception_status(exception_info)
89
+ return 500 unless exception_info
90
+
91
+ exception_class_name = exception_info.first
92
+ # ActionDispatch::ExceptionWrapper disponible desde Rails 3.2+
93
+ ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
94
+ rescue StandardError
95
+ 500
96
+ end
97
+
98
+ # Resuelve la clase a attachar: subclase configurada o ExisRay::LogSubscriber.
99
+ #
100
+ # @return [Class]
101
+ def self.subscriber_class
102
+ klass_name = ExisRay.configuration.log_subscriber_class
103
+ return self unless klass_name.present?
104
+
105
+ klass_name.safe_constantize || self
106
+ end
107
+
108
+ # Suprime ActionController::LogSubscriber y ActionView::LogSubscriber
109
+ # para evitar los logs multi-línea por defecto de Rails.
110
+ #
111
+ # Introducido en Rails 3.0. Presente sin cambios en Rails 6, 7 y 8.
112
+ #
113
+ # @return [void]
114
+ def self.suppress_default_log_subscribers!
115
+ require "action_controller/log_subscriber"
116
+ require "action_view/log_subscriber"
117
+
118
+ ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
119
+ case subscriber
120
+ when ActionController::LogSubscriber
121
+ unsubscribe_subscriber(:action_controller, subscriber)
122
+ when ActionView::LogSubscriber
123
+ unsubscribe_subscriber(:action_view, subscriber)
124
+ end
125
+ end
126
+ end
127
+
128
+ # Suprime las líneas "Started GET /..." emitidas por Rails::Rack::Logger.
129
+ #
130
+ # Rails::Rack::Logger introducido en Rails 3.2. La firma de `call_app` recibe
131
+ # (request, env) desde Rails 5.0+. Compatible con Rails 6, 7 y 8.
132
+ #
133
+ # @return [void]
134
+ def self.suppress_rack_logger!
135
+ require "rails/rack/logger"
136
+
137
+ ::Rails::Rack::Logger.class_eval do
138
+ def call_app(request, env) # rubocop:disable Lint/UnusedMethodArgument
139
+ status, headers, body = @app.call(env)
140
+ [status, headers, ::Rack::BodyProxy.new(body) {}]
141
+ ensure
142
+ ActiveSupport::LogSubscriber.flush_all!
143
+ end
144
+ end
145
+ end
146
+
147
+ # Desuscribe un subscriber de todas las notificaciones de un namespace.
148
+ #
149
+ # API de notificaciones:
150
+ # - Rails 6 / 7.0: `notifier.listeners_for(event_name)`
151
+ # - Rails 7.1+: `notifier.all_listeners_for(event_name)` (listeners_for fue deprecado)
152
+ # - Rails 8: solo `all_listeners_for`
153
+ #
154
+ # Si `all_listeners_for` desaparece en futuras versiones, revisar
155
+ # ActiveSupport::Notifications::Fanout para el API vigente.
156
+ #
157
+ # @param namespace [Symbol]
158
+ # @param subscriber [ActiveSupport::LogSubscriber]
159
+ # @return [void]
160
+ def self.unsubscribe_subscriber(namespace, subscriber)
161
+ events = subscriber.public_methods(false).reject { |m| m.to_s == "call" }
162
+ notifier = ActiveSupport::Notifications.notifier
163
+
164
+ events.each do |event|
165
+ event_name = "#{event}.#{namespace}"
166
+ listeners = if notifier.respond_to?(:all_listeners_for)
167
+ # Rails 7.1+ (introducido en 7.1.0)
168
+ notifier.all_listeners_for(event_name)
169
+ else
170
+ # Rails 6 / 7.0
171
+ notifier.listeners_for(event_name)
172
+ end
173
+
174
+ listeners.each do |listener|
175
+ delegate = listener.instance_variable_get(:@delegate)
176
+ ActiveSupport::Notifications.unsubscribe(listener) if delegate == subscriber
177
+ end
178
+ end
179
+ end
180
+
181
+ private_class_method :suppress_default_log_subscribers!,
182
+ :suppress_rack_logger!,
183
+ :unsubscribe_subscriber,
184
+ :subscriber_class
185
+ end
186
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/railtie"
4
- require "lograge" # Requerido globalmente para que su propio Railtie se registre en el boot
5
4
 
6
5
  module ExisRay
7
6
  # Integración automática de la gema con el ecosistema de Ruby on Rails.
@@ -17,14 +16,11 @@ module ExisRay
17
16
  app.middleware.insert_after ActionDispatch::RequestId, ExisRay::HttpMiddleware
18
17
  end
19
18
 
20
- # 2. Configuración de Estrategia de Logging (Lograge y Tags)
19
+ # 2. Configuración de Estrategia de Logging
21
20
  # CLAVE: Usamos `after: :load_config_initializers` para garantizar que la app
22
21
  # ya haya leído `config/initializers/exis_ray.rb` antes de tomar esta decisión.
23
22
  initializer "exis_ray.configure_logging", after: :load_config_initializers do |app|
24
- if ExisRay.configuration.json_logs?
25
- app.config.lograge.enabled = true
26
- app.config.lograge.formatter = Lograge::Formatters::Raw.new
27
- else
23
+ unless ExisRay.configuration.json_logs?
28
24
  # Comportamiento legacy: Text Plain Tags
29
25
  app.config.log_tags ||= []
30
26
  app.config.log_tags << proc do
@@ -39,6 +35,12 @@ module ExisRay
39
35
  # Aplicamos el formateador JSON globalmente al logger ya instanciado de Rails
40
36
  if ExisRay.configuration.json_logs? && Rails.logger
41
37
  Rails.logger.formatter = ExisRay::JsonFormatter.new
38
+
39
+ # Activamos nuestro LogSubscriber HTTP y suprimimos los de Rails por defecto.
40
+ # Reemplaza Lograge, compatible con Rails 6, 7 y 8.
41
+ require "exis_ray/log_subscriber"
42
+ ExisRay::LogSubscriber.install!
43
+
42
44
  Rails.logger.info({ message: "[ExisRay] JSON Logging unificado activado." })
43
45
  end
44
46
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.3.4"
5
+ VERSION = "0.4.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exis_ray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Edera
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.0'
41
- - !ruby/object:Gem::Dependency
42
- name: lograge
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0.11'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0.11'
55
41
  description: Gema que gestiona el contexto de request, logs y propagación de headers.
56
42
  email:
57
43
  - gab.edera@gmail.com
@@ -70,6 +56,7 @@ files:
70
56
  - lib/exis_ray/faraday_middleware.rb
71
57
  - lib/exis_ray/http_middleware.rb
72
58
  - lib/exis_ray/json_formatter.rb
59
+ - lib/exis_ray/log_subscriber.rb
73
60
  - lib/exis_ray/railtie.rb
74
61
  - lib/exis_ray/reporter.rb
75
62
  - lib/exis_ray/sidekiq/client_middleware.rb