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