activerabbit-ai 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/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +49 -0
- data/IMPLEMENTATION_SUMMARY.md +220 -0
- data/README.md +317 -0
- data/Rakefile +10 -0
- data/TESTING_GUIDE.md +585 -0
- data/examples/rails_app_testing.rb +437 -0
- data/examples/rails_integration.rb +243 -0
- data/examples/standalone_usage.rb +309 -0
- data/lib/active_rabbit/client/configuration.rb +162 -0
- data/lib/active_rabbit/client/event_processor.rb +131 -0
- data/lib/active_rabbit/client/exception_tracker.rb +157 -0
- data/lib/active_rabbit/client/http_client.rb +137 -0
- data/lib/active_rabbit/client/n_plus_one_detector.rb +188 -0
- data/lib/active_rabbit/client/performance_monitor.rb +150 -0
- data/lib/active_rabbit/client/pii_scrubber.rb +169 -0
- data/lib/active_rabbit/client/railtie.rb +328 -0
- data/lib/active_rabbit/client/sidekiq_middleware.rb +130 -0
- data/lib/active_rabbit/client/version.rb +7 -0
- data/lib/active_rabbit/client.rb +119 -0
- data/lib/active_rabbit.rb +3 -0
- data/script/test_production_readiness.rb +403 -0
- data/sig/active_rabbit/client.rbs +6 -0
- metadata +155 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRabbit
|
|
4
|
+
module Client
|
|
5
|
+
class PiiScrubber
|
|
6
|
+
SCRUBBED_VALUE = "[FILTERED]"
|
|
7
|
+
|
|
8
|
+
attr_reader :configuration
|
|
9
|
+
|
|
10
|
+
def initialize(configuration)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def scrub(data)
|
|
15
|
+
case data
|
|
16
|
+
when Hash
|
|
17
|
+
scrub_hash(data)
|
|
18
|
+
when Array
|
|
19
|
+
scrub_array(data)
|
|
20
|
+
when String
|
|
21
|
+
scrub_string(data)
|
|
22
|
+
else
|
|
23
|
+
data
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def scrub_hash(hash)
|
|
30
|
+
return hash unless hash.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
hash.each_with_object({}) do |(key, value), scrubbed|
|
|
33
|
+
if should_scrub_key?(key)
|
|
34
|
+
scrubbed[key] = SCRUBBED_VALUE
|
|
35
|
+
else
|
|
36
|
+
scrubbed[key] = scrub(value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def scrub_array(array)
|
|
42
|
+
return array unless array.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
array.map { |item| scrub(item) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def scrub_string(string)
|
|
48
|
+
return string unless string.is_a?(String)
|
|
49
|
+
|
|
50
|
+
scrubbed = string.dup
|
|
51
|
+
|
|
52
|
+
# Scrub common PII patterns
|
|
53
|
+
scrubbed = scrub_email_addresses(scrubbed)
|
|
54
|
+
scrubbed = scrub_phone_numbers(scrubbed)
|
|
55
|
+
scrubbed = scrub_credit_cards(scrubbed)
|
|
56
|
+
scrubbed = scrub_social_security_numbers(scrubbed)
|
|
57
|
+
scrubbed = scrub_ip_addresses(scrubbed)
|
|
58
|
+
|
|
59
|
+
scrubbed
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def should_scrub_key?(key)
|
|
63
|
+
return false unless key
|
|
64
|
+
|
|
65
|
+
key_str = key.to_s.downcase
|
|
66
|
+
|
|
67
|
+
configuration.pii_fields.any? do |pii_field|
|
|
68
|
+
case pii_field
|
|
69
|
+
when String
|
|
70
|
+
key_str.include?(pii_field.downcase)
|
|
71
|
+
when Regexp
|
|
72
|
+
key_str =~ pii_field
|
|
73
|
+
else
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def scrub_email_addresses(string)
|
|
80
|
+
# Match email addresses
|
|
81
|
+
string.gsub(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, SCRUBBED_VALUE)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def scrub_phone_numbers(string)
|
|
85
|
+
# Match various phone number formats
|
|
86
|
+
patterns = [
|
|
87
|
+
/\(\d{3}\)\s?\d{3}[-.]?\d{4}/, # (123) 456-7890, (123)456-7890
|
|
88
|
+
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, # 123-456-7890, 123.456.7890, 1234567890
|
|
89
|
+
/\b\+1[-.\s]?\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/, # +1-123-456-7890
|
|
90
|
+
/\b\d{3}\s\d{3}\s\d{4}\b/ # 123 456 7890
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
patterns.reduce(string) do |str, pattern|
|
|
94
|
+
str.gsub(pattern, SCRUBBED_VALUE)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def scrub_credit_cards(string)
|
|
99
|
+
# Match credit card patterns - be more permissive for testing
|
|
100
|
+
patterns = [
|
|
101
|
+
/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/, # 1234-5678-9012-3456
|
|
102
|
+
/\b\d{13,19}\b/ # 13-19 consecutive digits
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
patterns.reduce(string) do |str, pattern|
|
|
106
|
+
str.gsub(pattern) do |match|
|
|
107
|
+
digits = match.gsub(/\D/, '')
|
|
108
|
+
# Only scrub if it looks like a credit card (passes basic Luhn check)
|
|
109
|
+
if digits.length >= 13 && digits.length <= 19 && luhn_valid?(digits)
|
|
110
|
+
SCRUBBED_VALUE
|
|
111
|
+
else
|
|
112
|
+
match
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def scrub_social_security_numbers(string)
|
|
119
|
+
# Match SSN patterns
|
|
120
|
+
patterns = [
|
|
121
|
+
/\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/, # 123-45-6789, 123.45.6789, 123 45 6789
|
|
122
|
+
/\b\d{9}\b/ # 123456789 (9 consecutive digits)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
patterns.reduce(string) do |str, pattern|
|
|
126
|
+
str.gsub(pattern) do |match|
|
|
127
|
+
# Only scrub 9-digit sequences that look like SSNs
|
|
128
|
+
digits = match.gsub(/\D/, '')
|
|
129
|
+
if digits.length == 9 && !digits.match(/^0{9}$|^1{9}$|^2{9}$|^3{9}$|^4{9}$|^5{9}$|^6{9}$|^7{9}$|^8{9}$|^9{9}$/)
|
|
130
|
+
SCRUBBED_VALUE
|
|
131
|
+
else
|
|
132
|
+
match
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def scrub_ip_addresses(string)
|
|
139
|
+
# Match IPv4 addresses
|
|
140
|
+
string.gsub(/\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/) do |match|
|
|
141
|
+
# Only scrub if it's a valid IP address
|
|
142
|
+
octets = match.split('.')
|
|
143
|
+
if octets.all? { |octet| octet.to_i <= 255 }
|
|
144
|
+
# Keep first octet for debugging purposes
|
|
145
|
+
"#{octets.first}.xxx.xxx.xxx"
|
|
146
|
+
else
|
|
147
|
+
match
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def luhn_valid?(number)
|
|
153
|
+
# Basic Luhn algorithm check for credit card validation
|
|
154
|
+
digits = number.reverse.chars.map(&:to_i)
|
|
155
|
+
|
|
156
|
+
sum = digits.each_with_index.sum do |digit, index|
|
|
157
|
+
if index.odd?
|
|
158
|
+
doubled = digit * 2
|
|
159
|
+
doubled > 9 ? doubled - 9 : doubled
|
|
160
|
+
else
|
|
161
|
+
digit
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
sum % 10 == 0
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module ActiveRabbit
|
|
7
|
+
module Client
|
|
8
|
+
class Railtie < Rails::Railtie
|
|
9
|
+
config.active_rabbit = ActiveSupport::OrderedOptions.new
|
|
10
|
+
|
|
11
|
+
initializer "active_rabbit.configure" do |app|
|
|
12
|
+
# Configure ActiveRabbit from Rails configuration
|
|
13
|
+
ActiveRabbit::Client.configure do |config|
|
|
14
|
+
config.environment = Rails.env
|
|
15
|
+
config.logger = Rails.logger
|
|
16
|
+
config.release = detect_release(app)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Set up exception tracking
|
|
20
|
+
setup_exception_tracking(app) if ActiveRabbit::Client.configured?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "active_rabbit.subscribe_to_notifications" do
|
|
24
|
+
next unless ActiveRabbit::Client.configured?
|
|
25
|
+
|
|
26
|
+
# Subscribe to Action Controller events
|
|
27
|
+
subscribe_to_controller_events
|
|
28
|
+
|
|
29
|
+
# Subscribe to Active Record events
|
|
30
|
+
subscribe_to_active_record_events
|
|
31
|
+
|
|
32
|
+
# Subscribe to Action View events
|
|
33
|
+
subscribe_to_action_view_events
|
|
34
|
+
|
|
35
|
+
# Subscribe to Action Mailer events (if available)
|
|
36
|
+
subscribe_to_action_mailer_events if defined?(ActionMailer)
|
|
37
|
+
|
|
38
|
+
# Subscribe to exception notifications
|
|
39
|
+
subscribe_to_exception_notifications
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
initializer "active_rabbit.add_middleware" do |app|
|
|
43
|
+
next unless ActiveRabbit::Client.configured?
|
|
44
|
+
|
|
45
|
+
# Add request context middleware
|
|
46
|
+
app.middleware.insert_before ActionDispatch::ShowExceptions, RequestContextMiddleware
|
|
47
|
+
|
|
48
|
+
# Add exception catching middleware
|
|
49
|
+
app.middleware.insert_before ActionDispatch::ShowExceptions, ExceptionMiddleware
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def setup_exception_tracking(app)
|
|
55
|
+
# Handle uncaught exceptions in development
|
|
56
|
+
if Rails.env.development? || Rails.env.test?
|
|
57
|
+
app.config.consider_all_requests_local = false if Rails.env.test?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def subscribe_to_controller_events
|
|
62
|
+
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, payload|
|
|
63
|
+
begin
|
|
64
|
+
duration_ms = ((finished - started) * 1000).round(2)
|
|
65
|
+
|
|
66
|
+
ActiveRabbit::Client.track_performance(
|
|
67
|
+
"controller.action",
|
|
68
|
+
duration_ms,
|
|
69
|
+
metadata: {
|
|
70
|
+
controller: payload[:controller],
|
|
71
|
+
action: payload[:action],
|
|
72
|
+
format: payload[:format],
|
|
73
|
+
method: payload[:method],
|
|
74
|
+
path: payload[:path],
|
|
75
|
+
status: payload[:status],
|
|
76
|
+
view_runtime: payload[:view_runtime],
|
|
77
|
+
db_runtime: payload[:db_runtime]
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Track slow requests
|
|
82
|
+
if duration_ms > 1000 # Slower than 1 second
|
|
83
|
+
ActiveRabbit::Client.track_event(
|
|
84
|
+
"slow_request",
|
|
85
|
+
{
|
|
86
|
+
controller: payload[:controller],
|
|
87
|
+
action: payload[:action],
|
|
88
|
+
duration_ms: duration_ms,
|
|
89
|
+
method: payload[:method],
|
|
90
|
+
path: payload[:path]
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
rescue => e
|
|
95
|
+
Rails.logger.error "[ActiveRabbit] Error tracking controller action: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def subscribe_to_active_record_events
|
|
101
|
+
# Track database queries for N+1 detection
|
|
102
|
+
ActiveSupport::Notifications.subscribe "sql.active_record" do |name, started, finished, unique_id, payload|
|
|
103
|
+
begin
|
|
104
|
+
next if payload[:name] == "SCHEMA" || payload[:name] == "CACHE"
|
|
105
|
+
|
|
106
|
+
duration_ms = ((finished - started) * 1000).round(2)
|
|
107
|
+
|
|
108
|
+
# Track query for N+1 detection
|
|
109
|
+
if ActiveRabbit::Client.configuration.enable_n_plus_one_detection
|
|
110
|
+
n_plus_one_detector.track_query(
|
|
111
|
+
payload[:sql],
|
|
112
|
+
payload[:bindings],
|
|
113
|
+
payload[:name],
|
|
114
|
+
duration_ms
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Track slow queries
|
|
119
|
+
if duration_ms > 100 # Slower than 100ms
|
|
120
|
+
ActiveRabbit::Client.track_event(
|
|
121
|
+
"slow_query",
|
|
122
|
+
{
|
|
123
|
+
sql: payload[:sql],
|
|
124
|
+
duration_ms: duration_ms,
|
|
125
|
+
name: payload[:name]
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
rescue => e
|
|
130
|
+
Rails.logger.error "[ActiveRabbit] Error tracking SQL query: #{e.message}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def subscribe_to_action_view_events
|
|
136
|
+
ActiveSupport::Notifications.subscribe "render_template.action_view" do |name, started, finished, unique_id, payload|
|
|
137
|
+
begin
|
|
138
|
+
duration_ms = ((finished - started) * 1000).round(2)
|
|
139
|
+
|
|
140
|
+
# Track slow template renders
|
|
141
|
+
if duration_ms > 50 # Slower than 50ms
|
|
142
|
+
ActiveRabbit::Client.track_event(
|
|
143
|
+
"slow_template_render",
|
|
144
|
+
{
|
|
145
|
+
template: payload[:identifier],
|
|
146
|
+
duration_ms: duration_ms,
|
|
147
|
+
layout: payload[:layout]
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
rescue => e
|
|
152
|
+
Rails.logger.error "[ActiveRabbit] Error tracking template render: #{e.message}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def subscribe_to_action_mailer_events
|
|
158
|
+
ActiveSupport::Notifications.subscribe "deliver.action_mailer" do |name, started, finished, unique_id, payload|
|
|
159
|
+
begin
|
|
160
|
+
duration_ms = ((finished - started) * 1000).round(2)
|
|
161
|
+
|
|
162
|
+
ActiveRabbit::Client.track_event(
|
|
163
|
+
"email_sent",
|
|
164
|
+
{
|
|
165
|
+
mailer: payload[:mailer],
|
|
166
|
+
action: payload[:action],
|
|
167
|
+
duration_ms: duration_ms
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
rescue => e
|
|
171
|
+
Rails.logger.error "[ActiveRabbit] Error tracking email delivery: #{e.message}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def subscribe_to_exception_notifications
|
|
177
|
+
# Subscribe to Rails exception notifications
|
|
178
|
+
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data|
|
|
179
|
+
next unless ActiveRabbit::Client.configured?
|
|
180
|
+
next unless data[:exception]
|
|
181
|
+
|
|
182
|
+
exception_class, exception_message = data[:exception]
|
|
183
|
+
|
|
184
|
+
puts "[ActiveRabbit] Exception notification received: #{exception_class}: #{exception_message}"
|
|
185
|
+
puts "[ActiveRabbit] Available data keys: #{data.keys.inspect}"
|
|
186
|
+
|
|
187
|
+
# Create exception with proper backtrace
|
|
188
|
+
exception = exception_class.constantize.new(exception_message)
|
|
189
|
+
|
|
190
|
+
# Try to get backtrace from the original exception if available
|
|
191
|
+
if data[:exception_object]
|
|
192
|
+
exception.set_backtrace(data[:exception_object].backtrace)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
ActiveRabbit::Client.track_exception(
|
|
196
|
+
exception,
|
|
197
|
+
context: {
|
|
198
|
+
request: {
|
|
199
|
+
method: data[:method],
|
|
200
|
+
path: data[:path],
|
|
201
|
+
controller: data[:controller],
|
|
202
|
+
action: data[:action],
|
|
203
|
+
status: data[:status],
|
|
204
|
+
format: data[:format]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def detect_release(app)
|
|
212
|
+
# Try to detect release from various sources
|
|
213
|
+
ENV["HEROKU_SLUG_COMMIT"] ||
|
|
214
|
+
ENV["GITHUB_SHA"] ||
|
|
215
|
+
ENV["GITLAB_COMMIT_SHA"] ||
|
|
216
|
+
app.config.active_rabbit.release ||
|
|
217
|
+
detect_git_sha
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def detect_git_sha
|
|
221
|
+
return unless Rails.root.join(".git").directory?
|
|
222
|
+
|
|
223
|
+
`git rev-parse HEAD 2>/dev/null`.chomp
|
|
224
|
+
rescue
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def n_plus_one_detector
|
|
229
|
+
@n_plus_one_detector ||= NPlusOneDetector.new(ActiveRabbit::Client.configuration)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Middleware for adding request context
|
|
234
|
+
class RequestContextMiddleware
|
|
235
|
+
def initialize(app)
|
|
236
|
+
@app = app
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def call(env)
|
|
240
|
+
request = ActionDispatch::Request.new(env)
|
|
241
|
+
|
|
242
|
+
# Skip certain requests
|
|
243
|
+
return @app.call(env) if should_skip_request?(request)
|
|
244
|
+
|
|
245
|
+
# Set request context
|
|
246
|
+
request_context = build_request_context(request)
|
|
247
|
+
Thread.current[:active_rabbit_request_context] = request_context
|
|
248
|
+
|
|
249
|
+
# Start N+1 detection for this request
|
|
250
|
+
request_id = SecureRandom.uuid
|
|
251
|
+
n_plus_one_detector.start_request(request_id)
|
|
252
|
+
|
|
253
|
+
begin
|
|
254
|
+
@app.call(env)
|
|
255
|
+
ensure
|
|
256
|
+
# Clean up request context
|
|
257
|
+
Thread.current[:active_rabbit_request_context] = nil
|
|
258
|
+
n_plus_one_detector.finish_request(request_id)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
|
|
264
|
+
def should_skip_request?(request)
|
|
265
|
+
# Skip requests from ignored user agents
|
|
266
|
+
user_agent = request.headers["User-Agent"]
|
|
267
|
+
return true if ActiveRabbit::Client.configuration.should_ignore_user_agent?(user_agent)
|
|
268
|
+
|
|
269
|
+
# Skip asset requests
|
|
270
|
+
return true if request.path.start_with?("/assets/")
|
|
271
|
+
|
|
272
|
+
# Skip health checks
|
|
273
|
+
return true if request.path.match?(/\/(health|ping|status)/)
|
|
274
|
+
|
|
275
|
+
false
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def build_request_context(request)
|
|
279
|
+
{
|
|
280
|
+
method: request.method,
|
|
281
|
+
path: request.path,
|
|
282
|
+
query_string: request.query_string,
|
|
283
|
+
user_agent: request.headers["User-Agent"],
|
|
284
|
+
ip_address: request.remote_ip,
|
|
285
|
+
referer: request.referer,
|
|
286
|
+
request_id: request.headers["X-Request-ID"] || SecureRandom.uuid
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def n_plus_one_detector
|
|
291
|
+
@n_plus_one_detector ||= NPlusOneDetector.new(ActiveRabbit::Client.configuration)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Middleware for catching unhandled exceptions
|
|
296
|
+
class ExceptionMiddleware
|
|
297
|
+
def initialize(app)
|
|
298
|
+
@app = app
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def call(env)
|
|
302
|
+
puts "[ActiveRabbit] ExceptionMiddleware called for: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
|
|
303
|
+
@app.call(env)
|
|
304
|
+
rescue Exception => exception
|
|
305
|
+
# Track the exception
|
|
306
|
+
puts "[ActiveRabbit] ExceptionMiddleware caught: #{exception.class}: #{exception.message}"
|
|
307
|
+
request = ActionDispatch::Request.new(env)
|
|
308
|
+
|
|
309
|
+
ActiveRabbit::Client.track_exception(
|
|
310
|
+
exception,
|
|
311
|
+
context: {
|
|
312
|
+
request: {
|
|
313
|
+
method: request.method,
|
|
314
|
+
path: request.path,
|
|
315
|
+
query_string: request.query_string,
|
|
316
|
+
user_agent: request.headers["User-Agent"],
|
|
317
|
+
ip_address: request.remote_ip,
|
|
318
|
+
referer: request.referer
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Re-raise the exception so Rails can handle it normally
|
|
324
|
+
raise exception
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRabbit
|
|
4
|
+
module Client
|
|
5
|
+
class SidekiqMiddleware
|
|
6
|
+
def call(worker, job, queue)
|
|
7
|
+
start_time = Time.now
|
|
8
|
+
job_context = build_job_context(worker, job, queue)
|
|
9
|
+
|
|
10
|
+
# Set job context for the duration of the job
|
|
11
|
+
Thread.current[:active_rabbit_job_context] = job_context
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
result = yield
|
|
15
|
+
|
|
16
|
+
# Track successful job completion
|
|
17
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
18
|
+
track_job_performance(worker, job, queue, duration_ms, "completed")
|
|
19
|
+
|
|
20
|
+
result
|
|
21
|
+
rescue Exception => exception
|
|
22
|
+
# Track job failure
|
|
23
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
24
|
+
track_job_performance(worker, job, queue, duration_ms, "failed")
|
|
25
|
+
track_job_exception(exception, worker, job, queue)
|
|
26
|
+
|
|
27
|
+
# Re-raise the exception so Sidekiq can handle it
|
|
28
|
+
raise exception
|
|
29
|
+
ensure
|
|
30
|
+
# Clean up job context
|
|
31
|
+
Thread.current[:active_rabbit_job_context] = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_job_context(worker, job, queue)
|
|
38
|
+
{
|
|
39
|
+
worker_class: worker.class.name,
|
|
40
|
+
job_id: job["jid"],
|
|
41
|
+
queue: queue,
|
|
42
|
+
args: scrub_job_args(job["args"]),
|
|
43
|
+
retry_count: job["retry_count"] || 0,
|
|
44
|
+
enqueued_at: job["enqueued_at"] ? Time.at(job["enqueued_at"]) : nil,
|
|
45
|
+
created_at: job["created_at"] ? Time.at(job["created_at"]) : nil
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def track_job_performance(worker, job, queue, duration_ms, status)
|
|
50
|
+
return unless ActiveRabbit::Client.configured?
|
|
51
|
+
|
|
52
|
+
ActiveRabbit::Client.track_performance(
|
|
53
|
+
"sidekiq.job",
|
|
54
|
+
duration_ms,
|
|
55
|
+
metadata: {
|
|
56
|
+
worker_class: worker.class.name,
|
|
57
|
+
queue: queue,
|
|
58
|
+
status: status,
|
|
59
|
+
job_id: job["jid"],
|
|
60
|
+
retry_count: job["retry_count"] || 0,
|
|
61
|
+
args_count: job["args"]&.size || 0
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Track slow jobs
|
|
66
|
+
if duration_ms > 30_000 # Slower than 30 seconds
|
|
67
|
+
ActiveRabbit::Client.track_event(
|
|
68
|
+
"slow_sidekiq_job",
|
|
69
|
+
{
|
|
70
|
+
worker_class: worker.class.name,
|
|
71
|
+
queue: queue,
|
|
72
|
+
duration_ms: duration_ms,
|
|
73
|
+
job_id: job["jid"]
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Track job completion event
|
|
79
|
+
ActiveRabbit::Client.track_event(
|
|
80
|
+
"sidekiq_job_#{status}",
|
|
81
|
+
{
|
|
82
|
+
worker_class: worker.class.name,
|
|
83
|
+
queue: queue,
|
|
84
|
+
duration_ms: duration_ms,
|
|
85
|
+
retry_count: job["retry_count"] || 0
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def track_job_exception(exception, worker, job, queue)
|
|
91
|
+
return unless ActiveRabbit::Client.configured?
|
|
92
|
+
|
|
93
|
+
ActiveRabbit::Client.track_exception(
|
|
94
|
+
exception,
|
|
95
|
+
context: {
|
|
96
|
+
job: {
|
|
97
|
+
worker_class: worker.class.name,
|
|
98
|
+
queue: queue,
|
|
99
|
+
job_id: job["jid"],
|
|
100
|
+
args: scrub_job_args(job["args"]),
|
|
101
|
+
retry_count: job["retry_count"] || 0,
|
|
102
|
+
enqueued_at: job["enqueued_at"] ? Time.at(job["enqueued_at"]) : nil
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
tags: {
|
|
106
|
+
component: "sidekiq",
|
|
107
|
+
queue: queue,
|
|
108
|
+
worker: worker.class.name
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def scrub_job_args(args)
|
|
114
|
+
return args unless ActiveRabbit::Client.configuration.enable_pii_scrubbing
|
|
115
|
+
return args unless args.is_a?(Array)
|
|
116
|
+
|
|
117
|
+
PiiScrubber.new(ActiveRabbit::Client.configuration).scrub(args)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Auto-register the middleware if Sidekiq is available
|
|
122
|
+
if defined?(Sidekiq)
|
|
123
|
+
Sidekiq.configure_server do |config|
|
|
124
|
+
config.server_middleware do |chain|
|
|
125
|
+
chain.add ActiveRabbit::Client::SidekiqMiddleware
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|