brainzlab 0.1.1 → 0.1.2
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/README.md +8 -0
- data/lib/brainzlab/beacon/client.rb +209 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +341 -3
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +141 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +227 -0
- data/lib/brainzlab/dendrite/client.rb +232 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
- data/lib/brainzlab/devtools/assets/devtools.js +322 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +70 -0
- data/lib/brainzlab/flux/provisioner.rb +57 -0
- data/lib/brainzlab/flux.rb +174 -0
- data/lib/brainzlab/instrumentation/active_record.rb +18 -1
- data/lib/brainzlab/instrumentation/aws.rb +179 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/resque.rb +115 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
- data/lib/brainzlab/instrumentation/stripe.rb +164 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
- data/lib/brainzlab/instrumentation.rb +72 -0
- data/lib/brainzlab/nerve/client.rb +217 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/instrumentation.rb +35 -2
- data/lib/brainzlab/pulse/propagation.rb +1 -1
- data/lib/brainzlab/pulse/tracer.rb +1 -1
- data/lib/brainzlab/pulse.rb +1 -1
- data/lib/brainzlab/rails/log_subscriber.rb +1 -2
- data/lib/brainzlab/rails/railtie.rb +36 -3
- data/lib/brainzlab/recall/provisioner.rb +17 -0
- data/lib/brainzlab/recall.rb +6 -1
- data/lib/brainzlab/reflex.rb +2 -2
- data/lib/brainzlab/sentinel/client.rb +218 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +62 -0
- data/lib/brainzlab/signal/provisioner.rb +55 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +290 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
- data/lib/brainzlab/utilities/health_check.rb +296 -0
- data/lib/brainzlab/utilities/log_formatter.rb +256 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +198 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +268 -0
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +128 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +157 -0
- data/lib/brainzlab.rb +101 -0
- metadata +60 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module DevTools
|
|
5
|
+
module Middleware
|
|
6
|
+
class ErrorPage
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
@renderer = Renderers::ErrorPageRenderer.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
return @app.call(env) unless should_handle?(env)
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
status, headers, body = @app.call(env)
|
|
17
|
+
|
|
18
|
+
# Check if this is an error response that we should intercept
|
|
19
|
+
if status >= 400 && html_response?(headers) && !json_request?(env)
|
|
20
|
+
# Check if this looks like Rails' default error page
|
|
21
|
+
body_content = collect_body(body)
|
|
22
|
+
if body_content.include?("Action Controller: Exception caught") || body_content.include?("background: #C00")
|
|
23
|
+
# Extract exception info from the page
|
|
24
|
+
exception_info = extract_exception_from_html(body_content)
|
|
25
|
+
if exception_info
|
|
26
|
+
data = collect_debug_data_from_info(env, exception_info)
|
|
27
|
+
return render_error_page_from_info(exception_info, data)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
[status, headers, body]
|
|
33
|
+
rescue Exception => exception
|
|
34
|
+
# Don't intercept if request wants JSON
|
|
35
|
+
return raise_exception(exception) if json_request?(env)
|
|
36
|
+
|
|
37
|
+
# Still capture to Reflex if available
|
|
38
|
+
capture_to_reflex(exception)
|
|
39
|
+
|
|
40
|
+
# Collect debug data and render branded error page
|
|
41
|
+
data = collect_debug_data(env, exception)
|
|
42
|
+
render_error_page(exception, data)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def html_response?(headers)
|
|
47
|
+
# Handle both uppercase and lowercase header names
|
|
48
|
+
content_type = headers["Content-Type"] || headers["content-type"] || ""
|
|
49
|
+
content_type.to_s.downcase.include?("text/html")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_exception_from_html(body)
|
|
53
|
+
# Try to extract exception class and message from Rails error page
|
|
54
|
+
if match = body.match(/<h1>([^<]+)<\/h1>/)
|
|
55
|
+
error_title = match[1]
|
|
56
|
+
# Extract the exception message from the page
|
|
57
|
+
if msg_match = body.match(/<pre[^>]*>([^<]+)<\/pre>/)
|
|
58
|
+
error_message = msg_match[1]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Try to extract backtrace from Rails 8 format
|
|
62
|
+
# Format: <a class="trace-frames ...">path/to/file.rb:123:in 'method'</a>
|
|
63
|
+
backtrace = []
|
|
64
|
+
body.scan(/<a[^>]*class="trace-frames[^"]*"[^>]*>\s*([^<]+)\s*<\/a>/m) do |trace_match|
|
|
65
|
+
line = trace_match[0].strip
|
|
66
|
+
# Decode HTML entities
|
|
67
|
+
line = line.gsub("'", "'").gsub(""", '"').gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
68
|
+
backtrace << line unless line.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
class_name: decode_html_entities(error_title.strip),
|
|
73
|
+
message: decode_html_entities(error_message&.strip || error_title.strip),
|
|
74
|
+
backtrace: backtrace
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def decode_html_entities(str)
|
|
80
|
+
return str unless str
|
|
81
|
+
str.gsub("'", "'")
|
|
82
|
+
.gsub(""", '"')
|
|
83
|
+
.gsub("&", "&")
|
|
84
|
+
.gsub("<", "<")
|
|
85
|
+
.gsub(">", ">")
|
|
86
|
+
.gsub(" ", " ")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def collect_debug_data_from_info(env, info)
|
|
90
|
+
context = defined?(BrainzLab::Context) ? BrainzLab::Context.current : nil
|
|
91
|
+
collector_data = Data::Collector.get_request_data
|
|
92
|
+
|
|
93
|
+
backtrace = (info[:backtrace] || []).map do |line|
|
|
94
|
+
parsed = parse_backtrace_line(line)
|
|
95
|
+
parsed[:in_app] = in_app_frame?(parsed[:file])
|
|
96
|
+
parsed
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Extract source from the first in-app frame
|
|
100
|
+
source_extract = extract_source_from_backtrace(info[:backtrace] || [])
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
exception: nil,
|
|
104
|
+
exception_class: info[:class_name],
|
|
105
|
+
exception_message: info[:message],
|
|
106
|
+
backtrace: backtrace,
|
|
107
|
+
request: build_request_info(env, context),
|
|
108
|
+
context: build_context_info(context),
|
|
109
|
+
sql_queries: collector_data.dig(:database, :queries) || [],
|
|
110
|
+
environment: collect_environment_info,
|
|
111
|
+
source_extract: source_extract
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_error_page_from_info(info, data)
|
|
116
|
+
# Create a simple exception-like object
|
|
117
|
+
exception = StandardError.new(info[:message])
|
|
118
|
+
exception.define_singleton_method(:class) do
|
|
119
|
+
Class.new(StandardError) do
|
|
120
|
+
define_singleton_method(:name) { info[:class_name] }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
data[:exception] = exception
|
|
125
|
+
html = @renderer.render(exception, data)
|
|
126
|
+
|
|
127
|
+
[
|
|
128
|
+
500,
|
|
129
|
+
{
|
|
130
|
+
"Content-Type" => "text/html; charset=utf-8",
|
|
131
|
+
"Content-Length" => html.bytesize.to_s,
|
|
132
|
+
"X-Content-Type-Options" => "nosniff"
|
|
133
|
+
},
|
|
134
|
+
[html]
|
|
135
|
+
]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def collect_body(body)
|
|
141
|
+
full_body = +""
|
|
142
|
+
body.each { |part| full_body << part }
|
|
143
|
+
body.close if body.respond_to?(:close)
|
|
144
|
+
full_body
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def should_handle?(env)
|
|
148
|
+
return false unless DevTools.error_page_enabled?
|
|
149
|
+
return false unless DevTools.allowed_environment?
|
|
150
|
+
return false unless DevTools.allowed_ip?(extract_ip(env))
|
|
151
|
+
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def extract_ip(env)
|
|
156
|
+
forwarded = env["HTTP_X_FORWARDED_FOR"]
|
|
157
|
+
return forwarded.split(",").first.strip if forwarded
|
|
158
|
+
|
|
159
|
+
env["REMOTE_ADDR"]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def json_request?(env)
|
|
163
|
+
accept = env["HTTP_ACCEPT"] || ""
|
|
164
|
+
content_type = env["CONTENT_TYPE"] || ""
|
|
165
|
+
|
|
166
|
+
accept.include?("application/json") ||
|
|
167
|
+
content_type.include?("application/json") ||
|
|
168
|
+
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def capture_to_reflex(exception)
|
|
172
|
+
return unless defined?(BrainzLab::Reflex)
|
|
173
|
+
|
|
174
|
+
BrainzLab::Reflex.capture(exception)
|
|
175
|
+
rescue StandardError
|
|
176
|
+
# Ignore errors in error capturing
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def raise_exception(exception)
|
|
180
|
+
raise exception
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def collect_debug_data(env, exception)
|
|
184
|
+
context = defined?(BrainzLab::Context) ? BrainzLab::Context.current : nil
|
|
185
|
+
collector_data = Data::Collector.get_request_data
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
exception: exception,
|
|
189
|
+
backtrace: format_backtrace(exception),
|
|
190
|
+
request: build_request_info(env, context),
|
|
191
|
+
context: build_context_info(context),
|
|
192
|
+
sql_queries: collector_data.dig(:database, :queries) || [],
|
|
193
|
+
environment: collect_environment_info,
|
|
194
|
+
source_extract: extract_source_lines(exception)
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_request_info(env, context)
|
|
199
|
+
request = defined?(ActionDispatch::Request) ? ActionDispatch::Request.new(env) : nil
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
method: request&.request_method || env["REQUEST_METHOD"],
|
|
203
|
+
path: request&.path || env["PATH_INFO"],
|
|
204
|
+
url: request&.url || env["REQUEST_URI"],
|
|
205
|
+
params: scrub_params(context&.request_params || extract_params(env)),
|
|
206
|
+
headers: extract_headers(env),
|
|
207
|
+
session: {}
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def build_context_info(context)
|
|
212
|
+
{
|
|
213
|
+
controller: context&.controller,
|
|
214
|
+
action: context&.action,
|
|
215
|
+
request_id: context&.request_id,
|
|
216
|
+
user: context&.user
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def extract_params(env)
|
|
221
|
+
return {} unless defined?(Rack::Request)
|
|
222
|
+
|
|
223
|
+
Rack::Request.new(env).params
|
|
224
|
+
rescue StandardError
|
|
225
|
+
{}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def extract_headers(env)
|
|
229
|
+
headers = {}
|
|
230
|
+
env.each do |key, value|
|
|
231
|
+
if key.start_with?("HTTP_")
|
|
232
|
+
header_name = key.sub("HTTP_", "").split("_").map(&:capitalize).join("-")
|
|
233
|
+
headers[header_name] = value
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
headers
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def scrub_params(params)
|
|
240
|
+
return {} unless params.is_a?(Hash)
|
|
241
|
+
|
|
242
|
+
scrub_fields = BrainzLab.configuration.scrub_fields.map(&:to_s)
|
|
243
|
+
|
|
244
|
+
params.transform_values.with_index do |(key, value), _|
|
|
245
|
+
if scrub_fields.include?(key.to_s.downcase)
|
|
246
|
+
"[FILTERED]"
|
|
247
|
+
elsif value.is_a?(Hash)
|
|
248
|
+
scrub_params(value)
|
|
249
|
+
else
|
|
250
|
+
value
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
rescue StandardError
|
|
254
|
+
params
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def format_backtrace(exception)
|
|
258
|
+
(exception.backtrace || []).first(50).map do |line|
|
|
259
|
+
parsed = parse_backtrace_line(line)
|
|
260
|
+
parsed[:in_app] = in_app_frame?(parsed[:file])
|
|
261
|
+
parsed
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def parse_backtrace_line(line)
|
|
266
|
+
match = line.match(/\A(.+):(\d+)(?::in `(.+)')?/)
|
|
267
|
+
return { file: line, line: 0, function: nil, raw: line } unless match
|
|
268
|
+
|
|
269
|
+
{
|
|
270
|
+
file: match[1],
|
|
271
|
+
line: match[2].to_i,
|
|
272
|
+
function: match[3],
|
|
273
|
+
raw: line
|
|
274
|
+
}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def in_app_frame?(file)
|
|
278
|
+
return false unless file
|
|
279
|
+
|
|
280
|
+
file.include?("/app/") && !file.include?("/vendor/") && !file.include?("/gems/")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def extract_source_from_backtrace(backtrace_lines)
|
|
284
|
+
return nil if backtrace_lines.empty?
|
|
285
|
+
|
|
286
|
+
# Find the first in-app frame
|
|
287
|
+
target_line = backtrace_lines.find { |line| in_app_frame?(line.split(":").first) }
|
|
288
|
+
target_line ||= backtrace_lines.first
|
|
289
|
+
|
|
290
|
+
match = target_line.match(/\A(.+):(\d+)/)
|
|
291
|
+
return nil unless match
|
|
292
|
+
|
|
293
|
+
file = match[1]
|
|
294
|
+
line_number = match[2].to_i
|
|
295
|
+
return nil unless File.exist?(file)
|
|
296
|
+
|
|
297
|
+
lines = File.readlines(file)
|
|
298
|
+
start_line = [line_number - 6, 0].max
|
|
299
|
+
end_line = [line_number + 4, lines.length - 1].min
|
|
300
|
+
|
|
301
|
+
{
|
|
302
|
+
file: file,
|
|
303
|
+
line_number: line_number,
|
|
304
|
+
lines: lines[start_line..end_line].map.with_index do |content, idx|
|
|
305
|
+
{
|
|
306
|
+
number: start_line + idx + 1,
|
|
307
|
+
content: content.chomp,
|
|
308
|
+
highlight: (start_line + idx + 1) == line_number
|
|
309
|
+
}
|
|
310
|
+
end
|
|
311
|
+
}
|
|
312
|
+
rescue StandardError
|
|
313
|
+
nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def extract_source_lines(exception)
|
|
317
|
+
return nil unless exception.backtrace&.any?
|
|
318
|
+
|
|
319
|
+
# Find the first in-app frame (application code, not gems/framework)
|
|
320
|
+
target_line = exception.backtrace.find { |line| in_app_frame?(line.split(":").first) }
|
|
321
|
+
# Fall back to first frame if no in-app frame found
|
|
322
|
+
target_line ||= exception.backtrace.first
|
|
323
|
+
|
|
324
|
+
match = target_line.match(/\A(.+):(\d+)/)
|
|
325
|
+
return nil unless match
|
|
326
|
+
|
|
327
|
+
file = match[1]
|
|
328
|
+
line_number = match[2].to_i
|
|
329
|
+
return nil unless File.exist?(file)
|
|
330
|
+
|
|
331
|
+
lines = File.readlines(file)
|
|
332
|
+
start_line = [line_number - 6, 0].max
|
|
333
|
+
end_line = [line_number + 4, lines.length - 1].min
|
|
334
|
+
|
|
335
|
+
{
|
|
336
|
+
file: file,
|
|
337
|
+
line_number: line_number,
|
|
338
|
+
lines: lines[start_line..end_line].map.with_index do |content, idx|
|
|
339
|
+
{
|
|
340
|
+
number: start_line + idx + 1,
|
|
341
|
+
content: content.chomp,
|
|
342
|
+
highlight: (start_line + idx + 1) == line_number
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
}
|
|
346
|
+
rescue StandardError
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def collect_environment_info
|
|
351
|
+
{
|
|
352
|
+
rails_version: defined?(Rails::VERSION::STRING) ? Rails::VERSION::STRING : "N/A",
|
|
353
|
+
ruby_version: RUBY_VERSION,
|
|
354
|
+
env: BrainzLab.configuration.environment,
|
|
355
|
+
server: ENV["SERVER_SOFTWARE"] || "Unknown",
|
|
356
|
+
pid: Process.pid
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def render_error_page(exception, data)
|
|
361
|
+
html = @renderer.render(exception, data)
|
|
362
|
+
|
|
363
|
+
[
|
|
364
|
+
500,
|
|
365
|
+
{
|
|
366
|
+
"Content-Type" => "text/html; charset=utf-8",
|
|
367
|
+
"Content-Length" => html.bytesize.to_s,
|
|
368
|
+
"X-Content-Type-Options" => "nosniff"
|
|
369
|
+
},
|
|
370
|
+
[html]
|
|
371
|
+
]
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module DevTools
|
|
9
|
+
module Renderers
|
|
10
|
+
class DebugPanelRenderer
|
|
11
|
+
def initialize
|
|
12
|
+
@template_path = File.join(DevTools::ASSETS_PATH, "templates", "debug_panel.html.erb")
|
|
13
|
+
# Cache compiled ERB template to avoid file I/O on every request
|
|
14
|
+
@cached_erb = nil
|
|
15
|
+
@template_mtime = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(data)
|
|
19
|
+
erb = cached_erb
|
|
20
|
+
|
|
21
|
+
# Make data available to template
|
|
22
|
+
@data = data
|
|
23
|
+
@timing = data[:timing] || {}
|
|
24
|
+
@request = data[:request] || {}
|
|
25
|
+
@controller = data[:controller] || {}
|
|
26
|
+
@response = data[:response] || {}
|
|
27
|
+
@database = data[:database] || {}
|
|
28
|
+
@views = data[:views] || {}
|
|
29
|
+
@logs = data[:logs] || []
|
|
30
|
+
@memory = data[:memory] || {}
|
|
31
|
+
@user = data[:user]
|
|
32
|
+
@breadcrumbs = data[:breadcrumbs] || []
|
|
33
|
+
@expand_by_default = DevTools.expand_by_default?
|
|
34
|
+
@panel_position = DevTools.panel_position
|
|
35
|
+
|
|
36
|
+
erb.result(binding)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def h(text)
|
|
42
|
+
CGI.escapeHTML(text.to_s)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def asset_url(file)
|
|
46
|
+
"#{DevTools.asset_path}/#{file}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def json_pretty(obj)
|
|
50
|
+
return "" if obj.nil? || (obj.respond_to?(:empty?) && obj.empty?)
|
|
51
|
+
|
|
52
|
+
JSON.pretty_generate(obj)
|
|
53
|
+
rescue StandardError
|
|
54
|
+
obj.inspect
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def truncate(text, length = 80)
|
|
58
|
+
return "" unless text
|
|
59
|
+
|
|
60
|
+
text = text.to_s
|
|
61
|
+
text.length > length ? "#{text[0...length]}..." : text
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def format_duration(ms)
|
|
65
|
+
return "0ms" unless ms
|
|
66
|
+
|
|
67
|
+
if ms >= 1000
|
|
68
|
+
"#{(ms / 1000.0).round(2)}s"
|
|
69
|
+
else
|
|
70
|
+
"#{ms.round(2)}ms"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def duration_class(ms)
|
|
75
|
+
return "" unless ms
|
|
76
|
+
|
|
77
|
+
if ms > 1000
|
|
78
|
+
"very-slow"
|
|
79
|
+
elsif ms > 500
|
|
80
|
+
"slow"
|
|
81
|
+
elsif ms > 200
|
|
82
|
+
"moderate"
|
|
83
|
+
else
|
|
84
|
+
""
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def query_duration_class(ms)
|
|
89
|
+
return "" unless ms
|
|
90
|
+
|
|
91
|
+
if ms > 100
|
|
92
|
+
"very-slow"
|
|
93
|
+
elsif ms > 50
|
|
94
|
+
"slow"
|
|
95
|
+
elsif ms > 10
|
|
96
|
+
"moderate"
|
|
97
|
+
else
|
|
98
|
+
""
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def status_class(status)
|
|
103
|
+
case status
|
|
104
|
+
when 200..299 then "success"
|
|
105
|
+
when 300..399 then "redirect"
|
|
106
|
+
when 400..499 then "client-error"
|
|
107
|
+
when 500..599 then "server-error"
|
|
108
|
+
else ""
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_level_class(level)
|
|
113
|
+
case level.to_s.downcase
|
|
114
|
+
when "error", "fatal" then "error"
|
|
115
|
+
when "warn", "warning" then "warning"
|
|
116
|
+
when "info" then "info"
|
|
117
|
+
when "debug" then "debug"
|
|
118
|
+
else ""
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_timestamp(time)
|
|
123
|
+
return "" unless time
|
|
124
|
+
|
|
125
|
+
time.strftime("%H:%M:%S.%L")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def memory_class(delta_mb)
|
|
129
|
+
return "" unless delta_mb
|
|
130
|
+
|
|
131
|
+
if delta_mb > 50
|
|
132
|
+
"high"
|
|
133
|
+
elsif delta_mb > 20
|
|
134
|
+
"moderate"
|
|
135
|
+
else
|
|
136
|
+
""
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Cache compiled ERB template, reloading only if file changed (dev mode)
|
|
141
|
+
def cached_erb
|
|
142
|
+
current_mtime = File.mtime(@template_path) rescue nil
|
|
143
|
+
|
|
144
|
+
if @cached_erb.nil? || (current_mtime && current_mtime != @template_mtime)
|
|
145
|
+
template = File.read(@template_path)
|
|
146
|
+
@cached_erb = ERB.new(template, trim_mode: "-")
|
|
147
|
+
@template_mtime = current_mtime
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
@cached_erb
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "cgi"
|
|
5
|
+
|
|
6
|
+
module BrainzLab
|
|
7
|
+
module DevTools
|
|
8
|
+
module Renderers
|
|
9
|
+
class ErrorPageRenderer
|
|
10
|
+
def initialize
|
|
11
|
+
@template_path = File.join(DevTools::ASSETS_PATH, "templates", "error_page.html.erb")
|
|
12
|
+
@cached_erb = nil
|
|
13
|
+
@template_mtime = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render(exception, data)
|
|
17
|
+
erb = cached_erb
|
|
18
|
+
|
|
19
|
+
# Make data available to template
|
|
20
|
+
@exception = exception
|
|
21
|
+
@data = data
|
|
22
|
+
@backtrace = data[:backtrace] || []
|
|
23
|
+
@request = data[:request] || {}
|
|
24
|
+
@context = data[:context] || {}
|
|
25
|
+
@sql_queries = data[:sql_queries] || []
|
|
26
|
+
@environment = data[:environment] || {}
|
|
27
|
+
@source_extract = data[:source_extract]
|
|
28
|
+
|
|
29
|
+
erb.result(binding)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def h(text)
|
|
35
|
+
CGI.escapeHTML(text.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def asset_url(file)
|
|
39
|
+
"#{DevTools.asset_path}/#{file}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def format_params(params, indent = 0)
|
|
43
|
+
return "" if params.nil? || params.empty?
|
|
44
|
+
|
|
45
|
+
lines = []
|
|
46
|
+
prefix = " " * indent
|
|
47
|
+
|
|
48
|
+
params.each do |key, value|
|
|
49
|
+
if value.is_a?(Hash)
|
|
50
|
+
lines << "#{prefix}#{h(key)}:"
|
|
51
|
+
lines << format_params(value, indent + 1)
|
|
52
|
+
elsif value.is_a?(Array)
|
|
53
|
+
lines << "#{prefix}#{h(key)}: #{h(value.inspect)}"
|
|
54
|
+
else
|
|
55
|
+
lines << "#{prefix}#{h(key)}: #{h(value)}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
lines.join("\n")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def truncate(text, length = 100)
|
|
63
|
+
return "" unless text
|
|
64
|
+
|
|
65
|
+
text.length > length ? "#{text[0...length]}..." : text
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def time_ago(time)
|
|
69
|
+
return "unknown" unless time
|
|
70
|
+
|
|
71
|
+
seconds = Time.now.utc - time
|
|
72
|
+
case seconds
|
|
73
|
+
when 0..59 then "#{seconds.to_i}s ago"
|
|
74
|
+
when 60..3599 then "#{(seconds / 60).to_i}m ago"
|
|
75
|
+
else "#{(seconds / 3600).to_i}h ago"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Cache compiled ERB template, reloading only if file changed
|
|
80
|
+
def cached_erb
|
|
81
|
+
current_mtime = File.mtime(@template_path) rescue nil
|
|
82
|
+
|
|
83
|
+
if @cached_erb.nil? || (current_mtime && current_mtime != @template_mtime)
|
|
84
|
+
template = File.read(@template_path)
|
|
85
|
+
@cached_erb = ERB.new(template, trim_mode: "-")
|
|
86
|
+
@template_mtime = current_mtime
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@cached_erb
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "devtools/data/collector"
|
|
4
|
+
require_relative "devtools/middleware/asset_server"
|
|
5
|
+
require_relative "devtools/middleware/database_handler"
|
|
6
|
+
require_relative "devtools/middleware/error_page"
|
|
7
|
+
require_relative "devtools/middleware/debug_panel"
|
|
8
|
+
require_relative "devtools/renderers/error_page_renderer"
|
|
9
|
+
require_relative "devtools/renderers/debug_panel_renderer"
|
|
10
|
+
|
|
11
|
+
module BrainzLab
|
|
12
|
+
module DevTools
|
|
13
|
+
ASSETS_PATH = File.expand_path("devtools/assets", __dir__)
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def enabled?
|
|
17
|
+
BrainzLab.configuration.devtools_enabled
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error_page_enabled?
|
|
21
|
+
enabled? && BrainzLab.configuration.devtools_error_page_enabled
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def debug_panel_enabled?
|
|
25
|
+
enabled? && BrainzLab.configuration.devtools_debug_panel_enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def allowed_environment?
|
|
29
|
+
allowed = BrainzLab.configuration.devtools_allowed_environments
|
|
30
|
+
current = BrainzLab.configuration.environment
|
|
31
|
+
allowed.include?(current)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def allowed_ip?(request_ip)
|
|
35
|
+
# Skip IP checking in development - environment check is enough
|
|
36
|
+
return true if BrainzLab.configuration.environment == "development"
|
|
37
|
+
|
|
38
|
+
return true if BrainzLab.configuration.devtools_allowed_ips.empty?
|
|
39
|
+
|
|
40
|
+
allowed_ips = BrainzLab.configuration.devtools_allowed_ips
|
|
41
|
+
return true if allowed_ips.include?(request_ip)
|
|
42
|
+
|
|
43
|
+
# Check CIDR ranges
|
|
44
|
+
allowed_ips.any? do |ip|
|
|
45
|
+
if ip.include?("/")
|
|
46
|
+
ip_in_cidr?(request_ip, ip)
|
|
47
|
+
else
|
|
48
|
+
ip == request_ip
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def asset_path
|
|
54
|
+
BrainzLab.configuration.devtools_asset_path
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def panel_position
|
|
58
|
+
BrainzLab.configuration.devtools_panel_position
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def expand_by_default?
|
|
62
|
+
BrainzLab.configuration.devtools_expand_by_default
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def ip_in_cidr?(ip, cidr)
|
|
68
|
+
require "ipaddr"
|
|
69
|
+
IPAddr.new(cidr).include?(IPAddr.new(ip))
|
|
70
|
+
rescue IPAddr::InvalidAddressError
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|