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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE +26 -0
- data/README.md +311 -0
- data/lib/brainzlab/configuration.rb +215 -0
- data/lib/brainzlab/context.rb +91 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
- data/lib/brainzlab/instrumentation/active_record.rb +111 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
- data/lib/brainzlab/instrumentation/faraday.rb +182 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +251 -0
- data/lib/brainzlab/instrumentation/httparty.rb +194 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +109 -0
- data/lib/brainzlab/instrumentation/redis.rb +331 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
- data/lib/brainzlab/instrumentation.rb +132 -0
- data/lib/brainzlab/pulse/client.rb +132 -0
- data/lib/brainzlab/pulse/instrumentation.rb +364 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +224 -0
- data/lib/brainzlab/rails/log_formatter.rb +801 -0
- data/lib/brainzlab/rails/log_subscriber.rb +341 -0
- data/lib/brainzlab/rails/railtie.rb +590 -0
- data/lib/brainzlab/recall/buffer.rb +64 -0
- data/lib/brainzlab/recall/client.rb +86 -0
- data/lib/brainzlab/recall/logger.rb +118 -0
- data/lib/brainzlab/recall/provisioner.rb +113 -0
- data/lib/brainzlab/recall.rb +155 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +85 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +374 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +140 -0
- data/lib/generators/brainzlab/install/install_generator.rb +61 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- 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
|