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,649 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
class ActionController
|
|
6
|
+
# Thresholds for slow request detection (in milliseconds)
|
|
7
|
+
SLOW_REQUEST_THRESHOLD = 500
|
|
8
|
+
VERY_SLOW_REQUEST_THRESHOLD = 2000
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def install!
|
|
12
|
+
return unless defined?(::ActionController)
|
|
13
|
+
return if @installed
|
|
14
|
+
|
|
15
|
+
install_start_processing_subscriber!
|
|
16
|
+
install_process_action_subscriber!
|
|
17
|
+
install_redirect_subscriber!
|
|
18
|
+
install_halted_callback_subscriber!
|
|
19
|
+
install_unpermitted_parameters_subscriber!
|
|
20
|
+
install_send_file_subscriber!
|
|
21
|
+
install_send_data_subscriber!
|
|
22
|
+
install_send_stream_subscriber!
|
|
23
|
+
|
|
24
|
+
# Fragment caching
|
|
25
|
+
install_write_fragment_subscriber!
|
|
26
|
+
install_read_fragment_subscriber!
|
|
27
|
+
install_expire_fragment_subscriber!
|
|
28
|
+
install_exist_fragment_subscriber!
|
|
29
|
+
|
|
30
|
+
# Rails 7.1+ rate limiting
|
|
31
|
+
install_rate_limit_subscriber! if rails_71_or_higher?
|
|
32
|
+
|
|
33
|
+
@installed = true
|
|
34
|
+
BrainzLab.debug_log('ActionController instrumentation installed')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def installed?
|
|
38
|
+
@installed == true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# ============================================
|
|
44
|
+
# Start Processing (request begins)
|
|
45
|
+
# ============================================
|
|
46
|
+
def install_start_processing_subscriber!
|
|
47
|
+
ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |*args|
|
|
48
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
49
|
+
handle_start_processing(event)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_start_processing(event)
|
|
54
|
+
payload = event.payload
|
|
55
|
+
|
|
56
|
+
controller = payload[:controller]
|
|
57
|
+
action = payload[:action]
|
|
58
|
+
method = payload[:method]
|
|
59
|
+
path = payload[:path]
|
|
60
|
+
format = payload[:format]
|
|
61
|
+
params = payload[:params]
|
|
62
|
+
|
|
63
|
+
# Skip health check endpoints
|
|
64
|
+
return if excluded_path?(path)
|
|
65
|
+
|
|
66
|
+
# Store start time for later use
|
|
67
|
+
Thread.current[:brainzlab_request_start] = event.time
|
|
68
|
+
|
|
69
|
+
# Record breadcrumb
|
|
70
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
71
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
72
|
+
"Request started: #{method} #{controller}##{action}",
|
|
73
|
+
category: 'http.start',
|
|
74
|
+
level: :info,
|
|
75
|
+
data: {
|
|
76
|
+
controller: controller,
|
|
77
|
+
action: action,
|
|
78
|
+
method: method,
|
|
79
|
+
path: truncate_path(path),
|
|
80
|
+
format: format
|
|
81
|
+
}.compact
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
BrainzLab.debug_log("ActionController start_processing instrumentation failed: #{e.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ============================================
|
|
89
|
+
# Process Action (main request instrumentation)
|
|
90
|
+
# ============================================
|
|
91
|
+
def install_process_action_subscriber!
|
|
92
|
+
ActiveSupport::Notifications.subscribe('process_action.action_controller') do |*args|
|
|
93
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
94
|
+
handle_process_action(event)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def handle_process_action(event)
|
|
99
|
+
payload = event.payload
|
|
100
|
+
duration = event.duration.round(2)
|
|
101
|
+
|
|
102
|
+
controller = payload[:controller]
|
|
103
|
+
action = payload[:action]
|
|
104
|
+
status = payload[:status]
|
|
105
|
+
method = payload[:method]
|
|
106
|
+
path = payload[:path]
|
|
107
|
+
format = payload[:format]
|
|
108
|
+
|
|
109
|
+
# Skip health check endpoints
|
|
110
|
+
return if excluded_path?(path)
|
|
111
|
+
|
|
112
|
+
# Add breadcrumb for Reflex
|
|
113
|
+
record_request_breadcrumb(payload, duration, status)
|
|
114
|
+
|
|
115
|
+
# Record trace for Pulse (if not already recording via middleware)
|
|
116
|
+
record_request_trace(event, payload, duration)
|
|
117
|
+
|
|
118
|
+
# Log slow requests to Recall
|
|
119
|
+
log_slow_request(payload, duration) if duration >= SLOW_REQUEST_THRESHOLD
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
BrainzLab.debug_log("ActionController process_action instrumentation failed: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def record_request_breadcrumb(payload, duration, status)
|
|
125
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
126
|
+
|
|
127
|
+
controller = payload[:controller]
|
|
128
|
+
action = payload[:action]
|
|
129
|
+
method = payload[:method]
|
|
130
|
+
path = payload[:path]
|
|
131
|
+
|
|
132
|
+
level = case status.to_i
|
|
133
|
+
when 200..399 then :info
|
|
134
|
+
when 400..499 then :warning
|
|
135
|
+
else :error
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Adjust for slow requests
|
|
139
|
+
level = :warning if level == :info && duration >= SLOW_REQUEST_THRESHOLD
|
|
140
|
+
level = :error if duration >= VERY_SLOW_REQUEST_THRESHOLD
|
|
141
|
+
|
|
142
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
143
|
+
"#{method} #{controller}##{action} -> #{status} (#{duration}ms)",
|
|
144
|
+
category: 'http.request',
|
|
145
|
+
level: level,
|
|
146
|
+
data: {
|
|
147
|
+
controller: controller,
|
|
148
|
+
action: action,
|
|
149
|
+
method: method,
|
|
150
|
+
path: truncate_path(path),
|
|
151
|
+
status: status,
|
|
152
|
+
format: payload[:format],
|
|
153
|
+
duration_ms: duration,
|
|
154
|
+
view_ms: payload[:view_runtime]&.round(2),
|
|
155
|
+
db_ms: payload[:db_runtime]&.round(2)
|
|
156
|
+
}.compact
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def record_request_trace(event, payload, duration)
|
|
161
|
+
# Only record if Pulse is enabled and no trace is already active
|
|
162
|
+
# (middleware should handle this, but this is a fallback)
|
|
163
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
164
|
+
|
|
165
|
+
tracer = BrainzLab::Pulse.tracer
|
|
166
|
+
return if tracer.current_trace # Already being traced by middleware
|
|
167
|
+
|
|
168
|
+
controller = payload[:controller]
|
|
169
|
+
action = payload[:action]
|
|
170
|
+
|
|
171
|
+
BrainzLab::Pulse.record_trace(
|
|
172
|
+
"#{controller}##{action}",
|
|
173
|
+
started_at: event.time,
|
|
174
|
+
ended_at: event.end,
|
|
175
|
+
kind: 'request',
|
|
176
|
+
request_method: payload[:method],
|
|
177
|
+
request_path: payload[:path],
|
|
178
|
+
controller: controller,
|
|
179
|
+
action: action,
|
|
180
|
+
status: payload[:status],
|
|
181
|
+
view_ms: payload[:view_runtime]&.round(2),
|
|
182
|
+
db_ms: payload[:db_runtime]&.round(2),
|
|
183
|
+
error: payload[:status].to_i >= 500,
|
|
184
|
+
error_class: payload[:exception]&.first,
|
|
185
|
+
error_message: payload[:exception]&.last
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def log_slow_request(payload, duration)
|
|
190
|
+
return unless BrainzLab.configuration.recall_effectively_enabled?
|
|
191
|
+
|
|
192
|
+
level = duration >= VERY_SLOW_REQUEST_THRESHOLD ? :error : :warn
|
|
193
|
+
|
|
194
|
+
BrainzLab::Recall.send(
|
|
195
|
+
level,
|
|
196
|
+
"Slow request: #{payload[:controller]}##{payload[:action]} (#{duration}ms)",
|
|
197
|
+
controller: payload[:controller],
|
|
198
|
+
action: payload[:action],
|
|
199
|
+
method: payload[:method],
|
|
200
|
+
path: truncate_path(payload[:path]),
|
|
201
|
+
status: payload[:status],
|
|
202
|
+
format: payload[:format],
|
|
203
|
+
duration_ms: duration,
|
|
204
|
+
view_ms: payload[:view_runtime]&.round(2),
|
|
205
|
+
db_ms: payload[:db_runtime]&.round(2),
|
|
206
|
+
threshold_exceeded: duration >= VERY_SLOW_REQUEST_THRESHOLD ? 'critical' : 'warning'
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ============================================
|
|
211
|
+
# Redirect Tracking
|
|
212
|
+
# ============================================
|
|
213
|
+
def install_redirect_subscriber!
|
|
214
|
+
ActiveSupport::Notifications.subscribe('redirect_to.action_controller') do |*args|
|
|
215
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
216
|
+
handle_redirect(event)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def handle_redirect(event)
|
|
221
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
222
|
+
|
|
223
|
+
payload = event.payload
|
|
224
|
+
location = payload[:location]
|
|
225
|
+
status = payload[:status] || 302
|
|
226
|
+
|
|
227
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
228
|
+
"Redirect -> #{truncate_path(location)} (#{status})",
|
|
229
|
+
category: 'http.redirect',
|
|
230
|
+
level: :info,
|
|
231
|
+
data: {
|
|
232
|
+
location: truncate_path(location),
|
|
233
|
+
status: status
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
rescue StandardError => e
|
|
237
|
+
BrainzLab.debug_log("ActionController redirect instrumentation failed: #{e.message}")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# ============================================
|
|
241
|
+
# Halted Callbacks (before_action filters)
|
|
242
|
+
# ============================================
|
|
243
|
+
def install_halted_callback_subscriber!
|
|
244
|
+
ActiveSupport::Notifications.subscribe('halted_callback.action_controller') do |*args|
|
|
245
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
246
|
+
handle_halted_callback(event)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def handle_halted_callback(event)
|
|
251
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
252
|
+
|
|
253
|
+
payload = event.payload
|
|
254
|
+
filter = payload[:filter]
|
|
255
|
+
|
|
256
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
257
|
+
"Request halted by filter: #{filter}",
|
|
258
|
+
category: 'http.filter',
|
|
259
|
+
level: :warning,
|
|
260
|
+
data: {
|
|
261
|
+
filter: filter.to_s
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Also log to Recall - halted callbacks can indicate auth issues
|
|
266
|
+
if BrainzLab.configuration.recall_effectively_enabled?
|
|
267
|
+
BrainzLab::Recall.info(
|
|
268
|
+
"Request halted by before_action filter",
|
|
269
|
+
filter: filter.to_s
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
rescue StandardError => e
|
|
273
|
+
BrainzLab.debug_log("ActionController halted_callback instrumentation failed: #{e.message}")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ============================================
|
|
277
|
+
# Unpermitted Parameters (Strong Parameters)
|
|
278
|
+
# ============================================
|
|
279
|
+
def install_unpermitted_parameters_subscriber!
|
|
280
|
+
ActiveSupport::Notifications.subscribe('unpermitted_parameters.action_controller') do |*args|
|
|
281
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
282
|
+
handle_unpermitted_parameters(event)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def handle_unpermitted_parameters(event)
|
|
287
|
+
payload = event.payload
|
|
288
|
+
keys = payload[:keys] || []
|
|
289
|
+
context = payload[:context] || {}
|
|
290
|
+
|
|
291
|
+
return if keys.empty?
|
|
292
|
+
|
|
293
|
+
# Add breadcrumb
|
|
294
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
295
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
296
|
+
"Unpermitted parameters: #{keys.join(', ')}",
|
|
297
|
+
category: 'security.params',
|
|
298
|
+
level: :warning,
|
|
299
|
+
data: {
|
|
300
|
+
unpermitted_keys: keys,
|
|
301
|
+
controller: context[:controller],
|
|
302
|
+
action: context[:action]
|
|
303
|
+
}.compact
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Log to Recall - this is a security-relevant event
|
|
308
|
+
if BrainzLab.configuration.recall_effectively_enabled?
|
|
309
|
+
BrainzLab::Recall.warn(
|
|
310
|
+
"Unpermitted parameters rejected",
|
|
311
|
+
unpermitted_keys: keys,
|
|
312
|
+
controller: context[:controller],
|
|
313
|
+
action: context[:action]
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
rescue StandardError => e
|
|
317
|
+
BrainzLab.debug_log("ActionController unpermitted_parameters instrumentation failed: #{e.message}")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# ============================================
|
|
321
|
+
# Send File
|
|
322
|
+
# ============================================
|
|
323
|
+
def install_send_file_subscriber!
|
|
324
|
+
ActiveSupport::Notifications.subscribe('send_file.action_controller') do |*args|
|
|
325
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
326
|
+
handle_send_file(event)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def handle_send_file(event)
|
|
331
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
332
|
+
|
|
333
|
+
payload = event.payload
|
|
334
|
+
path = payload[:path]
|
|
335
|
+
|
|
336
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
337
|
+
"Sending file: #{File.basename(path.to_s)}",
|
|
338
|
+
category: 'http.file',
|
|
339
|
+
level: :info,
|
|
340
|
+
data: {
|
|
341
|
+
filename: File.basename(path.to_s)
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
rescue StandardError => e
|
|
345
|
+
BrainzLab.debug_log("ActionController send_file instrumentation failed: #{e.message}")
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# ============================================
|
|
349
|
+
# Send Data
|
|
350
|
+
# ============================================
|
|
351
|
+
def install_send_data_subscriber!
|
|
352
|
+
ActiveSupport::Notifications.subscribe('send_data.action_controller') do |*args|
|
|
353
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
354
|
+
handle_send_data(event)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def handle_send_data(event)
|
|
359
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
360
|
+
|
|
361
|
+
payload = event.payload
|
|
362
|
+
filename = payload[:filename]
|
|
363
|
+
|
|
364
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
365
|
+
"Sending data#{filename ? ": #{filename}" : ''}",
|
|
366
|
+
category: 'http.data',
|
|
367
|
+
level: :info,
|
|
368
|
+
data: {
|
|
369
|
+
filename: filename
|
|
370
|
+
}.compact
|
|
371
|
+
)
|
|
372
|
+
rescue StandardError => e
|
|
373
|
+
BrainzLab.debug_log("ActionController send_data instrumentation failed: #{e.message}")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# ============================================
|
|
377
|
+
# Send Stream (streaming responses)
|
|
378
|
+
# ============================================
|
|
379
|
+
def install_send_stream_subscriber!
|
|
380
|
+
ActiveSupport::Notifications.subscribe('send_stream.action_controller') do |*args|
|
|
381
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
382
|
+
handle_send_stream(event)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def handle_send_stream(event)
|
|
387
|
+
return unless BrainzLab.configuration.reflex_effectively_enabled?
|
|
388
|
+
|
|
389
|
+
payload = event.payload
|
|
390
|
+
filename = payload[:filename]
|
|
391
|
+
type = payload[:type]
|
|
392
|
+
|
|
393
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
394
|
+
"Streaming#{filename ? ": #{filename}" : ''}",
|
|
395
|
+
category: 'http.stream',
|
|
396
|
+
level: :info,
|
|
397
|
+
data: {
|
|
398
|
+
filename: filename,
|
|
399
|
+
type: type
|
|
400
|
+
}.compact
|
|
401
|
+
)
|
|
402
|
+
rescue StandardError => e
|
|
403
|
+
BrainzLab.debug_log("ActionController send_stream instrumentation failed: #{e.message}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# ============================================
|
|
407
|
+
# Fragment Caching: Write
|
|
408
|
+
# ============================================
|
|
409
|
+
def install_write_fragment_subscriber!
|
|
410
|
+
ActiveSupport::Notifications.subscribe('write_fragment.action_controller') do |*args|
|
|
411
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
412
|
+
handle_write_fragment(event)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def handle_write_fragment(event)
|
|
417
|
+
payload = event.payload
|
|
418
|
+
duration = event.duration.round(2)
|
|
419
|
+
|
|
420
|
+
key = payload[:key]
|
|
421
|
+
|
|
422
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
423
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
424
|
+
"Fragment cache write: #{truncate_cache_key(key)} (#{duration}ms)",
|
|
425
|
+
category: 'cache.fragment.write',
|
|
426
|
+
level: :info,
|
|
427
|
+
data: {
|
|
428
|
+
key: truncate_cache_key(key),
|
|
429
|
+
duration_ms: duration
|
|
430
|
+
}.compact
|
|
431
|
+
)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Add Pulse span
|
|
435
|
+
record_fragment_cache_span(event, 'write', key, duration)
|
|
436
|
+
rescue StandardError => e
|
|
437
|
+
BrainzLab.debug_log("ActionController write_fragment instrumentation failed: #{e.message}")
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# ============================================
|
|
441
|
+
# Fragment Caching: Read
|
|
442
|
+
# ============================================
|
|
443
|
+
def install_read_fragment_subscriber!
|
|
444
|
+
ActiveSupport::Notifications.subscribe('read_fragment.action_controller') do |*args|
|
|
445
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
446
|
+
handle_read_fragment(event)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def handle_read_fragment(event)
|
|
451
|
+
payload = event.payload
|
|
452
|
+
duration = event.duration.round(2)
|
|
453
|
+
|
|
454
|
+
key = payload[:key]
|
|
455
|
+
hit = payload[:hit]
|
|
456
|
+
|
|
457
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
458
|
+
status = hit ? 'hit' : 'miss'
|
|
459
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
460
|
+
"Fragment cache #{status}: #{truncate_cache_key(key)} (#{duration}ms)",
|
|
461
|
+
category: 'cache.fragment.read',
|
|
462
|
+
level: :info,
|
|
463
|
+
data: {
|
|
464
|
+
key: truncate_cache_key(key),
|
|
465
|
+
hit: hit,
|
|
466
|
+
duration_ms: duration
|
|
467
|
+
}.compact
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Add Pulse span
|
|
472
|
+
record_fragment_cache_span(event, 'read', key, duration, hit: hit)
|
|
473
|
+
rescue StandardError => e
|
|
474
|
+
BrainzLab.debug_log("ActionController read_fragment instrumentation failed: #{e.message}")
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# ============================================
|
|
478
|
+
# Fragment Caching: Expire
|
|
479
|
+
# ============================================
|
|
480
|
+
def install_expire_fragment_subscriber!
|
|
481
|
+
ActiveSupport::Notifications.subscribe('expire_fragment.action_controller') do |*args|
|
|
482
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
483
|
+
handle_expire_fragment(event)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def handle_expire_fragment(event)
|
|
488
|
+
payload = event.payload
|
|
489
|
+
duration = event.duration.round(2)
|
|
490
|
+
|
|
491
|
+
key = payload[:key]
|
|
492
|
+
|
|
493
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
494
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
495
|
+
"Fragment cache expire: #{truncate_cache_key(key)} (#{duration}ms)",
|
|
496
|
+
category: 'cache.fragment.expire',
|
|
497
|
+
level: :info,
|
|
498
|
+
data: {
|
|
499
|
+
key: truncate_cache_key(key),
|
|
500
|
+
duration_ms: duration
|
|
501
|
+
}.compact
|
|
502
|
+
)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Add Pulse span
|
|
506
|
+
record_fragment_cache_span(event, 'expire', key, duration)
|
|
507
|
+
rescue StandardError => e
|
|
508
|
+
BrainzLab.debug_log("ActionController expire_fragment instrumentation failed: #{e.message}")
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# ============================================
|
|
512
|
+
# Fragment Caching: Exist?
|
|
513
|
+
# ============================================
|
|
514
|
+
def install_exist_fragment_subscriber!
|
|
515
|
+
ActiveSupport::Notifications.subscribe('exist_fragment?.action_controller') do |*args|
|
|
516
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
517
|
+
handle_exist_fragment(event)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def handle_exist_fragment(event)
|
|
522
|
+
payload = event.payload
|
|
523
|
+
duration = event.duration.round(2)
|
|
524
|
+
|
|
525
|
+
key = payload[:key]
|
|
526
|
+
exist = payload[:exist]
|
|
527
|
+
|
|
528
|
+
# Only track slow checks or misses
|
|
529
|
+
return if duration < 1 && exist
|
|
530
|
+
|
|
531
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
532
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
533
|
+
"Fragment cache exist?: #{truncate_cache_key(key)} -> #{exist} (#{duration}ms)",
|
|
534
|
+
category: 'cache.fragment.exist',
|
|
535
|
+
level: :info,
|
|
536
|
+
data: {
|
|
537
|
+
key: truncate_cache_key(key),
|
|
538
|
+
exist: exist,
|
|
539
|
+
duration_ms: duration
|
|
540
|
+
}.compact
|
|
541
|
+
)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Add Pulse span
|
|
545
|
+
record_fragment_cache_span(event, 'exist', key, duration, exist: exist)
|
|
546
|
+
rescue StandardError => e
|
|
547
|
+
BrainzLab.debug_log("ActionController exist_fragment instrumentation failed: #{e.message}")
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def record_fragment_cache_span(event, operation, key, duration, **extra)
|
|
551
|
+
return unless BrainzLab.configuration.pulse_effectively_enabled?
|
|
552
|
+
|
|
553
|
+
tracer = BrainzLab::Pulse.tracer
|
|
554
|
+
return unless tracer.current_trace
|
|
555
|
+
|
|
556
|
+
span_data = {
|
|
557
|
+
span_id: SecureRandom.uuid,
|
|
558
|
+
name: "cache.fragment.#{operation}",
|
|
559
|
+
kind: 'cache',
|
|
560
|
+
started_at: event.time,
|
|
561
|
+
ended_at: event.end,
|
|
562
|
+
duration_ms: duration,
|
|
563
|
+
error: false,
|
|
564
|
+
data: {
|
|
565
|
+
'cache.operation' => operation,
|
|
566
|
+
'cache.key' => truncate_cache_key(key)
|
|
567
|
+
}.merge(extra.transform_keys { |k| "cache.#{k}" }).compact
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
tracer.current_spans << span_data
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def truncate_cache_key(key, max_length = 100)
|
|
574
|
+
return 'unknown' unless key
|
|
575
|
+
|
|
576
|
+
key_str = key.to_s
|
|
577
|
+
if key_str.length > max_length
|
|
578
|
+
"#{key_str[0, max_length - 3]}..."
|
|
579
|
+
else
|
|
580
|
+
key_str
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# ============================================
|
|
585
|
+
# Rate Limiting (Rails 7.1+)
|
|
586
|
+
# ============================================
|
|
587
|
+
def install_rate_limit_subscriber!
|
|
588
|
+
ActiveSupport::Notifications.subscribe('rate_limit.action_controller') do |*args|
|
|
589
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
590
|
+
handle_rate_limit(event)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def handle_rate_limit(event)
|
|
595
|
+
payload = event.payload
|
|
596
|
+
|
|
597
|
+
# Add breadcrumb
|
|
598
|
+
if BrainzLab.configuration.reflex_effectively_enabled?
|
|
599
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
600
|
+
"Rate limit triggered",
|
|
601
|
+
category: 'security.rate_limit',
|
|
602
|
+
level: :warning,
|
|
603
|
+
data: {
|
|
604
|
+
request: payload[:request]&.path
|
|
605
|
+
}.compact
|
|
606
|
+
)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Log to Recall - rate limiting is security-relevant
|
|
610
|
+
if BrainzLab.configuration.recall_effectively_enabled?
|
|
611
|
+
BrainzLab::Recall.warn(
|
|
612
|
+
"Rate limit triggered",
|
|
613
|
+
path: payload[:request]&.path,
|
|
614
|
+
ip: payload[:request]&.remote_ip
|
|
615
|
+
)
|
|
616
|
+
end
|
|
617
|
+
rescue StandardError => e
|
|
618
|
+
BrainzLab.debug_log("ActionController rate_limit instrumentation failed: #{e.message}")
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# ============================================
|
|
622
|
+
# Helper Methods
|
|
623
|
+
# ============================================
|
|
624
|
+
def excluded_path?(path)
|
|
625
|
+
excluded_paths = BrainzLab.configuration.pulse_excluded_paths || []
|
|
626
|
+
excluded_paths.any? { |excluded| path.to_s.start_with?(excluded) }
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def truncate_path(path, max_length = 200)
|
|
630
|
+
return nil unless path
|
|
631
|
+
|
|
632
|
+
path_str = path.to_s
|
|
633
|
+
if path_str.length > max_length
|
|
634
|
+
"#{path_str[0, max_length - 3]}..."
|
|
635
|
+
else
|
|
636
|
+
path_str
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def rails_71_or_higher?
|
|
641
|
+
return false unless defined?(::Rails::VERSION)
|
|
642
|
+
|
|
643
|
+
::Rails::VERSION::MAJOR > 7 ||
|
|
644
|
+
(::Rails::VERSION::MAJOR == 7 && ::Rails::VERSION::MINOR >= 1)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
end
|