brainzlab 0.1.0

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +26 -0
  4. data/README.md +311 -0
  5. data/lib/brainzlab/configuration.rb +215 -0
  6. data/lib/brainzlab/context.rb +91 -0
  7. data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
  8. data/lib/brainzlab/instrumentation/active_record.rb +111 -0
  9. data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
  10. data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
  11. data/lib/brainzlab/instrumentation/faraday.rb +182 -0
  12. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  13. data/lib/brainzlab/instrumentation/graphql.rb +251 -0
  14. data/lib/brainzlab/instrumentation/httparty.rb +194 -0
  15. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  16. data/lib/brainzlab/instrumentation/net_http.rb +109 -0
  17. data/lib/brainzlab/instrumentation/redis.rb +331 -0
  18. data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
  19. data/lib/brainzlab/instrumentation.rb +132 -0
  20. data/lib/brainzlab/pulse/client.rb +132 -0
  21. data/lib/brainzlab/pulse/instrumentation.rb +364 -0
  22. data/lib/brainzlab/pulse/propagation.rb +241 -0
  23. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  24. data/lib/brainzlab/pulse/tracer.rb +111 -0
  25. data/lib/brainzlab/pulse.rb +224 -0
  26. data/lib/brainzlab/rails/log_formatter.rb +801 -0
  27. data/lib/brainzlab/rails/log_subscriber.rb +341 -0
  28. data/lib/brainzlab/rails/railtie.rb +590 -0
  29. data/lib/brainzlab/recall/buffer.rb +64 -0
  30. data/lib/brainzlab/recall/client.rb +86 -0
  31. data/lib/brainzlab/recall/logger.rb +118 -0
  32. data/lib/brainzlab/recall/provisioner.rb +113 -0
  33. data/lib/brainzlab/recall.rb +155 -0
  34. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  35. data/lib/brainzlab/reflex/client.rb +85 -0
  36. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  37. data/lib/brainzlab/reflex.rb +374 -0
  38. data/lib/brainzlab/version.rb +5 -0
  39. data/lib/brainzlab-sdk.rb +3 -0
  40. data/lib/brainzlab.rb +140 -0
  41. data/lib/generators/brainzlab/install/install_generator.rb +61 -0
  42. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  43. metadata +159 -0
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/log_subscriber"
4
+
5
+ module BrainzLab
6
+ module Rails
7
+ class LogSubscriber < ActiveSupport::LogSubscriber
8
+ INTERNAL_PARAMS = %w[controller action format _method authenticity_token].freeze
9
+
10
+ class << self
11
+ attr_accessor :formatter
12
+ end
13
+
14
+ def start_processing(event)
15
+ return unless formatter
16
+
17
+ request_id = event.payload[:request]&.request_id || Thread.current[:brainzlab_request_id]
18
+ return unless request_id
19
+
20
+ payload = event.payload
21
+ params = payload[:params]&.except(*INTERNAL_PARAMS) || {}
22
+
23
+ formatter.start_request(request_id,
24
+ method: payload[:method],
25
+ path: payload[:path],
26
+ params: filter_params(params),
27
+ controller: payload[:controller],
28
+ action: payload[:action]
29
+ )
30
+ end
31
+
32
+ def process_action(event)
33
+ return unless formatter
34
+
35
+ request_id = event.payload[:request]&.request_id || Thread.current[:brainzlab_request_id]
36
+ return unless request_id
37
+
38
+ payload = event.payload
39
+
40
+ formatter.process_action(request_id,
41
+ controller: payload[:controller],
42
+ action: payload[:action],
43
+ status: payload[:status],
44
+ duration: event.duration,
45
+ view_runtime: payload[:view_runtime],
46
+ db_runtime: payload[:db_runtime]
47
+ )
48
+
49
+ # Handle exception if present
50
+ if payload[:exception_object]
51
+ formatter.error(request_id, payload[:exception_object])
52
+ end
53
+
54
+ # Output the formatted log
55
+ output = formatter.end_request(request_id)
56
+ log_output(output) if output
57
+ end
58
+
59
+ def halted_callback(event)
60
+ # Request was halted by a before_action
61
+ end
62
+
63
+ def redirect_to(event)
64
+ # Redirect happened
65
+ end
66
+
67
+ private
68
+
69
+ def formatter
70
+ self.class.formatter
71
+ end
72
+
73
+ def filter_params(params)
74
+ return {} unless params.is_a?(Hash)
75
+
76
+ filter_keys = BrainzLab.configuration.scrub_fields.map(&:to_s)
77
+ deep_filter(params, filter_keys)
78
+ end
79
+
80
+ def deep_filter(obj, filter_keys)
81
+ case obj
82
+ when Hash
83
+ obj.each_with_object({}) do |(k, v), h|
84
+ if filter_keys.include?(k.to_s.downcase)
85
+ h[k] = "[FILTERED]"
86
+ else
87
+ h[k] = deep_filter(v, filter_keys)
88
+ end
89
+ end
90
+ when Array
91
+ obj.map { |v| deep_filter(v, filter_keys) }
92
+ else
93
+ obj
94
+ end
95
+ end
96
+
97
+ def log_output(output)
98
+ # Output directly to stdout for clean formatting
99
+ # This bypasses the Rails logger which would add timestamps/prefixes
100
+ $stdout.write(output)
101
+ end
102
+ end
103
+
104
+ # SQL query subscriber to track query details
105
+ class SqlLogSubscriber < ActiveSupport::LogSubscriber
106
+ IGNORED_PAYLOADS = %w[SCHEMA].freeze
107
+
108
+ def sql(event)
109
+ return unless LogSubscriber.formatter
110
+
111
+ payload = event.payload
112
+ return if IGNORED_PAYLOADS.include?(payload[:name])
113
+
114
+ request_id = Thread.current[:brainzlab_request_id]
115
+ return unless request_id
116
+
117
+ # Extract source location from the backtrace
118
+ source = extract_source_location(caller)
119
+
120
+ # Normalize SQL for pattern detection (remove specific values)
121
+ sql_pattern = normalize_sql(payload[:sql])
122
+
123
+ LogSubscriber.formatter.sql_query(request_id,
124
+ name: payload[:name],
125
+ duration: event.duration,
126
+ sql: payload[:sql],
127
+ sql_pattern: sql_pattern,
128
+ cached: payload[:cached] || payload[:name] == "CACHE",
129
+ source: source
130
+ )
131
+ end
132
+
133
+ private
134
+
135
+ def extract_source_location(backtrace)
136
+ # Find the first line that's in app/ directory
137
+ backtrace.each do |line|
138
+ if line.include?("/app/") && !line.include?("/brainzlab")
139
+ # Extract just the relevant part: app/models/user.rb:42
140
+ match = line.match(%r{(app/[^:]+:\d+)})
141
+ return match[1] if match
142
+ end
143
+ end
144
+ nil
145
+ end
146
+
147
+ def normalize_sql(sql)
148
+ return nil unless sql
149
+
150
+ sql
151
+ .gsub(/\b\d+\b/, "?") # Replace numbers
152
+ .gsub(/'[^']*'/, "?") # Replace strings
153
+ .gsub(/"[^"]*"/, "?") # Replace double-quoted strings
154
+ .gsub(/\$\d+/, "?") # Replace positional params
155
+ .gsub(/\/\*.*?\*\//, "") # Remove comments
156
+ .gsub(/\s+/, " ") # Normalize whitespace
157
+ .strip
158
+ end
159
+ end
160
+
161
+ # ActionCable subscriber for broadcast formatting
162
+ class CableLogSubscriber < ActiveSupport::LogSubscriber
163
+ COLORS = LogFormatter::COLORS
164
+
165
+ def broadcast(event)
166
+ payload = event.payload
167
+ broadcasting = payload[:broadcasting]
168
+ message = payload[:message]
169
+
170
+ # Decode the channel name from the gid
171
+ channel_name = decode_broadcasting(broadcasting)
172
+
173
+ # Format the message summary
174
+ message_summary = format_message(message)
175
+
176
+ output = build_output(channel_name, message_summary, event.duration)
177
+ $stdout.write(output)
178
+ end
179
+
180
+ private
181
+
182
+ def decode_broadcasting(broadcasting)
183
+ return broadcasting unless broadcasting
184
+
185
+ # Extract channel name from gid format: logs:Z2lkOi8vcmVjYWxsL1Byb2plY3QvNDhi...
186
+ if broadcasting.start_with?("logs:")
187
+ "LogsChannel"
188
+ elsif broadcasting.include?(":")
189
+ # Generic channel:id format
190
+ broadcasting.split(":").first.capitalize + "Channel"
191
+ else
192
+ broadcasting
193
+ end
194
+ end
195
+
196
+ def format_message(message)
197
+ return "{}" unless message
198
+
199
+ case message
200
+ when Hash
201
+ format_hash(message)
202
+ when String
203
+ begin
204
+ parsed = JSON.parse(message)
205
+ format_hash(parsed)
206
+ rescue JSON::ParserError
207
+ truncate(message, 80)
208
+ end
209
+ else
210
+ message.to_s[0..80]
211
+ end
212
+ end
213
+
214
+ def format_hash(hash)
215
+ parts = []
216
+
217
+ # Show key fields for log entries
218
+ if hash["level"] || hash[:level]
219
+ level = hash["level"] || hash[:level]
220
+ parts << colorize_level(level)
221
+ end
222
+
223
+ if hash["message"] || hash[:message]
224
+ msg = hash["message"] || hash[:message]
225
+ parts << truncate(msg.to_s, 50)
226
+ end
227
+
228
+ if hash["id"] || hash[:id]
229
+ id = hash["id"] || hash[:id]
230
+ parts << colorize(id.to_s[0..7], :gray)
231
+ end
232
+
233
+ parts.any? ? parts.join(" ") : hash.keys.first(3).join(", ")
234
+ end
235
+
236
+ def build_output(channel, message, duration)
237
+ time = Time.current.strftime("%H:%M:%S")
238
+ duration_str = duration ? "#{duration.round(1)}ms" : ""
239
+
240
+ parts = [
241
+ colorize(time, :gray),
242
+ colorize("⚡", :magenta),
243
+ colorize(channel, :magenta),
244
+ colorize("→", :gray),
245
+ message,
246
+ colorize(duration_str, :gray)
247
+ ]
248
+
249
+ " " + parts.join(" ") + "\n"
250
+ end
251
+
252
+ def truncate(text, length)
253
+ return text if text.nil? || text.length <= length
254
+ "#{text[0..(length - 4)]}..."
255
+ end
256
+
257
+ def colorize(text, color)
258
+ return text unless $stdout.tty?
259
+ return text unless COLORS[color]
260
+ "#{COLORS[color]}#{text}#{COLORS[:reset]}"
261
+ end
262
+
263
+ def colorize_level(level)
264
+ color = case level.to_s.downcase
265
+ when "debug" then :gray
266
+ when "info" then :green
267
+ when "warn", "warning" then :yellow
268
+ when "error" then :red
269
+ when "fatal" then :red
270
+ else :white
271
+ end
272
+ colorize(level.to_s.upcase.ljust(5), color)
273
+ end
274
+ end
275
+
276
+ # View rendering subscriber
277
+ class ViewLogSubscriber < ActiveSupport::LogSubscriber
278
+ def render_template(event)
279
+ return unless LogSubscriber.formatter
280
+
281
+ request_id = Thread.current[:brainzlab_request_id]
282
+ return unless request_id
283
+
284
+ payload = event.payload
285
+ template = template_name(payload[:identifier])
286
+
287
+ LogSubscriber.formatter.render_template(request_id,
288
+ template: template,
289
+ duration: event.duration,
290
+ layout: payload[:layout]
291
+ )
292
+ end
293
+
294
+ def render_partial(event)
295
+ return unless LogSubscriber.formatter
296
+
297
+ request_id = Thread.current[:brainzlab_request_id]
298
+ return unless request_id
299
+
300
+ payload = event.payload
301
+ template = template_name(payload[:identifier])
302
+
303
+ LogSubscriber.formatter.render_partial(request_id,
304
+ template: template,
305
+ duration: event.duration,
306
+ count: payload[:count]
307
+ )
308
+ end
309
+
310
+ def render_layout(event)
311
+ return unless LogSubscriber.formatter
312
+
313
+ request_id = Thread.current[:brainzlab_request_id]
314
+ return unless request_id
315
+
316
+ payload = event.payload
317
+ layout = template_name(payload[:identifier])
318
+
319
+ LogSubscriber.formatter.render_layout(request_id,
320
+ layout: layout,
321
+ duration: event.duration
322
+ )
323
+ end
324
+
325
+ private
326
+
327
+ def template_name(identifier)
328
+ return nil unless identifier
329
+
330
+ # Extract relative path from full identifier
331
+ if identifier.include?("/app/views/")
332
+ identifier.split("/app/views/").last
333
+ elsif identifier.include?("/views/")
334
+ identifier.split("/views/").last
335
+ else
336
+ File.basename(identifier)
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end