fluyenta-ruby 0.1.14

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,807 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Rails
5
+ class LogFormatter
6
+ ASSET_PATHS = %w[/assets /packs /vite /images /fonts /stylesheets /javascripts].freeze
7
+ ASSET_EXTENSIONS = %w[.css .js .map .png .jpg .jpeg .gif .svg .ico .woff .woff2 .ttf .eot].freeze
8
+ SIMPLE_PATHS = %w[/up /health /healthz /ready /readiness /live /liveness /ping /favicon.ico /apple-touch-icon.png
9
+ /apple-touch-icon-precomposed.png /robots.txt /sitemap.xml].freeze
10
+ IGNORED_PATHS = %w[/apple-touch-icon.png /apple-touch-icon-precomposed.png /favicon.ico].freeze
11
+
12
+ # Thresholds for highlighting
13
+ SLOW_QUERY_MS = 5.0
14
+ N_PLUS_ONE_THRESHOLD = 3
15
+
16
+ # ANSI color codes
17
+ COLORS = {
18
+ reset: "\e[0m",
19
+ bold: "\e[1m",
20
+ dim: "\e[2m",
21
+ red: "\e[31m",
22
+ green: "\e[32m",
23
+ yellow: "\e[33m",
24
+ blue: "\e[34m",
25
+ magenta: "\e[35m",
26
+ cyan: "\e[36m",
27
+ white: "\e[37m",
28
+ gray: "\e[90m"
29
+ }.freeze
30
+
31
+ # Box drawing characters
32
+ BOX = {
33
+ top_left: '┌',
34
+ top_right: '─',
35
+ bottom_left: '└',
36
+ bottom_right: '─',
37
+ vertical: '│',
38
+ horizontal: '─'
39
+ }.freeze
40
+
41
+ attr_reader :config
42
+
43
+ def initialize(config = {})
44
+ @config = default_config.merge(config)
45
+ @request_data = {}
46
+ end
47
+
48
+ def default_config
49
+ {
50
+ enabled: true,
51
+ colors: $stdout.tty?,
52
+ hide_assets: false,
53
+ hide_ignored: true,
54
+ compact_assets: true,
55
+ show_params: true,
56
+ show_sql_count: true,
57
+ show_sql_details: true,
58
+ show_sql_queries: true, # Show actual SQL queries
59
+ show_views: true,
60
+ slow_query_threshold: SLOW_QUERY_MS,
61
+ n_plus_one_threshold: N_PLUS_ONE_THRESHOLD,
62
+ line_width: detect_terminal_width
63
+ }
64
+ end
65
+
66
+ def detect_terminal_width
67
+ # Try to get terminal width, fallback to 120
68
+ width = ENV['COLUMNS']&.to_i
69
+ return width if width&.positive?
70
+
71
+ if $stdout.tty? && IO.respond_to?(:console) && IO.console
72
+ _rows, cols = IO.console.winsize
73
+ return cols if cols.positive?
74
+ end
75
+
76
+ 120 # Default to 120 for wider output
77
+ rescue StandardError
78
+ 120
79
+ end
80
+
81
+ # Called when a request starts
82
+ def start_request(request_id, data = {})
83
+ @request_data[request_id] = {
84
+ started_at: Time.current,
85
+ method: data[:method],
86
+ path: data[:path],
87
+ params: data[:params] || {},
88
+ controller: nil,
89
+ action: nil,
90
+ status: nil,
91
+ duration: nil,
92
+ view_runtime: nil,
93
+ db_runtime: nil,
94
+ sql_queries: [],
95
+ views: [],
96
+ error: nil,
97
+ error_message: nil
98
+ }
99
+ end
100
+
101
+ # Called when controller processes action
102
+ def process_action(request_id, data = {})
103
+ return unless @request_data[request_id]
104
+
105
+ @request_data[request_id].merge!(
106
+ controller: data[:controller],
107
+ action: data[:action],
108
+ status: data[:status],
109
+ duration: data[:duration],
110
+ view_runtime: data[:view_runtime],
111
+ db_runtime: data[:db_runtime]
112
+ )
113
+ end
114
+
115
+ # Called when SQL query is executed
116
+ def sql_query(request_id, name: nil, duration: 0, sql: nil, sql_pattern: nil, cached: false, source: nil)
117
+ return unless @request_data[request_id]
118
+
119
+ @request_data[request_id][:sql_queries] << {
120
+ name: name,
121
+ duration: duration,
122
+ sql: sql,
123
+ sql_pattern: sql_pattern,
124
+ cached: cached,
125
+ source: source
126
+ }
127
+ end
128
+
129
+ # Called when template is rendered
130
+ def render_template(request_id, template: nil, duration: 0, layout: nil)
131
+ return unless @request_data[request_id]
132
+
133
+ @request_data[request_id][:views] << {
134
+ type: :template,
135
+ template: template,
136
+ duration: duration,
137
+ layout: layout
138
+ }
139
+ end
140
+
141
+ # Called when partial is rendered
142
+ def render_partial(request_id, template: nil, duration: 0, count: nil)
143
+ return unless @request_data[request_id]
144
+
145
+ @request_data[request_id][:views] << {
146
+ type: :partial,
147
+ template: template,
148
+ duration: duration,
149
+ count: count
150
+ }
151
+ end
152
+
153
+ # Called when layout is rendered
154
+ def render_layout(request_id, layout: nil, duration: 0)
155
+ return unless @request_data[request_id]
156
+
157
+ @request_data[request_id][:views] << {
158
+ type: :layout,
159
+ template: layout,
160
+ duration: duration
161
+ }
162
+ end
163
+
164
+ # Called when an error occurs
165
+ def error(request_id, exception)
166
+ return unless @request_data[request_id]
167
+
168
+ @request_data[request_id][:error] = exception.class.name
169
+ @request_data[request_id][:error_message] = exception.message
170
+ end
171
+
172
+ # Called when request ends - returns formatted output
173
+ def end_request(request_id)
174
+ data = @request_data.delete(request_id)
175
+ return nil unless data
176
+
177
+ format_request(data)
178
+ end
179
+
180
+ # Format a complete request
181
+ def format_request(data)
182
+ return nil if should_ignore?(data)
183
+ return format_simple(data) if should_be_simple?(data)
184
+
185
+ format_full(data)
186
+ end
187
+
188
+ private
189
+
190
+ def should_ignore?(data)
191
+ return false unless config[:hide_ignored]
192
+
193
+ path = data[:path].to_s.downcase
194
+ IGNORED_PATHS.any? { |p| path == p || path.end_with?(p) }
195
+ end
196
+
197
+ def should_be_simple?(data)
198
+ return true if asset_request?(data[:path])
199
+ return true if simple_path?(data[:path])
200
+
201
+ false
202
+ end
203
+
204
+ def asset_request?(path)
205
+ return false unless path
206
+
207
+ path = path.to_s.downcase
208
+ return true if ASSET_PATHS.any? { |p| path.start_with?(p) }
209
+ return true if ASSET_EXTENSIONS.any? { |ext| path.end_with?(ext) }
210
+
211
+ false
212
+ end
213
+
214
+ def simple_path?(path)
215
+ return false unless path
216
+
217
+ path = path.to_s.downcase
218
+ SIMPLE_PATHS.include?(path)
219
+ end
220
+
221
+ # Format: 09:13:23 GET /assets/app.css → 200 3ms
222
+ def format_simple(data)
223
+ return nil if config[:hide_assets] && asset_request?(data[:path])
224
+
225
+ time = format_time(data[:started_at])
226
+ method = format_method(data[:method])
227
+ path = truncate(data[:path], 40)
228
+ status = format_status(data[:status])
229
+ duration = format_duration(data[:duration])
230
+
231
+ parts = [
232
+ colorize(time, :gray),
233
+ method,
234
+ colorize(path, :white),
235
+ colorize('→', :gray),
236
+ status,
237
+ duration
238
+ ]
239
+
240
+ "#{parts.join(' ')}\n"
241
+ end
242
+
243
+ # Format full block output
244
+ def format_full(data)
245
+ lines = []
246
+ width = config[:line_width]
247
+
248
+ # Header line
249
+ method_path = "#{data[:method]} #{data[:path]}"
250
+ header = build_header(method_path, data[:error], width)
251
+ lines << header
252
+
253
+ # Status line
254
+ status_text = data[:status] ? "#{data[:status]} #{status_phrase(data[:status])}" : '---'
255
+ lines << build_line('status', format_status_value(data[:status], status_text))
256
+
257
+ # Duration line
258
+ lines << build_line('duration', format_duration_full(data[:duration])) if data[:duration]
259
+
260
+ # Database line with query analysis
261
+ if data[:db_runtime] || data[:sql_queries].any?
262
+ db_info = format_db_info(data)
263
+ lines << build_line('db', db_info)
264
+ end
265
+
266
+ # Views line
267
+ lines << build_line('views', "#{data[:view_runtime].round(1)}ms") if data[:view_runtime]
268
+
269
+ # Params section (if enabled and present) - TOML style
270
+ if config[:show_params] && data[:params].present?
271
+ params_str = format_params(data[:params])
272
+ if params_str
273
+ lines << build_line('', colorize('[params]', :gray))
274
+ lines << params_str
275
+ end
276
+ end
277
+
278
+ # Error lines
279
+ if data[:error]
280
+ lines << build_line('error', colorize(data[:error], :red))
281
+ if data[:error_message]
282
+ msg = truncate(data[:error_message], width - 15)
283
+ lines << build_line('message', colorize("\"#{msg}\"", :red))
284
+ end
285
+ end
286
+
287
+ # SQL details section (N+1, slow queries)
288
+ if config[:show_sql_details]
289
+ sql_issues = analyze_sql_queries(data[:sql_queries])
290
+ if sql_issues.any?
291
+ lines << build_separator(width)
292
+ sql_issues.each { |issue| lines << issue }
293
+ end
294
+ end
295
+
296
+ # View rendering details
297
+ if config[:show_views] && data[:views].any?
298
+ view_summary = format_view_summary(data[:views])
299
+ if view_summary.any?
300
+ lines << build_separator(width) unless sql_issues&.any?
301
+ view_summary.each { |line| lines << line }
302
+ end
303
+ end
304
+
305
+ # Footer line
306
+ lines << build_footer(width)
307
+
308
+ "#{lines.join("\n")}\n\n"
309
+ end
310
+
311
+ def analyze_sql_queries(queries)
312
+ issues = []
313
+ threshold = config[:slow_query_threshold] || SLOW_QUERY_MS
314
+ n1_threshold = config[:n_plus_one_threshold] || N_PLUS_ONE_THRESHOLD
315
+
316
+ # Group queries by pattern to detect N+1
317
+ pattern_counts = queries.reject { |q| q[:cached] }.group_by { |q| q[:sql_pattern] }
318
+
319
+ pattern_counts.each do |pattern, matching_queries|
320
+ next unless matching_queries.size >= n1_threshold
321
+ next unless pattern # Skip if no pattern
322
+
323
+ # This is a potential N+1
324
+ sample = matching_queries.first
325
+ source = sample[:source] || 'unknown'
326
+ name = sample[:name] || 'Query'
327
+
328
+ # Extract table name from pattern
329
+ table_match = pattern.match(/FROM "?(\w+)"?/i)
330
+ table = table_match ? table_match[1] : name
331
+
332
+ issues << build_line('',
333
+ "#{colorize('N+1',
334
+ :red)} #{colorize("#{table} × #{matching_queries.size}",
335
+ :yellow)}#{colorize(" (#{source})", :gray)}")
336
+
337
+ # Show the query SQL in TOML-like format
338
+ issues << format_sql_toml(sample[:sql], sample[:duration]) if config[:show_sql_queries] && sample[:sql]
339
+ end
340
+
341
+ # Find slow queries (not cached, not already reported as N+1)
342
+ n1_patterns = pattern_counts.select { |_, qs| qs.size >= n1_threshold }.keys
343
+ slow_queries = queries.reject { |q| q[:cached] || n1_patterns.include?(q[:sql_pattern]) }
344
+ .select { |q| q[:duration] && q[:duration] >= threshold }
345
+ .sort_by { |q| -q[:duration] }
346
+ .first(3)
347
+
348
+ slow_queries.each do |query|
349
+ source = query[:source] || 'unknown'
350
+ name = query[:name] || 'Query'
351
+ duration = query[:duration].round(1)
352
+
353
+ issues << build_line('',
354
+ "#{colorize('Slow',
355
+ :yellow)} #{colorize("#{name} #{duration}ms",
356
+ :white)}#{colorize(" (#{source})", :gray)}")
357
+
358
+ # Show the query SQL in TOML-like format
359
+ issues << format_sql_toml(query[:sql], query[:duration]) if config[:show_sql_queries] && query[:sql]
360
+ end
361
+
362
+ issues
363
+ end
364
+
365
+ def format_sql_toml(sql, duration = nil)
366
+ lines = []
367
+ prefix = colorize("#{BOX[:vertical]} ", :cyan)
368
+ indent = ' '
369
+
370
+ # Parse SQL to extract key components
371
+ parsed = parse_sql(sql)
372
+
373
+ # Format as TOML-like structure
374
+ lines << "#{prefix}#{indent}#{colorize('[query]', :gray)}"
375
+
376
+ if parsed[:operation]
377
+ lines << "#{prefix}#{indent}#{colorize('operation',
378
+ :gray)} = #{colorize("\"#{parsed[:operation]}\"", :green)}"
379
+ end
380
+
381
+ lines << "#{prefix}#{indent}#{colorize('table', :gray)} = #{colorize("\"#{parsed[:table]}\"", :green)}" if parsed[:table]
382
+
383
+ if parsed[:columns].any?
384
+ cols = parsed[:columns].first(5).map { |c| "\"#{c}\"" }.join(', ')
385
+ cols += ', ...' if parsed[:columns].size > 5
386
+ lines << "#{prefix}#{indent}#{colorize('columns', :gray)} = [#{colorize(cols, :cyan)}]"
387
+ end
388
+
389
+ if parsed[:conditions].any?
390
+ lines << "#{prefix}#{indent}#{colorize('[query.where]', :gray)}"
391
+ parsed[:conditions].each do |cond|
392
+ lines << "#{prefix}#{indent}#{colorize(cond[:column], :white)} = #{colorize(cond[:value], :yellow)}"
393
+ end
394
+ end
395
+
396
+ lines << "#{prefix}#{indent}#{colorize('order', :gray)} = #{colorize("\"#{parsed[:order]}\"", :green)}" if parsed[:order]
397
+
398
+ lines << "#{prefix}#{indent}#{colorize('limit', :gray)} = #{colorize(parsed[:limit].to_s, :magenta)}" if parsed[:limit]
399
+
400
+ lines << "#{prefix}#{indent}#{colorize('duration_ms', :gray)} = #{colorize(duration.round(2).to_s, :magenta)}" if duration
401
+
402
+ lines.join("\n")
403
+ end
404
+
405
+ def parse_sql(sql)
406
+ result = {
407
+ operation: nil,
408
+ table: nil,
409
+ columns: [],
410
+ conditions: [],
411
+ order: nil,
412
+ limit: nil
413
+ }
414
+
415
+ return result unless sql
416
+
417
+ # Detect operation
418
+ result[:operation] = case sql
419
+ when /^\s*SELECT/i then 'SELECT'
420
+ when /^\s*INSERT/i then 'INSERT'
421
+ when /^\s*UPDATE/i then 'UPDATE'
422
+ when /^\s*DELETE/i then 'DELETE'
423
+ else sql.split.first&.upcase
424
+ end
425
+
426
+ # Extract table name
427
+ if (match = sql.match(/FROM\s+"?(\w+)"?/i))
428
+ result[:table] = match[1]
429
+ elsif (match = sql.match(/INTO\s+"?(\w+)"?/i))
430
+ result[:table] = match[1]
431
+ elsif (match = sql.match(/UPDATE\s+"?(\w+)"?/i))
432
+ result[:table] = match[1]
433
+ end
434
+
435
+ # Extract selected columns (for SELECT)
436
+ if (match = sql.match(/SELECT\s+(.+?)\s+FROM/i))
437
+ cols = match[1]
438
+ result[:columns] = cols.split(',').map { |c| c.strip.gsub('"', '').split('.').last } if cols.strip != '*'
439
+ end
440
+
441
+ # Extract WHERE conditions
442
+ if (where_match = sql.match(/WHERE\s+(.+?)(?:ORDER|LIMIT|GROUP|$)/i))
443
+ where_clause = where_match[1]
444
+ # Parse individual conditions
445
+ where_clause.scan(/"?(\w+)"?\s*=\s*('[^']*'|\$\d+|\d+)/i).each do |col, val|
446
+ result[:conditions] << { column: col, value: val }
447
+ end
448
+ end
449
+
450
+ # Extract ORDER BY
451
+ if (match = sql.match(/ORDER\s+BY\s+"?(\w+)"?\s*(ASC|DESC)?/i))
452
+ result[:order] = "#{match[1]} #{match[2] || 'ASC'}".strip
453
+ end
454
+
455
+ # Extract LIMIT
456
+ if (match = sql.match(/LIMIT\s+(\d+)/i))
457
+ result[:limit] = match[1].to_i
458
+ end
459
+
460
+ result
461
+ end
462
+
463
+ def format_view_summary(views)
464
+ lines = []
465
+
466
+ # Find the main template and layout
467
+ templates = views.select { |v| v[:type] == :template }
468
+ partials = views.select { |v| v[:type] == :partial }
469
+
470
+ # Group partials by name to detect repeated renders
471
+ partial_counts = partials.group_by { |p| p[:template] }
472
+
473
+ templates.each do |template|
474
+ duration = template[:duration] ? " (#{template[:duration].round(1)}ms)" : ''
475
+ lines << build_line('',
476
+ "#{colorize('View',
477
+ :cyan)} #{colorize(template[:template], :white)}#{colorize(duration, :gray)}")
478
+ end
479
+
480
+ # Show partials that were rendered multiple times (potential issue)
481
+ partial_counts.each do |name, renders|
482
+ next if renders.size < 3 # Only show if rendered 3+ times
483
+
484
+ total_duration = renders.sum { |r| r[:duration] || 0 }
485
+ lines << build_line('',
486
+ "#{colorize('Partial',
487
+ :yellow)} #{colorize("#{name} × #{renders.size}",
488
+ :white)}#{colorize(
489
+ " (#{total_duration.round(1)}ms total)", :gray
490
+ )}")
491
+ end
492
+
493
+ lines
494
+ end
495
+
496
+ def build_header(text, has_error, width)
497
+ prefix = "#{BOX[:top_left]}#{BOX[:horizontal]} "
498
+ suffix = has_error ? " #{BOX[:horizontal]} ERROR #{BOX[:horizontal]}" : ' '
499
+
500
+ available = width - prefix.length - suffix.length
501
+ text = truncate(text, available)
502
+ padding = BOX[:horizontal] * [available - text.length, 0].max
503
+
504
+ header = "#{prefix}#{text} #{padding}#{suffix}"
505
+ header = header[0..width] if header.length > width + 1
506
+
507
+ if has_error
508
+ colorize(header, :red)
509
+ else
510
+ colorize(header, :cyan)
511
+ end
512
+ end
513
+
514
+ def build_line(key, value)
515
+ prefix = colorize("#{BOX[:vertical]} ", :cyan)
516
+ if key.empty?
517
+ "#{prefix}#{value}"
518
+ else
519
+ key_formatted = colorize(key.ljust(8), :gray)
520
+ "#{prefix}#{key_formatted} = #{value}"
521
+ end
522
+ end
523
+
524
+ def build_separator(width)
525
+ colorize("#{BOX[:vertical]} #{BOX[:horizontal] * (width - 4)}", :cyan)
526
+ end
527
+
528
+ def build_footer(width)
529
+ footer = "#{BOX[:bottom_left]}#{BOX[:horizontal] * (width - 1)}"
530
+ colorize(footer, :cyan)
531
+ end
532
+
533
+ def format_time(time)
534
+ return '--:--:--' unless time
535
+
536
+ time.strftime('%H:%M:%S')
537
+ end
538
+
539
+ def format_method(method)
540
+ method = method.to_s.upcase
541
+ color = case method
542
+ when 'GET' then :green
543
+ when 'POST' then :blue
544
+ when 'PUT', 'PATCH' then :yellow
545
+ when 'DELETE' then :red
546
+ else :white
547
+ end
548
+ colorize(method.ljust(6), color)
549
+ end
550
+
551
+ def format_status(status)
552
+ return colorize('---', :gray) unless status
553
+
554
+ color = case status
555
+ when 200..299 then :green
556
+ when 300..399 then :cyan
557
+ when 400..499 then :yellow
558
+ when 500..599 then :red
559
+ else :white
560
+ end
561
+ colorize(status.to_s, color)
562
+ end
563
+
564
+ def format_status_value(status, text)
565
+ color = case status
566
+ when 200..299 then :green
567
+ when 300..399 then :cyan
568
+ when 400..499 then :yellow
569
+ when 500..599 then :red
570
+ else :white
571
+ end
572
+ colorize(text, color)
573
+ end
574
+
575
+ def format_duration(ms)
576
+ return colorize('--', :gray) unless ms
577
+
578
+ formatted = if ms < 1
579
+ '<1ms'
580
+ elsif ms < 1000
581
+ "#{ms.round}ms"
582
+ else
583
+ "#{(ms / 1000.0).round(1)}s"
584
+ end
585
+
586
+ color = if ms < 100
587
+ :green
588
+ elsif ms < 500
589
+ :yellow
590
+ else
591
+ :red
592
+ end
593
+ colorize(formatted, color)
594
+ end
595
+
596
+ def format_duration_full(ms)
597
+ formatted = format_duration(ms)
598
+ # Remove color codes for comparison
599
+ ms_value = ms || 0
600
+ if ms_value < 100
601
+ formatted
602
+ elsif ms_value < 500
603
+ "#{formatted} #{colorize('(slow)', :yellow)}"
604
+ else
605
+ "#{formatted} #{colorize('(very slow)', :red)}"
606
+ end
607
+ end
608
+
609
+ def format_db_info(data)
610
+ parts = []
611
+
612
+ parts << "#{data[:db_runtime].round(1)}ms" if data[:db_runtime]
613
+
614
+ if config[:show_sql_count]
615
+ queries = data[:sql_queries] || []
616
+ total = queries.size
617
+ cached = queries.count { |q| q[:cached] }
618
+ non_cached = total - cached
619
+
620
+ query_text = "#{non_cached} #{non_cached == 1 ? 'query' : 'queries'}"
621
+ query_text += ", #{cached} cached" if cached.positive?
622
+ parts << "(#{query_text})"
623
+ end
624
+
625
+ parts.join(' ')
626
+ end
627
+
628
+ def format_params(params)
629
+ return nil if params.empty?
630
+
631
+ # Filter out controller, action, and duplicate keys
632
+ filtered = params.except('controller', 'action', :controller, :action)
633
+
634
+ # Skip 'ingest' if 'logs' exists (they're the same data)
635
+ filtered = filtered.except('ingest', :ingest) if filtered.key?('logs') || filtered.key?(:logs)
636
+
637
+ return nil if filtered.empty?
638
+
639
+ # Format as TOML-like structure
640
+ format_params_toml(filtered)
641
+ end
642
+
643
+ def hash_like?(obj)
644
+ obj.is_a?(Hash) || (!obj.is_a?(Array) && obj.respond_to?(:to_h) && obj.respond_to?(:each))
645
+ end
646
+
647
+ def format_params_toml(params, prefix = '', depth = 0)
648
+ lines = []
649
+ line_prefix = colorize("#{BOX[:vertical]} ", :cyan)
650
+ indent = " #{' ' * depth}"
651
+
652
+ params.each do |key, value|
653
+ full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
654
+
655
+ case value
656
+ when Hash, ActionController::Parameters
657
+ value_hash = begin
658
+ value.to_h
659
+ rescue StandardError
660
+ value
661
+ end
662
+ if value_hash.keys.length <= 3 && value_hash.values.all? { |v| !hash_like?(v) && !v.is_a?(Array) }
663
+ # Compact inline hash for simple cases
664
+ inline = value_hash.map { |k, v| "#{k} = #{format_value(v)}" }.join(', ')
665
+ lines << "#{line_prefix}#{indent}#{colorize(full_key, :white)} = { #{inline} }"
666
+ else
667
+ # Nested section - expand fully
668
+ lines << "#{line_prefix}#{indent}#{colorize("[#{full_key}]", :gray)}"
669
+ value_hash.each do |k, v|
670
+ if hash_like?(v)
671
+ # Recursively format nested hashes
672
+ lines << format_hash_nested(v.to_h, "#{full_key}.#{k}", depth + 1)
673
+ elsif v.is_a?(Array) && hash_like?(v.first)
674
+ lines << "#{line_prefix}#{indent} #{colorize("[[#{full_key}.#{k}]]",
675
+ :gray)} #{colorize("# #{v.length} items", :gray)}"
676
+ if v.first
677
+ first_hash = begin
678
+ v.first.to_h
679
+ rescue StandardError
680
+ v.first
681
+ end
682
+ first_hash.each do |nested_k, nested_v|
683
+ lines << "#{line_prefix}#{indent} #{colorize(nested_k.to_s, :white)} = #{format_value(nested_v)}"
684
+ end
685
+ end
686
+ else
687
+ lines << "#{line_prefix}#{indent} #{colorize(k.to_s, :white)} = #{format_value(v)}"
688
+ end
689
+ end
690
+ end
691
+ when Array
692
+ if value.length <= 5 && value.all? { |v| !hash_like?(v) && !v.is_a?(Array) }
693
+ # Compact inline array
694
+ arr = value.map { |v| format_value(v) }.join(', ')
695
+ lines << "#{line_prefix}#{indent}#{colorize(full_key, :white)} = [#{arr}]"
696
+ elsif hash_like?(value.first)
697
+ # Array of hashes (like logs array) - show each item
698
+ lines << "#{line_prefix}#{indent}#{colorize("[[#{full_key}]]",
699
+ :gray)} #{colorize("# #{value.length} items", :gray)}"
700
+ # Show first item fully expanded
701
+ if value.first
702
+ first_item = begin
703
+ value.first.to_h
704
+ rescue StandardError
705
+ value.first
706
+ end
707
+ first_item.each do |k, v|
708
+ if hash_like?(v)
709
+ # Expand nested hash fully
710
+ nested_hash = begin
711
+ v.to_h
712
+ rescue StandardError
713
+ v
714
+ end
715
+ lines << "#{line_prefix}#{indent} #{colorize("[#{k}]", :gray)}"
716
+ nested_hash.each do |nested_k, nested_v|
717
+ lines << "#{line_prefix}#{indent} #{colorize(nested_k.to_s,
718
+ :white)} = #{format_value(nested_v)}"
719
+ end
720
+ else
721
+ lines << "#{line_prefix}#{indent} #{colorize(k.to_s, :white)} = #{format_value(v)}"
722
+ end
723
+ end
724
+ end
725
+ else
726
+ # Large array of primitives
727
+ arr = value.first(5).map { |v| format_value(v) }.join(', ')
728
+ arr += ', ...' if value.length > 5
729
+ lines << "#{line_prefix}#{indent}#{colorize(full_key,
730
+ :white)} = [#{arr}] #{colorize("# #{value.length} items",
731
+ :gray)}"
732
+ end
733
+ else
734
+ lines << "#{line_prefix}#{indent}#{colorize(full_key, :white)} = #{format_value(value)}"
735
+ end
736
+ end
737
+
738
+ lines.join("\n")
739
+ end
740
+
741
+ def format_hash_nested(hash, prefix, depth)
742
+ lines = []
743
+ line_prefix = colorize("#{BOX[:vertical]} ", :cyan)
744
+ indent = " #{' ' * depth}"
745
+
746
+ lines << "#{line_prefix}#{indent}#{colorize("[#{prefix}]", :gray)}"
747
+ hash.each do |k, v|
748
+ lines << if v.is_a?(Hash)
749
+ format_hash_nested(v, "#{prefix}.#{k}", depth + 1)
750
+ else
751
+ "#{line_prefix}#{indent} #{colorize(k.to_s, :white)} = #{format_value(v)}"
752
+ end
753
+ end
754
+
755
+ lines.join("\n")
756
+ end
757
+
758
+ def format_value(value)
759
+ case value
760
+ when String
761
+ if value.length > 100
762
+ colorize("\"#{value[0..97]}...\"", :green)
763
+ else
764
+ colorize("\"#{value}\"", :green)
765
+ end
766
+ when Integer, Float
767
+ colorize(value.to_s, :magenta)
768
+ when TrueClass, FalseClass
769
+ colorize(value.to_s, :cyan)
770
+ when NilClass
771
+ colorize('null', :gray)
772
+ when Hash
773
+ items = value.first(3).map { |k, v| "#{k} = #{format_value(v)}" }.join(', ')
774
+ items += ', ...' if value.keys.length > 3
775
+ "{ #{items} }"
776
+ when Array
777
+ if value.length <= 3
778
+ "[#{value.map { |v| format_value(v) }.join(', ')}]"
779
+ else
780
+ "[#{value.first(3).map do |v|
781
+ format_value(v)
782
+ end.join(', ')}, ...] #{colorize("# #{value.length} items", :gray)}"
783
+ end
784
+ else
785
+ colorize(value.to_s, :white)
786
+ end
787
+ end
788
+
789
+ def status_phrase(status)
790
+ Rack::Utils::HTTP_STATUS_CODES[status] || 'Unknown'
791
+ end
792
+
793
+ def truncate(text, length)
794
+ return text if text.nil? || text.length <= length
795
+
796
+ "#{text[0..(length - 4)]}..."
797
+ end
798
+
799
+ def colorize(text, color)
800
+ return text unless config[:colors]
801
+ return text unless COLORS[color]
802
+
803
+ "#{COLORS[color]}#{text}#{COLORS[:reset]}"
804
+ end
805
+ end
806
+ end
807
+ end