brainzlab 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/brainzlab/configuration.rb +20 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +458 -25
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +700 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation.rb +91 -1
- data/lib/brainzlab/version.rb +1 -1
- metadata +11 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
class ActionDispatch
|
|
6
|
+
# Thresholds for slow operations (in milliseconds)
|
|
7
|
+
SLOW_MIDDLEWARE_THRESHOLD = 50
|
|
8
|
+
VERY_SLOW_MIDDLEWARE_THRESHOLD = 200
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def install!
|
|
12
|
+
return unless defined?(::ActionDispatch)
|
|
13
|
+
return if @installed
|
|
14
|
+
|
|
15
|
+
install_process_middleware_subscriber!
|
|
16
|
+
install_redirect_subscriber!
|
|
17
|
+
install_request_subscriber!
|
|
18
|
+
|
|
19
|
+
@installed = true
|
|
20
|
+
BrainzLab.debug_log('ActionDispatch instrumentation installed')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def installed?
|
|
24
|
+
@installed == true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# ============================================
|
|
30
|
+
# process_middleware.action_dispatch
|
|
31
|
+
# Fired when a middleware in the stack runs
|
|
32
|
+
# ============================================
|
|
33
|
+
def install_process_middleware_subscriber!
|
|
34
|
+
ActiveSupport::Notifications.subscribe('process_middleware.action_dispatch') do |*args|
|
|
35
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
36
|
+
handle_process_middleware(event)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def handle_process_middleware(event)
|
|
41
|
+
payload = event.payload
|
|
42
|
+
duration = event.duration.round(2)
|
|
43
|
+
|
|
44
|
+
middleware = payload[:middleware]
|
|
45
|
+
|
|
46
|
+
# Skip fast middleware to reduce noise
|
|
47
|
+
return if duration < 1
|
|
48
|
+
|
|
49
|
+
# Determine level based on duration
|
|
50
|
+
level = case duration
|
|
51
|
+
when 0...SLOW_MIDDLEWARE_THRESHOLD then :info
|
|
52
|
+
when SLOW_MIDDLEWARE_THRESHOLD...VERY_SLOW_MIDDLEWARE_THRESHOLD then :warning
|
|
53
|
+
else :error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Record breadcrumb
|
|
57
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
58
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
59
|
+
"Middleware: #{middleware} (#{duration}ms)",
|
|
60
|
+
category: 'dispatch.middleware',
|
|
61
|
+
level: level,
|
|
62
|
+
data: {
|
|
63
|
+
middleware: middleware,
|
|
64
|
+
duration_ms: duration
|
|
65
|
+
}.compact
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Add Pulse span
|
|
70
|
+
record_middleware_span(event, middleware, duration)
|
|
71
|
+
|
|
72
|
+
# Log slow middleware
|
|
73
|
+
if duration >= SLOW_MIDDLEWARE_THRESHOLD && BrainzLab.configuration.recall_effectively_enabled?
|
|
74
|
+
log_level = duration >= VERY_SLOW_MIDDLEWARE_THRESHOLD ? :error : :warn
|
|
75
|
+
BrainzLab::Recall.send(
|
|
76
|
+
log_level,
|
|
77
|
+
"Slow middleware: #{middleware} (#{duration}ms)",
|
|
78
|
+
middleware: middleware,
|
|
79
|
+
duration_ms: duration
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
BrainzLab.debug_log("ActionDispatch process_middleware instrumentation failed: #{e.message}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ============================================
|
|
87
|
+
# redirect.action_dispatch
|
|
88
|
+
# Fired when a redirect response is sent
|
|
89
|
+
# ============================================
|
|
90
|
+
def install_redirect_subscriber!
|
|
91
|
+
ActiveSupport::Notifications.subscribe('redirect.action_dispatch') do |*args|
|
|
92
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
93
|
+
handle_redirect(event)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_redirect(event)
|
|
98
|
+
payload = event.payload
|
|
99
|
+
duration = event.duration.round(2)
|
|
100
|
+
|
|
101
|
+
status = payload[:status]
|
|
102
|
+
location = payload[:location]
|
|
103
|
+
request = payload[:request]
|
|
104
|
+
|
|
105
|
+
# Record breadcrumb
|
|
106
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
107
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
108
|
+
"Redirect #{status}: #{truncate_url(location)}",
|
|
109
|
+
category: 'dispatch.redirect',
|
|
110
|
+
level: :info,
|
|
111
|
+
data: {
|
|
112
|
+
status: status,
|
|
113
|
+
location: truncate_url(location),
|
|
114
|
+
duration_ms: duration
|
|
115
|
+
}.compact
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Add Pulse span
|
|
120
|
+
record_redirect_span(event, status, location, duration)
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
BrainzLab.debug_log("ActionDispatch redirect instrumentation failed: #{e.message}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ============================================
|
|
126
|
+
# request.action_dispatch
|
|
127
|
+
# Fired for the full request lifecycle
|
|
128
|
+
# ============================================
|
|
129
|
+
def install_request_subscriber!
|
|
130
|
+
ActiveSupport::Notifications.subscribe('request.action_dispatch') do |*args|
|
|
131
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
132
|
+
handle_request(event)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def handle_request(event)
|
|
137
|
+
payload = event.payload
|
|
138
|
+
duration = event.duration.round(2)
|
|
139
|
+
|
|
140
|
+
request = payload[:request]
|
|
141
|
+
response = payload[:response]
|
|
142
|
+
|
|
143
|
+
method = request&.method
|
|
144
|
+
path = request&.path
|
|
145
|
+
status = response&.status
|
|
146
|
+
|
|
147
|
+
# Record breadcrumb
|
|
148
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
149
|
+
level = status && status >= 400 ? :warning : :info
|
|
150
|
+
level = :error if status && status >= 500
|
|
151
|
+
|
|
152
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
153
|
+
"Request: #{method} #{path} -> #{status} (#{duration}ms)",
|
|
154
|
+
category: 'dispatch.request',
|
|
155
|
+
level: level,
|
|
156
|
+
data: {
|
|
157
|
+
method: method,
|
|
158
|
+
path: path,
|
|
159
|
+
status: status,
|
|
160
|
+
duration_ms: duration
|
|
161
|
+
}.compact
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Add Pulse span
|
|
166
|
+
record_request_span(event, method, path, status, duration)
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
BrainzLab.debug_log("ActionDispatch request instrumentation failed: #{e.message}")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ============================================
|
|
172
|
+
# Span Recording Helpers
|
|
173
|
+
# ============================================
|
|
174
|
+
def record_middleware_span(event, middleware, duration)
|
|
175
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
176
|
+
|
|
177
|
+
tracer = BrainzLab::Pulse.tracer
|
|
178
|
+
return unless tracer.current_trace
|
|
179
|
+
|
|
180
|
+
span_data = {
|
|
181
|
+
span_id: SecureRandom.uuid,
|
|
182
|
+
name: "middleware.#{middleware.to_s.demodulize.underscore}",
|
|
183
|
+
kind: 'middleware',
|
|
184
|
+
started_at: event.time,
|
|
185
|
+
ended_at: event.end,
|
|
186
|
+
duration_ms: duration,
|
|
187
|
+
error: false,
|
|
188
|
+
data: {
|
|
189
|
+
'middleware.class' => middleware
|
|
190
|
+
}.compact
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
tracer.current_spans << span_data
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def record_redirect_span(event, status, location, duration)
|
|
197
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
198
|
+
|
|
199
|
+
tracer = BrainzLab::Pulse.tracer
|
|
200
|
+
return unless tracer.current_trace
|
|
201
|
+
|
|
202
|
+
span_data = {
|
|
203
|
+
span_id: SecureRandom.uuid,
|
|
204
|
+
name: 'dispatch.redirect',
|
|
205
|
+
kind: 'http',
|
|
206
|
+
started_at: event.time,
|
|
207
|
+
ended_at: event.end,
|
|
208
|
+
duration_ms: duration,
|
|
209
|
+
error: false,
|
|
210
|
+
data: {
|
|
211
|
+
'http.status' => status,
|
|
212
|
+
'http.redirect_location' => truncate_url(location)
|
|
213
|
+
}.compact
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
tracer.current_spans << span_data
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def record_request_span(event, method, path, status, duration)
|
|
220
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
221
|
+
|
|
222
|
+
tracer = BrainzLab::Pulse.tracer
|
|
223
|
+
return unless tracer.current_trace
|
|
224
|
+
|
|
225
|
+
span_data = {
|
|
226
|
+
span_id: SecureRandom.uuid,
|
|
227
|
+
name: 'dispatch.request',
|
|
228
|
+
kind: 'http',
|
|
229
|
+
started_at: event.time,
|
|
230
|
+
ended_at: event.end,
|
|
231
|
+
duration_ms: duration,
|
|
232
|
+
error: status && status >= 500,
|
|
233
|
+
data: {
|
|
234
|
+
'http.method' => method,
|
|
235
|
+
'http.path' => path,
|
|
236
|
+
'http.status' => status
|
|
237
|
+
}.compact
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
tracer.current_spans << span_data
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# ============================================
|
|
244
|
+
# Helper Methods
|
|
245
|
+
# ============================================
|
|
246
|
+
def truncate_url(url, max_length = 200)
|
|
247
|
+
return 'unknown' unless url
|
|
248
|
+
|
|
249
|
+
url_str = url.to_s
|
|
250
|
+
if url_str.length > max_length
|
|
251
|
+
"#{url_str[0, max_length - 3]}..."
|
|
252
|
+
else
|
|
253
|
+
url_str
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
class ActionMailbox
|
|
6
|
+
# Thresholds for slow processing (in milliseconds)
|
|
7
|
+
SLOW_PROCESSING_THRESHOLD = 1000 # 1 second
|
|
8
|
+
VERY_SLOW_PROCESSING_THRESHOLD = 5000 # 5 seconds
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def install!
|
|
12
|
+
return unless defined?(::ActionMailbox)
|
|
13
|
+
return if @installed
|
|
14
|
+
|
|
15
|
+
install_process_subscriber!
|
|
16
|
+
|
|
17
|
+
@installed = true
|
|
18
|
+
BrainzLab.debug_log('ActionMailbox instrumentation installed')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def installed?
|
|
22
|
+
@installed == true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# ============================================
|
|
28
|
+
# process.action_mailbox
|
|
29
|
+
# Fired when an inbound email is processed
|
|
30
|
+
# ============================================
|
|
31
|
+
def install_process_subscriber!
|
|
32
|
+
ActiveSupport::Notifications.subscribe('process.action_mailbox') do |*args|
|
|
33
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
34
|
+
handle_process(event)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_process(event)
|
|
39
|
+
payload = event.payload
|
|
40
|
+
duration = event.duration.round(2)
|
|
41
|
+
|
|
42
|
+
mailbox = payload[:mailbox]
|
|
43
|
+
inbound_email = payload[:inbound_email]
|
|
44
|
+
|
|
45
|
+
mailbox_class = mailbox.is_a?(Class) ? mailbox.name : mailbox.class.name
|
|
46
|
+
email_id = inbound_email&.id
|
|
47
|
+
email_status = inbound_email&.status
|
|
48
|
+
message_id = inbound_email&.message_id
|
|
49
|
+
|
|
50
|
+
# Extract sender/recipient info if available
|
|
51
|
+
from = extract_from(inbound_email)
|
|
52
|
+
to = extract_to(inbound_email)
|
|
53
|
+
subject = extract_subject(inbound_email)
|
|
54
|
+
|
|
55
|
+
# Determine level based on duration
|
|
56
|
+
level = case duration
|
|
57
|
+
when 0...SLOW_PROCESSING_THRESHOLD then :info
|
|
58
|
+
when SLOW_PROCESSING_THRESHOLD...VERY_SLOW_PROCESSING_THRESHOLD then :warning
|
|
59
|
+
else :error
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Record breadcrumb
|
|
63
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
64
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
65
|
+
"Mailbox process: #{mailbox_class} (#{duration}ms)",
|
|
66
|
+
category: 'mailbox.process',
|
|
67
|
+
level: level,
|
|
68
|
+
data: {
|
|
69
|
+
mailbox: mailbox_class,
|
|
70
|
+
email_id: email_id,
|
|
71
|
+
status: email_status,
|
|
72
|
+
message_id: truncate(message_id),
|
|
73
|
+
from: truncate(from),
|
|
74
|
+
subject: truncate(subject, 100),
|
|
75
|
+
duration_ms: duration
|
|
76
|
+
}.compact
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Add Pulse span
|
|
81
|
+
record_process_span(event, mailbox_class, email_id, duration, email_status, from, to, subject)
|
|
82
|
+
|
|
83
|
+
# Log to Recall
|
|
84
|
+
log_email_processing(mailbox_class, email_id, email_status, duration, from, to, subject)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
BrainzLab.debug_log("ActionMailbox process instrumentation failed: #{e.message}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ============================================
|
|
90
|
+
# Span Recording
|
|
91
|
+
# ============================================
|
|
92
|
+
def record_process_span(event, mailbox_class, email_id, duration, status, from, to, subject)
|
|
93
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
94
|
+
|
|
95
|
+
tracer = BrainzLab::Pulse.tracer
|
|
96
|
+
return unless tracer.current_trace
|
|
97
|
+
|
|
98
|
+
span_data = {
|
|
99
|
+
span_id: SecureRandom.uuid,
|
|
100
|
+
name: "mailbox.process.#{mailbox_class.underscore}",
|
|
101
|
+
kind: 'mailbox',
|
|
102
|
+
started_at: event.time,
|
|
103
|
+
ended_at: event.end,
|
|
104
|
+
duration_ms: duration,
|
|
105
|
+
error: status == 'bounced' || status == 'failed',
|
|
106
|
+
data: {
|
|
107
|
+
'mailbox.class' => mailbox_class,
|
|
108
|
+
'mailbox.email_id' => email_id,
|
|
109
|
+
'mailbox.status' => status,
|
|
110
|
+
'mailbox.from' => truncate(from),
|
|
111
|
+
'mailbox.to' => truncate(to),
|
|
112
|
+
'mailbox.subject' => truncate(subject, 100)
|
|
113
|
+
}.compact
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
tracer.current_spans << span_data
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ============================================
|
|
120
|
+
# Logging
|
|
121
|
+
# ============================================
|
|
122
|
+
def log_email_processing(mailbox_class, email_id, status, duration, from, to, subject)
|
|
123
|
+
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
124
|
+
|
|
125
|
+
# Determine log level based on status and duration
|
|
126
|
+
if status == 'bounced' || status == 'failed'
|
|
127
|
+
BrainzLab::Recall.error(
|
|
128
|
+
"Mailbox processing failed: #{mailbox_class}",
|
|
129
|
+
mailbox: mailbox_class,
|
|
130
|
+
email_id: email_id,
|
|
131
|
+
status: status,
|
|
132
|
+
from: from,
|
|
133
|
+
to: to,
|
|
134
|
+
subject: truncate(subject, 200),
|
|
135
|
+
duration_ms: duration
|
|
136
|
+
)
|
|
137
|
+
elsif duration >= SLOW_PROCESSING_THRESHOLD
|
|
138
|
+
level = duration >= VERY_SLOW_PROCESSING_THRESHOLD ? :error : :warn
|
|
139
|
+
BrainzLab::Recall.send(
|
|
140
|
+
level,
|
|
141
|
+
"Slow mailbox processing: #{mailbox_class} (#{duration}ms)",
|
|
142
|
+
mailbox: mailbox_class,
|
|
143
|
+
email_id: email_id,
|
|
144
|
+
status: status,
|
|
145
|
+
duration_ms: duration,
|
|
146
|
+
threshold_exceeded: duration >= VERY_SLOW_PROCESSING_THRESHOLD ? 'critical' : 'warning'
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ============================================
|
|
152
|
+
# Helper Methods
|
|
153
|
+
# ============================================
|
|
154
|
+
def extract_from(inbound_email)
|
|
155
|
+
return nil unless inbound_email
|
|
156
|
+
|
|
157
|
+
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:from)
|
|
158
|
+
Array(inbound_email.mail.from).first
|
|
159
|
+
end
|
|
160
|
+
rescue StandardError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def extract_to(inbound_email)
|
|
165
|
+
return nil unless inbound_email
|
|
166
|
+
|
|
167
|
+
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:to)
|
|
168
|
+
Array(inbound_email.mail.to).first
|
|
169
|
+
end
|
|
170
|
+
rescue StandardError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def extract_subject(inbound_email)
|
|
175
|
+
return nil unless inbound_email
|
|
176
|
+
|
|
177
|
+
if inbound_email.respond_to?(:mail) && inbound_email.mail.respond_to?(:subject)
|
|
178
|
+
inbound_email.mail.subject
|
|
179
|
+
end
|
|
180
|
+
rescue StandardError
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def truncate(value, max_length = 200)
|
|
185
|
+
return nil unless value
|
|
186
|
+
|
|
187
|
+
str = value.to_s
|
|
188
|
+
if str.length > max_length
|
|
189
|
+
"#{str[0, max_length - 3]}..."
|
|
190
|
+
else
|
|
191
|
+
str
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|