exis_ray 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/Rakefile +8 -0
- data/lib/exis_ray/active_resource_instrumentation.rb +39 -0
- data/lib/exis_ray/configuration.rb +36 -0
- data/lib/exis_ray/current.rb +112 -0
- data/lib/exis_ray/faraday_middleware.rb +19 -0
- data/lib/exis_ray/http_middleware.rb +30 -0
- data/lib/exis_ray/railtie.rb +60 -0
- data/lib/exis_ray/reporter.rb +187 -0
- data/lib/exis_ray/sidekiq/client_middleware.rb +31 -0
- data/lib/exis_ray/sidekiq/server_middleware.rb +103 -0
- data/lib/exis_ray/task_monitor.rb +55 -0
- data/lib/exis_ray/tracer.rb +111 -0
- data/lib/exis_ray/version.rb +6 -0
- data/lib/exis_ray.rb +88 -0
- data/sig/exis_ray.rbs +4 -0
- metadata +89 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 10abd6a535e59a6d883a669567f45450fdbf694163699c2d5fb3cab4436a6ba9
|
|
4
|
+
data.tar.gz: c64fa72c61bc02921264714ec334ceaa8aee37b6507ebc42e9936f1f2eca86b5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c3bda3764d45ce3de886ed7a48d79df083c62bd48e84ac42e832f77bd736d35d333a723167809b8db6d1aceb21a6b9dd3c9ae42d7b359b9a6b4bb6c78da0b5c1
|
|
7
|
+
data.tar.gz: 8e7b43e3fea6235d93365f89efa180a870785cbfb3329a380563aee4f69334c16d8703599104e12c746c5b06a1ee8af527bbccac19595e492ff172f0b85a3390
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 gabriel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# ExisRay
|
|
2
|
+
|
|
3
|
+
**ExisRay** is a robust observability framework designed for Ruby on Rails microservices. It unifies **Distributed Tracing** (AWS X-Ray compatible), **Business Context Propagation**, and **Error Reporting** into a single, cohesive gem.
|
|
4
|
+
|
|
5
|
+
It acts as the backbone of your architecture, ensuring that every request, background task (Sidekiq/Cron), log line, and external API call carries the necessary context to debug issues across a distributed system.
|
|
6
|
+
|
|
7
|
+
## 馃殌 Features
|
|
8
|
+
|
|
9
|
+
* **Distributed Tracing:** Automatically parses, generates, and propagates Trace headers (compatible with AWS ALB `X-Amzn-Trace-Id`).
|
|
10
|
+
* **Unified Logging:** Injects the global `Root ID` into every Rails log line automatically, making Kibana/CloudWatch filtering effortless.
|
|
11
|
+
* **Context Management:** Thread-safe storage for business identity (`User`, `ISP`, `CorrelationId`) with automatic cleanup.
|
|
12
|
+
* **Error Reporting:** A wrapper for Sentry (Legacy & Modern SDKs) that enriches errors with the full trace and business context.
|
|
13
|
+
* **Sidekiq Integration:** Automatic context propagation (User/ISP/Trace) between the Enqueuer and the Worker.
|
|
14
|
+
* **Task Monitor:** A specialized monitor for Rake/Cron tasks to initialize traces where no HTTP request exists.
|
|
15
|
+
* **HTTP Clients:** Automatically patches `ActiveResource` and provides middleware for `Faraday`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 馃摝 Installation
|
|
20
|
+
|
|
21
|
+
Add this line to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'exis_ray'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
And then execute:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
$ bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 鈿欙笍 Configuration
|
|
36
|
+
|
|
37
|
+
Create an initializer to configure the behavior. This is crucial to link ExisRay with your specific application logic.
|
|
38
|
+
|
|
39
|
+
**File:** `config/initializers/exis_ray.rb`
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
ExisRay.configure do |config|
|
|
43
|
+
# 1. Trace Header (Incoming)
|
|
44
|
+
# The HTTP header used to read the Trace ID from the Load Balancer (Rack format).
|
|
45
|
+
# Default: 'HTTP_X_AMZN_TRACE_ID' (AWS Standard).
|
|
46
|
+
config.trace_header = 'HTTP_X_WP_TRACE_ID'
|
|
47
|
+
|
|
48
|
+
# 2. Propagation Header (Outgoing)
|
|
49
|
+
# The header sent to downstream services via ActiveResource/Faraday.
|
|
50
|
+
config.propagation_trace_header = 'X-Wp-Trace-Id'
|
|
51
|
+
|
|
52
|
+
# 3. Dynamic Classes (Required)
|
|
53
|
+
# Link your app's specific classes to the gem.
|
|
54
|
+
# We use Strings to avoid "uninitialized constant" errors during boot.
|
|
55
|
+
config.current_class = 'Current' # Your Context Model
|
|
56
|
+
config.reporter_class = 'Choto' # Your Sentry Wrapper
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 馃摉 Implementation Guide
|
|
63
|
+
|
|
64
|
+
### 1. Define Business Context (`Current`)
|
|
65
|
+
|
|
66
|
+
Inherit from `ExisRay::Current` to manage your global state. This class handles thread-safety and ensures data is wiped after every request.
|
|
67
|
+
|
|
68
|
+
**File:** `app/models/current.rb`
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class Current < ExisRay::Current
|
|
72
|
+
# Add app-specific attributes here
|
|
73
|
+
attribute :billing_cycle, :permissions
|
|
74
|
+
|
|
75
|
+
# ExisRay provides: user_id, isp_id, correlation_id
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Define Error Reporter (`Reporter`)
|
|
80
|
+
|
|
81
|
+
Inherit from `ExisRay::Reporter` to standardize error handling. This wrapper automatically attaches the `Trace ID`, `User`, `ISP`, and `Tags` to every Sentry event.
|
|
82
|
+
|
|
83
|
+
**File:** `app/models/choto.rb`
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class Choto < ExisRay::Reporter
|
|
87
|
+
# Optional hook to add service-specific context
|
|
88
|
+
def self.build_custom_context
|
|
89
|
+
if ExisRay.current_class.respond_to?(:olt)
|
|
90
|
+
add_tags(olt_id: ExisRay.current_class.olt&.id)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3. Hydrate Context (Controller)
|
|
97
|
+
|
|
98
|
+
In your `ApplicationController`, verify the incoming request and set the context. ExisRay handles the Trace ID automatically, you just handle the Business Logic.
|
|
99
|
+
|
|
100
|
+
**File:** `app/controllers/application_controller.rb`
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
before_action :set_exis_ray_context
|
|
104
|
+
|
|
105
|
+
def set_exis_ray_context
|
|
106
|
+
# 1. User Context (e.g., from Devise)
|
|
107
|
+
Current.user = current_user if current_user
|
|
108
|
+
|
|
109
|
+
# 2. ISP Context (e.g., from Headers)
|
|
110
|
+
Current.isp_id = request.headers['X-Isp-Id']
|
|
111
|
+
|
|
112
|
+
# Note: Setting these automatically prepares headers for ActiveResource
|
|
113
|
+
# and tags for Sentry.
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 馃洜 Usage Scenarios
|
|
120
|
+
|
|
121
|
+
### A. Automatic Sidekiq Integration
|
|
122
|
+
|
|
123
|
+
If `Sidekiq` is present, ExisRay automatically configures Client and Server middlewares. **No code changes are required in your workers.**
|
|
124
|
+
|
|
125
|
+
**How it works:**
|
|
126
|
+
1. **Enqueue:** When you call `Worker.perform_async`, the current `Trace ID` and `Current` attributes are injected into the job payload.
|
|
127
|
+
2. **Process:** When the worker executes, `Current` is hydrated with the original data.
|
|
128
|
+
3. **Logs:** Sidekiq logs will show the same `[Root=...]` ID as the web request.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Controller
|
|
132
|
+
def create
|
|
133
|
+
# Trace ID: A, User: 42
|
|
134
|
+
HardWorker.perform_async(100)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Worker
|
|
138
|
+
class HardWorker
|
|
139
|
+
include Sidekiq::Worker
|
|
140
|
+
def perform(amount)
|
|
141
|
+
puts Current.user_id # => 42 (Restored!)
|
|
142
|
+
Rails.logger.info "Processing" # => [Root=A] Processing...
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### B. Background Tasks (Cron/Rake)
|
|
148
|
+
|
|
149
|
+
For Rake tasks or Cron jobs (where no HTTP request exists), use `ExisRay::TaskMonitor`. It generates a fresh `Root ID`.
|
|
150
|
+
|
|
151
|
+
**File:** `lib/tasks/billing.rake`
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
task generate_invoices: :environment do
|
|
155
|
+
ExisRay::TaskMonitor.run('billing:generate_invoices') do
|
|
156
|
+
# Logs are tagged: [Root=1-65a...bc] [ExisRay] Starting task...
|
|
157
|
+
InvoiceService.process_all
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### C. HTTP Clients
|
|
163
|
+
|
|
164
|
+
ExisRay ensures traceability across microservices.
|
|
165
|
+
|
|
166
|
+
#### ActiveResource (Automatic)
|
|
167
|
+
If `ActiveResource` is detected, ExisRay automatically patches it. All outgoing requests will include:
|
|
168
|
+
* `X-Wp-Trace-Id` (Trace Header)
|
|
169
|
+
* `UserId`, `IspId`, `CorrelationId`
|
|
170
|
+
|
|
171
|
+
#### Faraday (Manual)
|
|
172
|
+
For Faraday, you must explicitly add the middleware:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
conn = Faraday.new(url: '[https://api.internal](https://api.internal)') do |f|
|
|
176
|
+
f.use ExisRay::FaradayMiddleware
|
|
177
|
+
f.adapter Faraday.default_adapter
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 馃彈 Architecture
|
|
184
|
+
|
|
185
|
+
* **`ExisRay::Tracer`**: The infrastructure layer. Handles AWS X-Ray format parsing and ID generation.
|
|
186
|
+
* **`ExisRay::Current`**: The business layer. Manages domain identity (`User`, `ISP`).
|
|
187
|
+
* **`ExisRay::Reporter`**: The observability layer. Bridges the gap between your app and Sentry.
|
|
188
|
+
* **`ExisRay::TaskMonitor`**: The entry point for non-HTTP processes.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module ExisRay
|
|
2
|
+
# M贸dulo dise帽ado para interceptar e instrumentar las peticiones HTTP salientes realizadas con ActiveResource.
|
|
3
|
+
# Utiliza el patr贸n `prepend` para envolver el m茅todo `headers` original sin romper la cadena de herencia.
|
|
4
|
+
#
|
|
5
|
+
# Su funci贸n principal es inyectar autom谩ticamente el header de trazabilidad (Trace ID)
|
|
6
|
+
# en todas las peticiones salientes para mantener la traza distribuida entre microservicios.
|
|
7
|
+
module ActiveResourceInstrumentation
|
|
8
|
+
# Sobrescribe el m茅todo `headers` de ActiveResource para inyectar el Trace ID actual.
|
|
9
|
+
#
|
|
10
|
+
# L贸gica de inyecci贸n:
|
|
11
|
+
# 1. Obtiene los headers definidos originalmente por el modelo o la request.
|
|
12
|
+
# 2. Verifica si existe un contexto de traza activo (Root ID).
|
|
13
|
+
# 3. Si existe, genera el header formateado (AWS/Wispro) y lo fusiona con los headers originales.
|
|
14
|
+
#
|
|
15
|
+
# @return [Hash] Un hash de headers HTTP que incluye el header de trazabilidad si corresponde.
|
|
16
|
+
def headers
|
|
17
|
+
# 1. Obtenemos los headers originales (si los hay)
|
|
18
|
+
original_headers = super
|
|
19
|
+
|
|
20
|
+
# 2. Verificaci贸n Universal:
|
|
21
|
+
# Usamos `root_id` en lugar de `trace_id`.
|
|
22
|
+
# - trace_id: Solo existe si recibimos una petici贸n Web (viene del header entrante).
|
|
23
|
+
# - root_id: Existe SIEMPRE que haya traza (sea Web o sea un Cron generado por TaskMonitor).
|
|
24
|
+
if ExisRay::Tracer.root_id.present?
|
|
25
|
+
# Generamos el string propagable: "Root=...;Parent=...;Sampled=..."
|
|
26
|
+
trace_header_value = ExisRay::Tracer.generate_trace_header
|
|
27
|
+
|
|
28
|
+
# Buscamos la key configurada (ej: 'HTTP_X_AMZN_TRACE_ID' o custom)
|
|
29
|
+
trace_header_key = ExisRay.configuration.trace_header
|
|
30
|
+
|
|
31
|
+
# Retornamos un nuevo hash combinado (merge) para no mutar el original por error
|
|
32
|
+
original_headers.merge(trace_header_key => trace_header_value)
|
|
33
|
+
else
|
|
34
|
+
# Si no hay traza activa, devolvemos los headers tal cual
|
|
35
|
+
original_headers
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ExisRay
|
|
2
|
+
# Clase de configuraci贸n global para la gema.
|
|
3
|
+
# Permite personalizar los headers de trazabilidad y definir las clases de la aplicaci贸n host
|
|
4
|
+
# que la gema utilizar谩 para gestionar el contexto (Current) y el reporte de errores (Reporter).
|
|
5
|
+
class Configuration
|
|
6
|
+
# @!attribute [rw] trace_header
|
|
7
|
+
# @return [String] La key del header HTTP (formato Rack) donde se buscar谩 el Trace ID entrante.
|
|
8
|
+
# Por defecto es 'HTTP_X_AMZN_TRACE_ID'.
|
|
9
|
+
attr_accessor :trace_header
|
|
10
|
+
|
|
11
|
+
# @!attribute [rw] propagation_trace_header
|
|
12
|
+
# @return [String] La key del header HTTP que se enviar谩 a otros servicios (formato est谩ndar).
|
|
13
|
+
# Por defecto es 'X-Amzn-Trace-Id'.
|
|
14
|
+
attr_accessor :propagation_trace_header
|
|
15
|
+
|
|
16
|
+
# @!attribute [rw] reporter_class
|
|
17
|
+
# @return [String, Class, nil] El nombre de la clase de la aplicaci贸n host que hereda de {ExisRay::Reporter}.
|
|
18
|
+
# Se recomienda usar un String para evitar problemas de carga (autoloading) durante la inicializaci贸n.
|
|
19
|
+
# @example 'Choto'
|
|
20
|
+
attr_accessor :reporter_class
|
|
21
|
+
|
|
22
|
+
# @!attribute [rw] current_class
|
|
23
|
+
# @return [String, Class, nil] El nombre de la clase de la aplicaci贸n host que hereda de {ExisRay::Current}.
|
|
24
|
+
# Se utiliza para inyectar/leer user_id, isp_id y correlation_id.
|
|
25
|
+
# @example 'Current'
|
|
26
|
+
attr_accessor :current_class
|
|
27
|
+
|
|
28
|
+
# Inicializa la configuraci贸n con valores por defecto compatibles con AWS X-Ray.
|
|
29
|
+
def initialize
|
|
30
|
+
@trace_header = 'HTTP_X_AMZN_TRACE_ID'
|
|
31
|
+
@propagation_trace_header = 'X-Amzn-Trace-Id'
|
|
32
|
+
@reporter_class = 'Reporter'
|
|
33
|
+
@current_class = 'Current'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require 'active_support/current_attributes'
|
|
2
|
+
|
|
3
|
+
module ExisRay
|
|
4
|
+
# Clase base para la gesti贸n del contexto de negocio (User, ISP, Correlation).
|
|
5
|
+
# Debe ser heredada por la aplicaci贸n host (ej: class Current < ExisRay::Current).
|
|
6
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
7
|
+
attribute :user_id, :isp_id, :correlation_id
|
|
8
|
+
|
|
9
|
+
# Callback nativo de Rails: Se ejecuta autom谩ticamente al llamar a Current.reset
|
|
10
|
+
resets do
|
|
11
|
+
@user = nil
|
|
12
|
+
@isp = nil
|
|
13
|
+
|
|
14
|
+
if defined?(PaperTrail)
|
|
15
|
+
PaperTrail.request.whodunnit = nil
|
|
16
|
+
PaperTrail.request.controller_info = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if defined?(ActiveResource::Base)
|
|
20
|
+
ActiveResource::Base.headers.delete('UserId')
|
|
21
|
+
ActiveResource::Base.headers.delete('IspId')
|
|
22
|
+
ActiveResource::Base.headers.delete('CorrelationId')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# --- Setters con Hooks ---
|
|
27
|
+
|
|
28
|
+
def user_id=(id)
|
|
29
|
+
super
|
|
30
|
+
if defined?(ActiveResource::Base)
|
|
31
|
+
ActiveResource::Base.headers['UserId'] = id.to_s
|
|
32
|
+
end
|
|
33
|
+
if defined?(PaperTrail)
|
|
34
|
+
PaperTrail.request.whodunnit = id
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def isp_id=(id)
|
|
39
|
+
super
|
|
40
|
+
@isp = nil # Invalida cache
|
|
41
|
+
if defined?(ActiveResource::Base)
|
|
42
|
+
ActiveResource::Base.headers['IspId'] = id.to_s
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def correlation_id=(id)
|
|
47
|
+
super
|
|
48
|
+
|
|
49
|
+
if defined?(::Session)
|
|
50
|
+
::Session.request_id = id # Deprecated legacy support
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if defined?(ActiveResource::Base)
|
|
54
|
+
ActiveResource::Base.headers['CorrelationId'] = id.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if defined?(PaperTrail)
|
|
58
|
+
PaperTrail.request.controller_info = { correlation_id: id }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Integraci贸n con el Reporter configurado
|
|
62
|
+
if (reporter = ExisRay.reporter_class) && reporter.respond_to?(:add_tags)
|
|
63
|
+
reporter.add_tags(correlation_id: id)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# --- Helpers de Objetos (Lazy Loading) ---
|
|
68
|
+
# Estos m茅todos asumen que la app host tiene modelos ::User e ::Isp
|
|
69
|
+
|
|
70
|
+
def user=(object)
|
|
71
|
+
@user = object
|
|
72
|
+
self.user_id = object&.id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def user
|
|
76
|
+
return @user if defined?(@user) && @user
|
|
77
|
+
|
|
78
|
+
if user_id && defined?(::User) && ::User.respond_to?(:find_by)
|
|
79
|
+
@user = ::User.find_by(id: user_id)
|
|
80
|
+
else
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def isp=(object)
|
|
86
|
+
@isp = object
|
|
87
|
+
self.isp_id = object&.id
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def isp
|
|
91
|
+
return @isp if defined?(@isp) && @isp
|
|
92
|
+
|
|
93
|
+
if isp_id && defined?(::Isp) && ::Isp.respond_to?(:find_by)
|
|
94
|
+
@isp = ::Isp.find_by(id: isp_id)
|
|
95
|
+
else
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def user?
|
|
101
|
+
user_id.present?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def isp?
|
|
105
|
+
isp_id.present?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def correlation_id?
|
|
109
|
+
correlation_id.present?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module ExisRay
|
|
4
|
+
# Middleware para Faraday que inyecta el header de trazabilidad saliente.
|
|
5
|
+
class FaradayMiddleware < Faraday::Middleware
|
|
6
|
+
def call(env)
|
|
7
|
+
if ExisRay::Tracer.root_id.present?
|
|
8
|
+
# Generamos el valor de traza
|
|
9
|
+
header_value = ExisRay::Tracer.generate_trace_header
|
|
10
|
+
# Obtenemos la key configurada para propagaci贸n
|
|
11
|
+
header_key = ExisRay.configuration.propagation_trace_header
|
|
12
|
+
|
|
13
|
+
env.request_headers[header_key] = header_value
|
|
14
|
+
end
|
|
15
|
+
@app.call(env)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module ExisRay
|
|
2
|
+
# Middleware de Rack para interceptar peticiones HTTP.
|
|
3
|
+
# Inicializa el Tracer y sincroniza el Correlation ID con la clase Current configurada.
|
|
4
|
+
class HttpMiddleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
@base_service_name = defined?(Rails) ? Rails.application.class.module_parent_name : 'App'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
# 1. Hidratar Infraestructura
|
|
12
|
+
ExisRay::Tracer.created_at = Time.now.utc.to_f
|
|
13
|
+
ExisRay::Tracer.service_name = "#{@base_service_name}-HTTP"
|
|
14
|
+
|
|
15
|
+
trace_header_key = ExisRay.configuration.trace_header
|
|
16
|
+
|
|
17
|
+
ExisRay::Tracer.trace_id = env[trace_header_key]
|
|
18
|
+
ExisRay::Tracer.request_id = env['action_dispatch.request_id']
|
|
19
|
+
ExisRay::Tracer.parse_trace_id
|
|
20
|
+
|
|
21
|
+
# 2. Hidratar Negocio
|
|
22
|
+
# Usamos el helper centralizado para obtener la clase Current
|
|
23
|
+
if (curr = ExisRay.current_class) && curr.respond_to?(:correlation_id=) && ExisRay::Tracer.root_id.present?
|
|
24
|
+
curr.correlation_id = ExisRay::Tracer.correlation_id
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@app.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'rails/railtie'
|
|
2
|
+
|
|
3
|
+
module ExisRay
|
|
4
|
+
# Integraci贸n autom谩tica con Rails.
|
|
5
|
+
# Carga middlewares HTTP, tags de logs e integraciones (Sidekiq/ActiveResource).
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
# 1. Middleware HTTP
|
|
8
|
+
initializer "exis_ray.configure_middleware" do |app|
|
|
9
|
+
require 'exis_ray/http_middleware'
|
|
10
|
+
app.middleware.insert_after ActionDispatch::RequestId, ExisRay::HttpMiddleware
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 2. Logs Tags
|
|
14
|
+
initializer "exis_ray.configure_log_tags" do |app|
|
|
15
|
+
app.config.log_tags ||= []
|
|
16
|
+
app.config.log_tags << proc {
|
|
17
|
+
if ExisRay::Tracer.trace_id.present?
|
|
18
|
+
ExisRay::Tracer.trace_id
|
|
19
|
+
elsif ExisRay::Tracer.root_id.present?
|
|
20
|
+
ExisRay::Tracer.root_id
|
|
21
|
+
else
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 3. Integraciones Post-Boot
|
|
28
|
+
config.after_initialize do
|
|
29
|
+
# ActiveResource
|
|
30
|
+
if defined?(ActiveResource::Base)
|
|
31
|
+
require 'exis_ray/active_resource_instrumentation'
|
|
32
|
+
ActiveResource::Base.send(:prepend, ExisRay::ActiveResourceInstrumentation)
|
|
33
|
+
Rails.logger.info "[ExisRay] ActiveResource instrumentado."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Sidekiq
|
|
37
|
+
# Usamos ::Sidekiq para referirnos a la Gema Global y no al m贸dulo local ExisRay::Sidekiq
|
|
38
|
+
if defined?(::Sidekiq)
|
|
39
|
+
require 'exis_ray/sidekiq/client_middleware'
|
|
40
|
+
require 'exis_ray/sidekiq/server_middleware'
|
|
41
|
+
|
|
42
|
+
::Sidekiq.configure_client do |config|
|
|
43
|
+
config.client_middleware do |chain|
|
|
44
|
+
chain.add ExisRay::Sidekiq::ClientMiddleware
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
::Sidekiq.configure_server do |config|
|
|
49
|
+
config.client_middleware do |chain|
|
|
50
|
+
chain.add ExisRay::Sidekiq::ClientMiddleware
|
|
51
|
+
end
|
|
52
|
+
config.server_middleware do |chain|
|
|
53
|
+
chain.prepend ExisRay::Sidekiq::ServerMiddleware
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
Rails.logger.info "[ExisRay] Sidekiq Middleware integrado."
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require 'active_support/current_attributes'
|
|
2
|
+
|
|
3
|
+
module ExisRay
|
|
4
|
+
# Clase base h铆brida para reporte de errores.
|
|
5
|
+
# Soporta integraci贸n moderna (Sentry SDK) y legacy (Raven/Session).
|
|
6
|
+
class Reporter < ActiveSupport::CurrentAttributes
|
|
7
|
+
attribute :contexts, :tags, :transaction_name, :fingerprint
|
|
8
|
+
|
|
9
|
+
resets do
|
|
10
|
+
self.contexts = {}
|
|
11
|
+
self.tags = {}
|
|
12
|
+
self.fingerprint = []
|
|
13
|
+
self.transaction_name = nil
|
|
14
|
+
|
|
15
|
+
# Invocamos limpieza legacy si existe.
|
|
16
|
+
# Usamos self.class porque clean_legacy_session! es m茅todo de clase.
|
|
17
|
+
self.class.clean_legacy_session!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# --- M茅todos P煤blicos ---
|
|
21
|
+
|
|
22
|
+
def self.report(message, context: {}, tags: {}, fingerprint: [], transaction_name: nil)
|
|
23
|
+
prepare_scope(context, tags, fingerprint, transaction_name)
|
|
24
|
+
if report_to_new_sentry?
|
|
25
|
+
report_to_new_sentry(message)
|
|
26
|
+
else
|
|
27
|
+
report_to_old_sentry(message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.exception(excep, context: {}, tags: {}, fingerprint: [], transaction_name: nil)
|
|
32
|
+
prepare_scope(context, tags, fingerprint, transaction_name)
|
|
33
|
+
add_tags(exception: excep.class.to_s)
|
|
34
|
+
|
|
35
|
+
Rails.logger.error(excep) unless Rails.env.production?
|
|
36
|
+
|
|
37
|
+
if report_to_new_sentry?
|
|
38
|
+
exception_to_new_sentry(excep)
|
|
39
|
+
else
|
|
40
|
+
exception_to_old_sentry(excep)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- Builders de Datos ---
|
|
45
|
+
|
|
46
|
+
def self.add_fingerprint(value)
|
|
47
|
+
current_values = fingerprint || []
|
|
48
|
+
current_values << value
|
|
49
|
+
self.fingerprint = current_values.flatten.compact.uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.add_context(attrs)
|
|
53
|
+
return if attrs.blank?
|
|
54
|
+
|
|
55
|
+
self.contexts = (contexts || {}).merge(attrs.as_json)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.add_tags(attrs)
|
|
59
|
+
return if attrs.blank?
|
|
60
|
+
self.tags = (tags || {}).merge(attrs.as_json)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.build_custom_context
|
|
64
|
+
# Hook para subclases
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# --- L贸gica Legacy ---
|
|
68
|
+
def self.clean_legacy_session!
|
|
69
|
+
return unless defined?(::Session) && ::Session.respond_to?(:clean!)
|
|
70
|
+
|
|
71
|
+
::Session.clean!
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.session_tag!
|
|
75
|
+
return unless defined?(::Session)
|
|
76
|
+
|
|
77
|
+
::Session.tags_context ||= {}
|
|
78
|
+
|
|
79
|
+
if fingerprint.present?
|
|
80
|
+
str_fingerprint = fingerprint.flatten.join(',')
|
|
81
|
+
::Session.tags_context.merge!(fingerprint: str_fingerprint)
|
|
82
|
+
end
|
|
83
|
+
if transaction_name.present?
|
|
84
|
+
::Session.tags_context.merge!(transaction_name: transaction_name)
|
|
85
|
+
end
|
|
86
|
+
::Session.tags_context.merge!(tags) if tags.present?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.session_context!
|
|
90
|
+
return unless contexts.present? && defined?(::Session)
|
|
91
|
+
|
|
92
|
+
::Session.extra_context ||= {}
|
|
93
|
+
::Session.extra_context.merge!(contexts)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.report_to_old_sentry(message)
|
|
97
|
+
session_tag!
|
|
98
|
+
session_context!
|
|
99
|
+
Sentry.send_event(message) if defined?(Sentry)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.exception_to_old_sentry(exception)
|
|
103
|
+
session_tag!
|
|
104
|
+
session_context!
|
|
105
|
+
|
|
106
|
+
if defined?(Sentry)
|
|
107
|
+
Sentry.populate_context(contexts) if contexts.present?
|
|
108
|
+
Sentry.notify(exception)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# --- L贸gica Moderna ---
|
|
113
|
+
|
|
114
|
+
def self.report_to_new_sentry?
|
|
115
|
+
defined?(::NEW_SENTRY) && ::NEW_SENTRY
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.report_to_new_sentry(message)
|
|
119
|
+
send_to_new_sentry { Sentry.capture_message(message, fingerprint: fingerprint) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.exception_to_new_sentry(exception)
|
|
123
|
+
send_to_new_sentry { Sentry.capture_exception(exception, level: 'error', fingerprint: fingerprint) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.send_to_new_sentry
|
|
127
|
+
return unless defined?(Sentry)
|
|
128
|
+
|
|
129
|
+
Sentry.with_scope do |scope|
|
|
130
|
+
scope.set_transaction_name(transaction_name) if transaction_name.present?
|
|
131
|
+
if contexts.present?
|
|
132
|
+
contexts.each do |key, value|
|
|
133
|
+
val = value.is_a?(Hash) ? value : { value: value }
|
|
134
|
+
scope.set_context(key, val)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
scope.set_tags(tags) if tags.present?
|
|
138
|
+
yield(scope)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# --- Inicializaci贸n de Contexto ---
|
|
143
|
+
|
|
144
|
+
private_class_method def self.prepare_scope(context, tags, fingerprint, transaction_name)
|
|
145
|
+
add_context(context)
|
|
146
|
+
add_tags(tags)
|
|
147
|
+
add_fingerprint(fingerprint)
|
|
148
|
+
self.transaction_name = transaction_name if transaction_name.present?
|
|
149
|
+
|
|
150
|
+
build_from_tracer
|
|
151
|
+
build_from_current
|
|
152
|
+
build_custom_context
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.build_from_tracer
|
|
156
|
+
return unless defined?(ExisRay::Tracer)
|
|
157
|
+
|
|
158
|
+
if ExisRay::Tracer.root_id.present?
|
|
159
|
+
add_tags(correlation_id: ExisRay::Tracer.root_id)
|
|
160
|
+
add_context(trace: {
|
|
161
|
+
root_id: ExisRay::Tracer.root_id,
|
|
162
|
+
request_id: ExisRay::Tracer.request_id
|
|
163
|
+
})
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.build_from_current
|
|
168
|
+
klass = ExisRay.current_class
|
|
169
|
+
return unless klass
|
|
170
|
+
|
|
171
|
+
add_tags(user_id: klass.user_id) if klass.respond_to?(:user_id?) && klass.user_id?
|
|
172
|
+
add_tags(isp_id: klass.isp_id) if klass.respond_to?(:isp_id?) && klass.isp_id?
|
|
173
|
+
|
|
174
|
+
if klass.respond_to?(:user) && klass.user.present?
|
|
175
|
+
user_json = klass.user.respond_to?(:as_json) ? klass.user.as_json : { id: klass.user_id }
|
|
176
|
+
add_context(user: user_json)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if klass.respond_to?(:isp) && klass.isp.present?
|
|
180
|
+
isp_json = klass.isp.respond_to?(:as_json) ? klass.isp.as_json : { id: klass.isp_id }
|
|
181
|
+
add_context(isp: isp_json)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private_class_method :build_from_current, :build_from_tracer
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module ExisRay
|
|
2
|
+
module Sidekiq
|
|
3
|
+
class ClientMiddleware
|
|
4
|
+
# Intercepta el push del trabajo a Redis.
|
|
5
|
+
# @param worker_class [String, Class] La clase del worker.
|
|
6
|
+
# @param job [Hash] El payload del trabajo (aqu铆 inyectamos datos).
|
|
7
|
+
# @param queue [String] Nombre de la cola.
|
|
8
|
+
# @param redis_pool [Object] Pool de conexi贸n (Legacy v6).
|
|
9
|
+
def call(worker_class, job, queue, redis_pool = nil)
|
|
10
|
+
# Solo inyectamos si hay una traza activa (viniendo de Web o Cron)
|
|
11
|
+
if ExisRay::Tracer.root_id.present?
|
|
12
|
+
# 1. Inyectamos la traza (usamos generate_trace_header para mantener la cadena)
|
|
13
|
+
job['exis_ray_trace'] = ExisRay::Tracer.generate_trace_header
|
|
14
|
+
|
|
15
|
+
# 2. Inyectamos el contexto de negocio (Current)
|
|
16
|
+
# Esto permite saber qu茅 Usuario o ISP dispar贸 el job.
|
|
17
|
+
if ExisRay.current_class.present?
|
|
18
|
+
context = {}
|
|
19
|
+
context[:user_id] = ExisRay.current_class.user_id if ExisRay.current_class.respond_to?(:user_id)
|
|
20
|
+
context[:isp_id] = ExisRay.current_class.isp_id if ExisRay.current_class.respond_to?(:isp_id)
|
|
21
|
+
context[:correlation_id] = ExisRay.current_class.correlation_id if ExisRay.current_class.respond_to?(:correlation_id)
|
|
22
|
+
|
|
23
|
+
job['exis_ray_context'] = context
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
yield
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module ExisRay
|
|
2
|
+
module Sidekiq
|
|
3
|
+
# Middleware de Servidor para Sidekiq.
|
|
4
|
+
# Se ejecuta alrededor de cada trabajo (job) procesado por un Worker.
|
|
5
|
+
#
|
|
6
|
+
# Su responsabilidad es:
|
|
7
|
+
# 1. Recuperar el Trace ID y contexto (User, ISP) inyectados por el cliente.
|
|
8
|
+
# 2. Configurar el entorno (Tracer, Current, Reporter).
|
|
9
|
+
# 3. Limpiar todo al finalizar para no contaminar el thread (Thread Pooling).
|
|
10
|
+
class ServerMiddleware
|
|
11
|
+
# Intercepta la ejecuci贸n del job.
|
|
12
|
+
#
|
|
13
|
+
# @param worker [Object] La instancia del worker que procesar谩 el job.
|
|
14
|
+
# @param job [Hash] El payload del trabajo (contiene argumentos y metadatos).
|
|
15
|
+
# @param queue [String] El nombre de la cola.
|
|
16
|
+
def call(worker, job, queue)
|
|
17
|
+
# 1. Hidrataci贸n de Infraestructura (Tracer)
|
|
18
|
+
hydrate_tracer(worker, job)
|
|
19
|
+
|
|
20
|
+
# 2. Hidrataci贸n de Negocio (Current Class configurada)
|
|
21
|
+
hydrate_current(job)
|
|
22
|
+
|
|
23
|
+
# 3. Configuraci贸n de Reporte (Sentry/Reporter Class configurada)
|
|
24
|
+
setup_reporter(worker)
|
|
25
|
+
|
|
26
|
+
# 4. Ejecuci贸n con Logs Taggeados
|
|
27
|
+
# Inyectamos el Root ID en los logs de Rails para correlacionarlos con Sidekiq.
|
|
28
|
+
tags = [ExisRay::Tracer.root_id]
|
|
29
|
+
|
|
30
|
+
if Rails.logger.respond_to?(:tagged)
|
|
31
|
+
Rails.logger.tagged(*tags) { yield }
|
|
32
|
+
else
|
|
33
|
+
yield
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
ensure
|
|
37
|
+
# 5. Limpieza Total (Vital en Sidekiq)
|
|
38
|
+
# Sidekiq reutiliza threads. Si no limpiamos, el contexto de un job
|
|
39
|
+
# (ej: usuario actual) podr铆a filtrarse al siguiente job.
|
|
40
|
+
ExisRay::Tracer.reset
|
|
41
|
+
|
|
42
|
+
# Limpieza usando los helpers centralizados (sin hardcodear Current)
|
|
43
|
+
ExisRay.current_class&.reset if ExisRay.current_class.respond_to?(:reset)
|
|
44
|
+
ExisRay.reporter_class&.reset if ExisRay.reporter_class.respond_to?(:reset)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Configura el Tracer con el ID recibido o genera uno nuevo.
|
|
50
|
+
def hydrate_tracer(worker, job)
|
|
51
|
+
ExisRay::Tracer.created_at = Time.now.utc.to_f
|
|
52
|
+
ExisRay::Tracer.service_name = "Sidekiq-#{worker.class.name}"
|
|
53
|
+
|
|
54
|
+
if job['exis_ray_trace']
|
|
55
|
+
# Continuidad: Usamos la traza que viene del cliente (Web/Cron)
|
|
56
|
+
ExisRay::Tracer.trace_id = job['exis_ray_trace']
|
|
57
|
+
ExisRay::Tracer.parse_trace_id
|
|
58
|
+
else
|
|
59
|
+
# Origen: El job naci贸 aqu铆 (ej: desde consola o trigger externo sin contexto)
|
|
60
|
+
ExisRay::Tracer.root_id = ExisRay::Tracer.send(:generate_new_root)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Hidrata la clase Current configurada con los datos del payload.
|
|
65
|
+
def hydrate_current(job)
|
|
66
|
+
# Obtenemos la clase din谩mica (ej: Current)
|
|
67
|
+
klass = ExisRay.current_class
|
|
68
|
+
|
|
69
|
+
# Salimos si no hay clase configurada o no hay contexto en el job
|
|
70
|
+
return unless klass && job['exis_ray_context']
|
|
71
|
+
|
|
72
|
+
ctx = job['exis_ray_context']
|
|
73
|
+
|
|
74
|
+
# Asignaci贸n segura usando la clase din谩mica
|
|
75
|
+
klass.user_id = ctx['user_id'] if ctx['user_id'] && klass.respond_to?(:user_id=)
|
|
76
|
+
klass.isp_id = ctx['isp_id'] if ctx['isp_id'] && klass.respond_to?(:isp_id=)
|
|
77
|
+
|
|
78
|
+
if ctx['correlation_id'] && klass.respond_to?(:correlation_id=)
|
|
79
|
+
klass.correlation_id = ctx['correlation_id']
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Configura tags y nombres de transacci贸n en el Reporter.
|
|
84
|
+
def setup_reporter(worker)
|
|
85
|
+
klass = ExisRay.reporter_class
|
|
86
|
+
return unless klass
|
|
87
|
+
|
|
88
|
+
# Nombre de transacci贸n para Sentry: "Sidekiq/HardWorker"
|
|
89
|
+
if klass.respond_to?(:transaction_name=)
|
|
90
|
+
klass.transaction_name = "Sidekiq/#{worker.class.name}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Tags adicionales de infraestructura Sidekiq
|
|
94
|
+
if klass.respond_to?(:add_tags)
|
|
95
|
+
klass.add_tags(
|
|
96
|
+
sidekiq_queue: worker.class.get_sidekiq_options['queue'],
|
|
97
|
+
retry_count: worker.respond_to?(:retry_count) ? worker.retry_count : 0
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ExisRay
|
|
2
|
+
# Wrapper para monitorear tareas en segundo plano (Rake/Cron).
|
|
3
|
+
module TaskMonitor
|
|
4
|
+
# Ejecuta un bloque dentro de un contexto monitoreado.
|
|
5
|
+
# @param task_name [String] Nombre identificador (ej: 'billing:generate').
|
|
6
|
+
def self.run(task_name)
|
|
7
|
+
setup_tracer(task_name)
|
|
8
|
+
|
|
9
|
+
short_name = task_name.to_s.split(':').last
|
|
10
|
+
|
|
11
|
+
# Configurar Reporter
|
|
12
|
+
if (rep = ExisRay.reporter_class) && rep.respond_to?(:transaction_name=)
|
|
13
|
+
rep.transaction_name = short_name
|
|
14
|
+
rep.add_tags(service: :cron, task: short_name) if rep.respond_to?(:add_tags)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Configurar Current
|
|
18
|
+
if (curr = ExisRay.current_class) && curr.respond_to?(:correlation_id=)
|
|
19
|
+
curr.correlation_id = ExisRay::Tracer.correlation_id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Logs con Root ID
|
|
23
|
+
tags = [ExisRay::Tracer.root_id]
|
|
24
|
+
Rails.logger.tagged(*tags) do
|
|
25
|
+
Rails.logger.info "[ExisRay] Iniciando tarea: #{task_name}"
|
|
26
|
+
yield
|
|
27
|
+
Rails.logger.info "[ExisRay] Finalizada con 茅xito."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
Rails.logger.error "[ExisRay] Fall贸 la tarea #{task_name}: #{e.message}"
|
|
32
|
+
raise e
|
|
33
|
+
ensure
|
|
34
|
+
# Limpieza centralizada
|
|
35
|
+
ExisRay::Tracer.reset
|
|
36
|
+
ExisRay.current_class&.reset if ExisRay.current_class.respond_to?(:reset)
|
|
37
|
+
ExisRay.reporter_class&.reset if ExisRay.reporter_class.respond_to?(:reset)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.setup_tracer(task_name)
|
|
41
|
+
ExisRay::Tracer.service_name = task_name.to_s.gsub(':', '-').camelize
|
|
42
|
+
ExisRay::Tracer.request_id = SecureRandom.uuid
|
|
43
|
+
ExisRay::Tracer.created_at = Time.now.utc.to_f
|
|
44
|
+
|
|
45
|
+
pod_id = get_pod_identifier
|
|
46
|
+
ExisRay::Tracer.root_id = ExisRay::Tracer.send(:generate_new_root, pod_id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.get_pod_identifier
|
|
50
|
+
(ENV['HOSTNAME'] || 'local').split('-').last.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private_class_method :get_pod_identifier, :setup_tracer
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'active_support/current_attributes'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
|
|
4
|
+
module ExisRay
|
|
5
|
+
# Gestiona el contexto de trazabilidad distribuida (Distributed Tracing).
|
|
6
|
+
# Utiliza `ActiveSupport::CurrentAttributes` para mantener el estado de la petici贸n actual
|
|
7
|
+
# de forma segura entre hilos (thread-safe).
|
|
8
|
+
#
|
|
9
|
+
# Esta clase parsea headers de AWS X-Ray y genera nuevos headers para la propagaci贸n.
|
|
10
|
+
#
|
|
11
|
+
# @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html Documentaci贸n de AWS X-Ray
|
|
12
|
+
class Tracer < ActiveSupport::CurrentAttributes
|
|
13
|
+
attribute :trace_id, :request_id, :root_id, :self_id, :called_from, :total_time_so_far, :created_at, :service_name
|
|
14
|
+
|
|
15
|
+
# Devuelve el nombre del servicio actual.
|
|
16
|
+
# Si no se ha definido manualmente, hace fallback al nombre de la aplicaci贸n Rails.
|
|
17
|
+
#
|
|
18
|
+
# @return [String] El nombre del servicio (ej: "Wispro", "Wispro-Worker", "App").
|
|
19
|
+
def self.service_name
|
|
20
|
+
super || (defined?(Rails) ? Rails.application.class.module_parent_name : 'App')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Genera un ID de correlaci贸n compuesto, 煤til para logs y auditor铆a.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# ExisRay::Tracer.correlation_id #=> "Wispro-HTTP;1-5759...-..."
|
|
27
|
+
#
|
|
28
|
+
# @return [String] Cadena compuesta "ServiceName;RootID".
|
|
29
|
+
def self.correlation_id
|
|
30
|
+
"#{service_name};#{root_id}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Parsea el string de trazabilidad recibido y popula los atributos individuales.
|
|
34
|
+
# Maneja el formato est谩ndar de AWS: "Root=...;Self=...;CalledFrom=...;TotalTimeSoFar=..."
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def self.parse_trace_id
|
|
38
|
+
return unless trace_id.present?
|
|
39
|
+
|
|
40
|
+
# Fallback para desarrollo: Si el header no trae Root, generamos uno nuevo.
|
|
41
|
+
self.trace_id = generate_new_root(trace_id) if trace_id.exclude?('Root')
|
|
42
|
+
|
|
43
|
+
# Parseo a Hash
|
|
44
|
+
data = trace_id.split(';').map { |part| part.split('=', 2) }.to_h
|
|
45
|
+
|
|
46
|
+
self.root_id = data['Root']
|
|
47
|
+
self.self_id = data['Self']
|
|
48
|
+
self.called_from = data['CalledFrom']
|
|
49
|
+
|
|
50
|
+
if data['TotalTimeSoFar']
|
|
51
|
+
self.total_time_so_far = data['TotalTimeSoFar'].gsub(/ms$/i, '').to_i
|
|
52
|
+
else
|
|
53
|
+
self.total_time_so_far = 0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Calcula el tiempo transcurrido en milisegundos desde el inicio de la request.
|
|
58
|
+
#
|
|
59
|
+
# @return [Integer] Duraci贸n en ms.
|
|
60
|
+
def self.current_duration_ms
|
|
61
|
+
return 0 unless created_at
|
|
62
|
+
((Time.now.utc.to_f - created_at) * 1000).round
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Construye el header de trazabilidad para enviar al siguiente servicio.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] Header formateado: "Root=...;Self=...;CalledFrom=...;TotalTimeSoFar=...ms"
|
|
68
|
+
def self.generate_trace_header
|
|
69
|
+
safe_root = if root_id.present?
|
|
70
|
+
root_id.start_with?('Root=') ? root_id : "Root=#{root_id}"
|
|
71
|
+
else
|
|
72
|
+
generate_new_root
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
total_acc_time = (total_time_so_far || 0) + current_duration_ms
|
|
76
|
+
|
|
77
|
+
# Nuevo ID para el span actual
|
|
78
|
+
my_new_id = "1-#{Time.now.to_i.to_s(16)}-#{clean_request_id}"
|
|
79
|
+
|
|
80
|
+
"#{safe_root};Self=#{my_new_id};CalledFrom=#{service_name};TotalTimeSoFar=#{total_acc_time}ms"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Genera un nuevo Root ID compatible con AWS X-Ray.
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
# @param suffix_id [String, nil] Sufijo opcional (ej: ID del Pod).
|
|
87
|
+
# @return [String] Formato: 1-TimestampHex-RandomHex
|
|
88
|
+
def self.generate_new_root(suffix_id = nil)
|
|
89
|
+
timestamp_hex = Time.now.to_i.to_s(16)
|
|
90
|
+
|
|
91
|
+
if suffix_id.present?
|
|
92
|
+
suffix_hex = suffix_id.to_i.to_s(16).rjust(8, '0')
|
|
93
|
+
unique_part = SecureRandom.hex(8) + suffix_hex
|
|
94
|
+
else
|
|
95
|
+
unique_part = SecureRandom.hex(12)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
"Root=1-#{timestamp_hex}-#{unique_part}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Limpia el Request ID para cumplir con los 24 caracteres hex de AWS.
|
|
102
|
+
#
|
|
103
|
+
# @api private
|
|
104
|
+
# @return [String]
|
|
105
|
+
def self.clean_request_id
|
|
106
|
+
(request_id || SecureRandom.hex).delete('-').first(24)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private_class_method :clean_request_id, :generate_new_root
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/exis_ray.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require "exis_ray/version"
|
|
2
|
+
|
|
3
|
+
# Dependencias externas
|
|
4
|
+
# Necesario para 'safe_constantize', 'present?', y 'CurrentAttributes'
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/core_ext/string/inflections" # Para safe_constantize
|
|
7
|
+
require "active_support/current_attributes"
|
|
8
|
+
|
|
9
|
+
# Componentes internos del Core
|
|
10
|
+
require "exis_ray/configuration"
|
|
11
|
+
require "exis_ray/tracer"
|
|
12
|
+
require "exis_ray/task_monitor"
|
|
13
|
+
require "exis_ray/http_middleware"
|
|
14
|
+
require "exis_ray/current"
|
|
15
|
+
require "exis_ray/reporter"
|
|
16
|
+
|
|
17
|
+
# Integraciones Opcionales
|
|
18
|
+
# Solo cargamos el middleware de Faraday si la gema est谩 presente en el sistema.
|
|
19
|
+
require "exis_ray/faraday_middleware" if defined?(Faraday)
|
|
20
|
+
|
|
21
|
+
# Solo cargamos la instrumentaci贸n si ActiveResource est谩 presente.
|
|
22
|
+
require "exis_ray/active_resource_instrumentation" if defined?(ActiveResource::Base)
|
|
23
|
+
|
|
24
|
+
# Integraci贸n autom谩tica con Rails
|
|
25
|
+
# Solo cargamos el Railtie si la constante Rails est谩 definida.
|
|
26
|
+
require "exis_ray/railtie" if defined?(Rails)
|
|
27
|
+
|
|
28
|
+
# Namespace principal de la gema ExisRay.
|
|
29
|
+
# Contiene la configuraci贸n global y los helpers de resoluci贸n de clases din谩micas.
|
|
30
|
+
module ExisRay
|
|
31
|
+
class Error < StandardError; end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# @!attribute [w] configuration
|
|
35
|
+
attr_writer :configuration
|
|
36
|
+
|
|
37
|
+
# Accesor para la configuraci贸n global de la gema.
|
|
38
|
+
# Inicializa una nueva instancia de {Configuration} si no existe.
|
|
39
|
+
#
|
|
40
|
+
# @return [ExisRay::Configuration] La instancia de configuraci贸n actual.
|
|
41
|
+
def configuration
|
|
42
|
+
@configuration ||= Configuration.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Bloque de configuraci贸n para inicializar la gema.
|
|
46
|
+
#
|
|
47
|
+
# @example Configurar en un initializer de Rails
|
|
48
|
+
# ExisRay.configure do |config|
|
|
49
|
+
# config.trace_header = 'HTTP_X_WP_TRACE_ID'
|
|
50
|
+
# config.current_class = 'Current'
|
|
51
|
+
# config.reporter_class = 'Choto'
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# @yieldparam config [ExisRay::Configuration] El objeto de configuraci贸n.
|
|
55
|
+
def configure
|
|
56
|
+
yield(configuration)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# --- Helpers Centralizados de Resoluci贸n de Clases ---
|
|
60
|
+
|
|
61
|
+
# Resuelve y retorna la clase configurada para manejar el contexto de negocio (Current).
|
|
62
|
+
# Convierte el String configurado (ej: 'Current') en la clase real constante.
|
|
63
|
+
#
|
|
64
|
+
# @return [Class, nil] La clase constante (ej: Current) o nil si no se encuentra/configura.
|
|
65
|
+
def current_class
|
|
66
|
+
return nil unless configuration
|
|
67
|
+
|
|
68
|
+
klass_name = configuration.current_class
|
|
69
|
+
return nil unless klass_name.present?
|
|
70
|
+
|
|
71
|
+
# Si es String, lo convertimos a constante de forma segura.
|
|
72
|
+
klass_name.is_a?(String) ? klass_name.safe_constantize : klass_name
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Resuelve y retorna la clase configurada para el reporte de errores (Reporter).
|
|
76
|
+
# Convierte el String configurado (ej: 'Choto') en la clase real constante.
|
|
77
|
+
#
|
|
78
|
+
# @return [Class, nil] La clase constante (ej: Choto) o nil si no se encuentra/configura.
|
|
79
|
+
def reporter_class
|
|
80
|
+
return nil unless configuration
|
|
81
|
+
|
|
82
|
+
klass_name = configuration.reporter_class
|
|
83
|
+
return nil unless klass_name.present?
|
|
84
|
+
|
|
85
|
+
klass_name.is_a?(String) ? klass_name.safe_constantize : klass_name
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/sig/exis_ray.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: exis_ray
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Gabriel Edera
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: railties
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.0'
|
|
41
|
+
description: Gema que gestiona el contexto de request, logs y propagaci贸n de headers.
|
|
42
|
+
email:
|
|
43
|
+
- gab.edera@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- CHANGELOG.md
|
|
49
|
+
- LICENSE.txt
|
|
50
|
+
- README.md
|
|
51
|
+
- Rakefile
|
|
52
|
+
- lib/exis_ray.rb
|
|
53
|
+
- lib/exis_ray/active_resource_instrumentation.rb
|
|
54
|
+
- lib/exis_ray/configuration.rb
|
|
55
|
+
- lib/exis_ray/current.rb
|
|
56
|
+
- lib/exis_ray/faraday_middleware.rb
|
|
57
|
+
- lib/exis_ray/http_middleware.rb
|
|
58
|
+
- lib/exis_ray/railtie.rb
|
|
59
|
+
- lib/exis_ray/reporter.rb
|
|
60
|
+
- lib/exis_ray/sidekiq/client_middleware.rb
|
|
61
|
+
- lib/exis_ray/sidekiq/server_middleware.rb
|
|
62
|
+
- lib/exis_ray/task_monitor.rb
|
|
63
|
+
- lib/exis_ray/tracer.rb
|
|
64
|
+
- lib/exis_ray/version.rb
|
|
65
|
+
- sig/exis_ray.rbs
|
|
66
|
+
homepage: https://github.com/gedera/exis_ray
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata: {}
|
|
70
|
+
post_install_message:
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 2.6.0
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 3.4.19
|
|
86
|
+
signing_key:
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Trazabilidad distribuida entre microservicios (AWS X-Ray style).
|
|
89
|
+
test_files: []
|