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.
@@ -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