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,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