brainzlab-rails 0.1.1
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/CLAUDE.md +144 -0
- data/IMPLEMENTATION_PLAN.md +370 -0
- data/Rakefile +8 -0
- data/brainzlab-rails.gemspec +42 -0
- data/lib/brainzlab/rails/analyzers/cache_efficiency.rb +123 -0
- data/lib/brainzlab/rails/analyzers/n_plus_one_detector.rb +90 -0
- data/lib/brainzlab/rails/analyzers/slow_query_analyzer.rb +118 -0
- data/lib/brainzlab/rails/collectors/action_cable.rb +212 -0
- data/lib/brainzlab/rails/collectors/action_controller.rb +299 -0
- data/lib/brainzlab/rails/collectors/action_mailer.rb +187 -0
- data/lib/brainzlab/rails/collectors/action_view.rb +176 -0
- data/lib/brainzlab/rails/collectors/active_job.rb +374 -0
- data/lib/brainzlab/rails/collectors/active_record.rb +250 -0
- data/lib/brainzlab/rails/collectors/active_storage.rb +306 -0
- data/lib/brainzlab/rails/collectors/base.rb +129 -0
- data/lib/brainzlab/rails/collectors/cache.rb +384 -0
- data/lib/brainzlab/rails/configuration.rb +121 -0
- data/lib/brainzlab/rails/event_router.rb +67 -0
- data/lib/brainzlab/rails/railtie.rb +98 -0
- data/lib/brainzlab/rails/subscriber.rb +164 -0
- data/lib/brainzlab/rails/version.rb +7 -0
- data/lib/brainzlab-rails.rb +72 -0
- metadata +178 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
module Collectors
|
|
6
|
+
# Collects Action Controller events and routes to products
|
|
7
|
+
# Primary source for HTTP request observability
|
|
8
|
+
class ActionController < Base
|
|
9
|
+
def process(event_data)
|
|
10
|
+
case event_data[:name]
|
|
11
|
+
when 'start_processing.action_controller'
|
|
12
|
+
handle_start_processing(event_data)
|
|
13
|
+
when 'process_action.action_controller'
|
|
14
|
+
handle_process_action(event_data)
|
|
15
|
+
when 'redirect_to.action_controller'
|
|
16
|
+
handle_redirect(event_data)
|
|
17
|
+
when 'halted_callback.action_controller'
|
|
18
|
+
handle_halted_callback(event_data)
|
|
19
|
+
when 'send_file.action_controller', 'send_data.action_controller', 'send_stream.action_controller'
|
|
20
|
+
handle_send_file(event_data)
|
|
21
|
+
when 'unpermitted_parameters.action_controller'
|
|
22
|
+
handle_unpermitted_parameters(event_data)
|
|
23
|
+
when 'rate_limit.action_controller'
|
|
24
|
+
handle_rate_limit(event_data)
|
|
25
|
+
when /fragment.*\.action_controller$/
|
|
26
|
+
handle_fragment_cache(event_data)
|
|
27
|
+
when 'deprecation.rails'
|
|
28
|
+
handle_deprecation(event_data)
|
|
29
|
+
when 'process_middleware.action_dispatch'
|
|
30
|
+
handle_middleware(event_data)
|
|
31
|
+
when 'redirect.action_dispatch'
|
|
32
|
+
handle_dispatch_redirect(event_data)
|
|
33
|
+
when 'request.action_dispatch'
|
|
34
|
+
handle_dispatch_request(event_data)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def handle_start_processing(event_data)
|
|
41
|
+
payload = event_data[:payload]
|
|
42
|
+
controller = payload[:controller]
|
|
43
|
+
action = payload[:action]
|
|
44
|
+
|
|
45
|
+
return if @configuration.ignored_action?(controller, action)
|
|
46
|
+
|
|
47
|
+
# Add breadcrumb for request start
|
|
48
|
+
add_breadcrumb(
|
|
49
|
+
"#{controller}##{action} started",
|
|
50
|
+
category: 'http.request',
|
|
51
|
+
level: :info,
|
|
52
|
+
data: {
|
|
53
|
+
method: payload[:method],
|
|
54
|
+
path: payload[:path],
|
|
55
|
+
format: payload[:format]
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_process_action(event_data)
|
|
61
|
+
payload = event_data[:payload]
|
|
62
|
+
controller = payload[:controller]
|
|
63
|
+
action = payload[:action]
|
|
64
|
+
duration_ms = event_data[:duration_ms]
|
|
65
|
+
|
|
66
|
+
return if @configuration.ignored_action?(controller, action)
|
|
67
|
+
|
|
68
|
+
# Check for exception
|
|
69
|
+
if payload[:exception_object]
|
|
70
|
+
handle_request_error(event_data, payload[:exception_object])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# === PULSE: APM Span ===
|
|
74
|
+
send_to_pulse(event_data, {
|
|
75
|
+
name: "#{controller}##{action}",
|
|
76
|
+
category: 'http.request',
|
|
77
|
+
attributes: {
|
|
78
|
+
controller: controller,
|
|
79
|
+
action: action,
|
|
80
|
+
method: payload[:method],
|
|
81
|
+
path: payload[:path],
|
|
82
|
+
status: payload[:status],
|
|
83
|
+
format: payload[:format],
|
|
84
|
+
view_runtime_ms: payload[:view_runtime]&.round(2),
|
|
85
|
+
db_runtime_ms: payload[:db_runtime]&.round(2)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
# === RECALL: Structured Log ===
|
|
90
|
+
log_level = payload[:status].to_i >= 400 ? :warn : :info
|
|
91
|
+
send_to_recall(log_level, "#{payload[:method]} #{payload[:path]}", {
|
|
92
|
+
controller: controller,
|
|
93
|
+
action: action,
|
|
94
|
+
status: payload[:status],
|
|
95
|
+
duration_ms: duration_ms,
|
|
96
|
+
view_runtime_ms: payload[:view_runtime]&.round(2),
|
|
97
|
+
db_runtime_ms: payload[:db_runtime]&.round(2),
|
|
98
|
+
format: payload[:format],
|
|
99
|
+
params: sanitize_params(payload[:params])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
# === FLUX: Metrics ===
|
|
103
|
+
tags = { controller: controller, action: action, status: payload[:status] }
|
|
104
|
+
send_to_flux(:increment, 'rails.requests.total', 1, tags)
|
|
105
|
+
send_to_flux(:timing, 'rails.requests.duration_ms', duration_ms, tags)
|
|
106
|
+
|
|
107
|
+
if payload[:view_runtime]
|
|
108
|
+
send_to_flux(:timing, 'rails.view.runtime_ms', payload[:view_runtime], tags)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if payload[:db_runtime]
|
|
112
|
+
send_to_flux(:timing, 'rails.db.runtime_ms', payload[:db_runtime], tags)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# === REFLEX: Breadcrumb ===
|
|
116
|
+
add_breadcrumb(
|
|
117
|
+
"#{controller}##{action} completed (#{payload[:status]})",
|
|
118
|
+
category: 'http.request',
|
|
119
|
+
level: payload[:status].to_i >= 400 ? :warning : :info,
|
|
120
|
+
data: {
|
|
121
|
+
status: payload[:status],
|
|
122
|
+
duration_ms: duration_ms
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_request_error(event_data, exception)
|
|
128
|
+
payload = event_data[:payload]
|
|
129
|
+
|
|
130
|
+
send_to_reflex(exception, {
|
|
131
|
+
controller: payload[:controller],
|
|
132
|
+
action: payload[:action],
|
|
133
|
+
request_context: extract_request_context(payload),
|
|
134
|
+
params: sanitize_params(payload[:params])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
send_to_flux(:increment, 'rails.requests.errors', 1, {
|
|
138
|
+
controller: payload[:controller],
|
|
139
|
+
action: payload[:action],
|
|
140
|
+
exception_class: exception.class.name
|
|
141
|
+
})
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def handle_redirect(event_data)
|
|
145
|
+
payload = event_data[:payload]
|
|
146
|
+
|
|
147
|
+
add_breadcrumb(
|
|
148
|
+
"Redirect to #{payload[:location]}",
|
|
149
|
+
category: 'http.redirect',
|
|
150
|
+
level: :info,
|
|
151
|
+
data: {
|
|
152
|
+
status: payload[:status],
|
|
153
|
+
location: payload[:location]
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
send_to_flux(:increment, 'rails.redirects.total', 1, {
|
|
158
|
+
status: payload[:status]
|
|
159
|
+
})
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def handle_halted_callback(event_data)
|
|
163
|
+
payload = event_data[:payload]
|
|
164
|
+
filter = payload[:filter]
|
|
165
|
+
|
|
166
|
+
# This is often auth failures - important for security monitoring
|
|
167
|
+
add_breadcrumb(
|
|
168
|
+
"Callback halted by #{filter}",
|
|
169
|
+
category: 'http.callback',
|
|
170
|
+
level: :warning,
|
|
171
|
+
data: { filter: filter.to_s }
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
send_to_recall(:warn, "Request halted by callback", {
|
|
175
|
+
filter: filter.to_s
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
send_to_flux(:increment, 'rails.callbacks.halted', 1, {
|
|
179
|
+
filter: filter.to_s
|
|
180
|
+
})
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def handle_send_file(event_data)
|
|
184
|
+
payload = event_data[:payload]
|
|
185
|
+
|
|
186
|
+
send_to_pulse(event_data, {
|
|
187
|
+
name: "send_file",
|
|
188
|
+
category: 'http.file',
|
|
189
|
+
attributes: payload.slice(:path, :filename, :type, :disposition).compact
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
send_to_flux(:increment, 'rails.files.sent', 1)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def handle_unpermitted_parameters(event_data)
|
|
196
|
+
payload = event_data[:payload]
|
|
197
|
+
keys = payload[:keys]
|
|
198
|
+
context = payload[:context] || {}
|
|
199
|
+
|
|
200
|
+
# Security-relevant: log unpermitted params
|
|
201
|
+
send_to_recall(:warn, "Unpermitted parameters detected", {
|
|
202
|
+
keys: keys,
|
|
203
|
+
controller: context[:controller],
|
|
204
|
+
action: context[:action]
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
add_breadcrumb(
|
|
208
|
+
"Unpermitted params: #{keys.join(', ')}",
|
|
209
|
+
category: 'security.params',
|
|
210
|
+
level: :warning,
|
|
211
|
+
data: { keys: keys }
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
send_to_flux(:increment, 'rails.params.unpermitted', keys.size, {
|
|
215
|
+
controller: context[:controller],
|
|
216
|
+
action: context[:action]
|
|
217
|
+
})
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def handle_rate_limit(event_data)
|
|
221
|
+
payload = event_data[:payload]
|
|
222
|
+
|
|
223
|
+
send_to_recall(:warn, "Rate limit exceeded", {
|
|
224
|
+
count: payload[:count],
|
|
225
|
+
limit: payload[:to],
|
|
226
|
+
within: payload[:within],
|
|
227
|
+
name: payload[:name]
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
send_to_flux(:increment, 'rails.rate_limit.exceeded', 1, {
|
|
231
|
+
name: payload[:name]
|
|
232
|
+
})
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_fragment_cache(event_data)
|
|
236
|
+
payload = event_data[:payload]
|
|
237
|
+
operation = event_data[:name].split('.').first # read_fragment, write_fragment, etc.
|
|
238
|
+
|
|
239
|
+
send_to_flux(:increment, "rails.fragment_cache.#{operation}", 1)
|
|
240
|
+
|
|
241
|
+
add_breadcrumb(
|
|
242
|
+
"Fragment cache #{operation}: #{payload[:key]}",
|
|
243
|
+
category: 'cache.fragment',
|
|
244
|
+
level: :debug,
|
|
245
|
+
data: { key: payload[:key] }
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def handle_deprecation(event_data)
|
|
250
|
+
payload = event_data[:payload]
|
|
251
|
+
|
|
252
|
+
send_to_recall(:warn, "Deprecation warning: #{payload[:message]}", {
|
|
253
|
+
gem_name: payload[:gem_name],
|
|
254
|
+
deprecation_horizon: payload[:deprecation_horizon],
|
|
255
|
+
callstack: payload[:callstack]&.first(5)&.map(&:to_s)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
send_to_flux(:increment, 'rails.deprecations.total', 1, {
|
|
259
|
+
gem_name: payload[:gem_name]
|
|
260
|
+
})
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def handle_middleware(event_data)
|
|
264
|
+
payload = event_data[:payload]
|
|
265
|
+
|
|
266
|
+
send_to_pulse(event_data, {
|
|
267
|
+
name: "middleware.#{payload[:middleware]}",
|
|
268
|
+
category: 'http.middleware',
|
|
269
|
+
attributes: { middleware: payload[:middleware] }
|
|
270
|
+
})
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def handle_dispatch_redirect(event_data)
|
|
274
|
+
payload = event_data[:payload]
|
|
275
|
+
|
|
276
|
+
add_breadcrumb(
|
|
277
|
+
"Dispatch redirect to #{payload[:location]}",
|
|
278
|
+
category: 'http.dispatch',
|
|
279
|
+
level: :info,
|
|
280
|
+
data: {
|
|
281
|
+
status: payload[:status],
|
|
282
|
+
location: payload[:location],
|
|
283
|
+
source_location: payload[:source_location]
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def handle_dispatch_request(event_data)
|
|
289
|
+
# Initial request dispatch - useful for timing the full request lifecycle
|
|
290
|
+
send_to_pulse(event_data, {
|
|
291
|
+
name: 'request.dispatch',
|
|
292
|
+
category: 'http.dispatch',
|
|
293
|
+
attributes: {}
|
|
294
|
+
})
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
module Collectors
|
|
6
|
+
# Collects Action Mailer events for email observability
|
|
7
|
+
class ActionMailer < Base
|
|
8
|
+
def process(event_data)
|
|
9
|
+
case event_data[:name]
|
|
10
|
+
when 'deliver.action_mailer'
|
|
11
|
+
handle_deliver(event_data)
|
|
12
|
+
when 'process.action_mailer'
|
|
13
|
+
handle_process(event_data)
|
|
14
|
+
when 'process.action_mailbox'
|
|
15
|
+
handle_mailbox_process(event_data)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def handle_deliver(event_data)
|
|
22
|
+
payload = event_data[:payload]
|
|
23
|
+
mailer = payload[:mailer]
|
|
24
|
+
message_id = payload[:message_id]
|
|
25
|
+
subject = payload[:subject]
|
|
26
|
+
duration_ms = event_data[:duration_ms]
|
|
27
|
+
|
|
28
|
+
to_count = Array(payload[:to]).size
|
|
29
|
+
perform_deliveries = payload[:perform_deliveries]
|
|
30
|
+
|
|
31
|
+
# === PULSE: Email delivery span ===
|
|
32
|
+
send_to_pulse(event_data, {
|
|
33
|
+
name: "mailer.#{mailer}.deliver",
|
|
34
|
+
category: 'email.deliver',
|
|
35
|
+
attributes: {
|
|
36
|
+
mailer: mailer,
|
|
37
|
+
message_id: message_id,
|
|
38
|
+
subject: truncate_subject(subject),
|
|
39
|
+
to_count: to_count,
|
|
40
|
+
performed: perform_deliveries
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
# === FLUX: Metrics ===
|
|
45
|
+
tags = { mailer: mailer, performed: perform_deliveries }
|
|
46
|
+
send_to_flux(:increment, 'rails.mailer.delivered', 1, tags)
|
|
47
|
+
send_to_flux(:timing, 'rails.mailer.delivery_ms', duration_ms, tags)
|
|
48
|
+
send_to_flux(:histogram, 'rails.mailer.recipients', to_count, tags)
|
|
49
|
+
|
|
50
|
+
# === RECALL: Log delivery ===
|
|
51
|
+
send_to_recall(:info, "Email delivered", {
|
|
52
|
+
mailer: mailer,
|
|
53
|
+
message_id: message_id,
|
|
54
|
+
subject: truncate_subject(subject),
|
|
55
|
+
to_count: to_count,
|
|
56
|
+
from: payload[:from],
|
|
57
|
+
duration_ms: duration_ms,
|
|
58
|
+
performed: perform_deliveries
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
# === REFLEX: Breadcrumb ===
|
|
62
|
+
add_breadcrumb(
|
|
63
|
+
"Email: #{mailer} - #{truncate_subject(subject)}",
|
|
64
|
+
category: 'email.deliver',
|
|
65
|
+
level: :info,
|
|
66
|
+
data: {
|
|
67
|
+
mailer: mailer,
|
|
68
|
+
message_id: message_id,
|
|
69
|
+
to_count: to_count,
|
|
70
|
+
duration_ms: duration_ms
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_process(event_data)
|
|
76
|
+
payload = event_data[:payload]
|
|
77
|
+
mailer = payload[:mailer]
|
|
78
|
+
action = payload[:action]
|
|
79
|
+
duration_ms = event_data[:duration_ms]
|
|
80
|
+
|
|
81
|
+
# === PULSE: Mailer processing span ===
|
|
82
|
+
send_to_pulse(event_data, {
|
|
83
|
+
name: "mailer.#{mailer}##{action}",
|
|
84
|
+
category: 'email.process',
|
|
85
|
+
attributes: {
|
|
86
|
+
mailer: mailer,
|
|
87
|
+
action: action,
|
|
88
|
+
args: sanitize_mailer_args(payload[:args])
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# === FLUX: Metrics ===
|
|
93
|
+
send_to_flux(:increment, 'rails.mailer.processed', 1, {
|
|
94
|
+
mailer: mailer,
|
|
95
|
+
action: action
|
|
96
|
+
})
|
|
97
|
+
send_to_flux(:timing, 'rails.mailer.process_ms', duration_ms, {
|
|
98
|
+
mailer: mailer,
|
|
99
|
+
action: action
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
# === REFLEX: Breadcrumb ===
|
|
103
|
+
add_breadcrumb(
|
|
104
|
+
"Mailer: #{mailer}##{action}",
|
|
105
|
+
category: 'email.process',
|
|
106
|
+
level: :debug,
|
|
107
|
+
data: {
|
|
108
|
+
mailer: mailer,
|
|
109
|
+
action: action,
|
|
110
|
+
duration_ms: duration_ms
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def handle_mailbox_process(event_data)
|
|
116
|
+
payload = event_data[:payload]
|
|
117
|
+
mailbox = payload[:mailbox]
|
|
118
|
+
inbound_email = payload[:inbound_email] || {}
|
|
119
|
+
duration_ms = event_data[:duration_ms]
|
|
120
|
+
|
|
121
|
+
# === PULSE: Mailbox processing span ===
|
|
122
|
+
send_to_pulse(event_data, {
|
|
123
|
+
name: "mailbox.#{mailbox.class.name}",
|
|
124
|
+
category: 'email.inbound',
|
|
125
|
+
attributes: {
|
|
126
|
+
mailbox: mailbox.class.name,
|
|
127
|
+
message_id: inbound_email[:message_id],
|
|
128
|
+
status: inbound_email[:status]
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# === FLUX: Metrics ===
|
|
133
|
+
send_to_flux(:increment, 'rails.mailbox.processed', 1, {
|
|
134
|
+
mailbox: mailbox.class.name,
|
|
135
|
+
status: inbound_email[:status]
|
|
136
|
+
})
|
|
137
|
+
send_to_flux(:timing, 'rails.mailbox.process_ms', duration_ms, {
|
|
138
|
+
mailbox: mailbox.class.name
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
# === RECALL: Log inbound email ===
|
|
142
|
+
send_to_recall(:info, "Inbound email processed", {
|
|
143
|
+
mailbox: mailbox.class.name,
|
|
144
|
+
message_id: inbound_email[:message_id],
|
|
145
|
+
status: inbound_email[:status],
|
|
146
|
+
duration_ms: duration_ms
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
# === REFLEX: Breadcrumb ===
|
|
150
|
+
add_breadcrumb(
|
|
151
|
+
"Mailbox: #{mailbox.class.name}",
|
|
152
|
+
category: 'email.inbound',
|
|
153
|
+
level: :info,
|
|
154
|
+
data: {
|
|
155
|
+
mailbox: mailbox.class.name,
|
|
156
|
+
status: inbound_email[:status],
|
|
157
|
+
duration_ms: duration_ms
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def truncate_subject(subject, max_length = 100)
|
|
163
|
+
return '' if subject.nil?
|
|
164
|
+
|
|
165
|
+
subject.length > max_length ? "#{subject[0, max_length]}..." : subject
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def sanitize_mailer_args(args)
|
|
169
|
+
return [] unless args.is_a?(Array)
|
|
170
|
+
|
|
171
|
+
args.map do |arg|
|
|
172
|
+
case arg
|
|
173
|
+
when Hash
|
|
174
|
+
sanitize_params(arg)
|
|
175
|
+
when String, Numeric, Symbol, TrueClass, FalseClass, NilClass
|
|
176
|
+
arg
|
|
177
|
+
else
|
|
178
|
+
arg.class.name
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
rescue StandardError
|
|
182
|
+
[]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Rails
|
|
5
|
+
module Collectors
|
|
6
|
+
# Collects Action View rendering events
|
|
7
|
+
# Tracks template, partial, and layout rendering performance
|
|
8
|
+
class ActionView < Base
|
|
9
|
+
def process(event_data)
|
|
10
|
+
case event_data[:name]
|
|
11
|
+
when 'render_template.action_view'
|
|
12
|
+
handle_render_template(event_data)
|
|
13
|
+
when 'render_partial.action_view'
|
|
14
|
+
handle_render_partial(event_data)
|
|
15
|
+
when 'render_collection.action_view'
|
|
16
|
+
handle_render_collection(event_data)
|
|
17
|
+
when 'render_layout.action_view'
|
|
18
|
+
handle_render_layout(event_data)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def handle_render_template(event_data)
|
|
25
|
+
payload = event_data[:payload]
|
|
26
|
+
identifier = extract_template_name(payload[:identifier])
|
|
27
|
+
duration_ms = event_data[:duration_ms]
|
|
28
|
+
|
|
29
|
+
# === PULSE: View rendering span ===
|
|
30
|
+
send_to_pulse(event_data, {
|
|
31
|
+
name: "render_template.#{identifier}",
|
|
32
|
+
category: 'view.template',
|
|
33
|
+
attributes: {
|
|
34
|
+
identifier: identifier,
|
|
35
|
+
layout: payload[:layout]
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
# === FLUX: Metrics ===
|
|
40
|
+
send_to_flux(:timing, 'rails.view.template_ms', duration_ms, {
|
|
41
|
+
template: identifier
|
|
42
|
+
})
|
|
43
|
+
send_to_flux(:increment, 'rails.view.templates_rendered', 1)
|
|
44
|
+
|
|
45
|
+
# === REFLEX: Breadcrumb ===
|
|
46
|
+
add_breadcrumb(
|
|
47
|
+
"Rendered #{identifier}",
|
|
48
|
+
category: 'view.render',
|
|
49
|
+
level: :debug,
|
|
50
|
+
data: {
|
|
51
|
+
template: identifier,
|
|
52
|
+
layout: payload[:layout],
|
|
53
|
+
duration_ms: duration_ms
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handle_render_partial(event_data)
|
|
59
|
+
payload = event_data[:payload]
|
|
60
|
+
identifier = extract_template_name(payload[:identifier])
|
|
61
|
+
duration_ms = event_data[:duration_ms]
|
|
62
|
+
|
|
63
|
+
# === PULSE: Partial rendering span ===
|
|
64
|
+
send_to_pulse(event_data, {
|
|
65
|
+
name: "render_partial.#{identifier}",
|
|
66
|
+
category: 'view.partial',
|
|
67
|
+
attributes: {
|
|
68
|
+
identifier: identifier
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
# === FLUX: Metrics ===
|
|
73
|
+
send_to_flux(:timing, 'rails.view.partial_ms', duration_ms, {
|
|
74
|
+
partial: identifier
|
|
75
|
+
})
|
|
76
|
+
send_to_flux(:increment, 'rails.view.partials_rendered', 1)
|
|
77
|
+
|
|
78
|
+
# Flag slow partials (> 50ms is concerning for a partial)
|
|
79
|
+
if duration_ms > 50
|
|
80
|
+
send_to_recall(:warn, "Slow partial rendering", {
|
|
81
|
+
partial: identifier,
|
|
82
|
+
duration_ms: duration_ms
|
|
83
|
+
})
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_render_collection(event_data)
|
|
88
|
+
payload = event_data[:payload]
|
|
89
|
+
identifier = extract_template_name(payload[:identifier])
|
|
90
|
+
count = payload[:count] || 0
|
|
91
|
+
cache_hits = payload[:cache_hits] || 0
|
|
92
|
+
duration_ms = event_data[:duration_ms]
|
|
93
|
+
|
|
94
|
+
# === PULSE: Collection rendering span ===
|
|
95
|
+
send_to_pulse(event_data, {
|
|
96
|
+
name: "render_collection.#{identifier}",
|
|
97
|
+
category: 'view.collection',
|
|
98
|
+
attributes: {
|
|
99
|
+
identifier: identifier,
|
|
100
|
+
count: count,
|
|
101
|
+
cache_hits: cache_hits,
|
|
102
|
+
cache_hit_rate: count > 0 ? (cache_hits.to_f / count * 100).round(1) : 0
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
# === FLUX: Metrics ===
|
|
107
|
+
send_to_flux(:timing, 'rails.view.collection_ms', duration_ms, {
|
|
108
|
+
partial: identifier
|
|
109
|
+
})
|
|
110
|
+
send_to_flux(:gauge, 'rails.view.collection_size', count)
|
|
111
|
+
|
|
112
|
+
if cache_hits > 0
|
|
113
|
+
send_to_flux(:increment, 'rails.view.collection_cache_hits', cache_hits)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# === REFLEX: Breadcrumb ===
|
|
117
|
+
add_breadcrumb(
|
|
118
|
+
"Rendered collection #{identifier} (#{count} items, #{cache_hits} cached)",
|
|
119
|
+
category: 'view.collection',
|
|
120
|
+
level: :debug,
|
|
121
|
+
data: {
|
|
122
|
+
partial: identifier,
|
|
123
|
+
count: count,
|
|
124
|
+
cache_hits: cache_hits,
|
|
125
|
+
duration_ms: duration_ms
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Flag potentially slow collection renders
|
|
130
|
+
if count > 0 && duration_ms / count > 10
|
|
131
|
+
send_to_recall(:warn, "Slow collection item rendering", {
|
|
132
|
+
partial: identifier,
|
|
133
|
+
count: count,
|
|
134
|
+
avg_ms_per_item: (duration_ms / count).round(2),
|
|
135
|
+
total_duration_ms: duration_ms
|
|
136
|
+
})
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_render_layout(event_data)
|
|
141
|
+
payload = event_data[:payload]
|
|
142
|
+
identifier = extract_template_name(payload[:identifier])
|
|
143
|
+
duration_ms = event_data[:duration_ms]
|
|
144
|
+
|
|
145
|
+
# === PULSE: Layout rendering span ===
|
|
146
|
+
send_to_pulse(event_data, {
|
|
147
|
+
name: "render_layout.#{identifier}",
|
|
148
|
+
category: 'view.layout',
|
|
149
|
+
attributes: {
|
|
150
|
+
identifier: identifier
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
# === FLUX: Metrics ===
|
|
155
|
+
send_to_flux(:timing, 'rails.view.layout_ms', duration_ms, {
|
|
156
|
+
layout: identifier
|
|
157
|
+
})
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Extract clean template name from full path
|
|
161
|
+
def extract_template_name(identifier)
|
|
162
|
+
return 'unknown' if identifier.nil?
|
|
163
|
+
|
|
164
|
+
# Extract the relative path from app/views
|
|
165
|
+
if identifier.include?('/app/views/')
|
|
166
|
+
identifier.split('/app/views/').last
|
|
167
|
+
elsif identifier.include?('/views/')
|
|
168
|
+
identifier.split('/views/').last
|
|
169
|
+
else
|
|
170
|
+
File.basename(identifier)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|