log_bench 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/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/Rakefile +10 -0
- data/exe/log_bench +87 -0
- data/lib/log_bench/app/filter.rb +60 -0
- data/lib/log_bench/app/input_handler.rb +245 -0
- data/lib/log_bench/app/main.rb +96 -0
- data/lib/log_bench/app/monitor.rb +59 -0
- data/lib/log_bench/app/renderer/ansi.rb +176 -0
- data/lib/log_bench/app/renderer/details.rb +488 -0
- data/lib/log_bench/app/renderer/header.rb +99 -0
- data/lib/log_bench/app/renderer/main.rb +30 -0
- data/lib/log_bench/app/renderer/request_list.rb +211 -0
- data/lib/log_bench/app/renderer/scrollbar.rb +38 -0
- data/lib/log_bench/app/screen.rb +96 -0
- data/lib/log_bench/app/sort.rb +61 -0
- data/lib/log_bench/app/state.rb +175 -0
- data/lib/log_bench/json_formatter.rb +92 -0
- data/lib/log_bench/log/cache_entry.rb +82 -0
- data/lib/log_bench/log/call_line_entry.rb +60 -0
- data/lib/log_bench/log/collection.rb +98 -0
- data/lib/log_bench/log/entry.rb +108 -0
- data/lib/log_bench/log/file.rb +103 -0
- data/lib/log_bench/log/parser.rb +64 -0
- data/lib/log_bench/log/query_entry.rb +132 -0
- data/lib/log_bench/log/request.rb +90 -0
- data/lib/log_bench/railtie.rb +45 -0
- data/lib/log_bench/version.rb +5 -0
- data/lib/log_bench.rb +26 -0
- data/lib/tasks/log_bench.rake +97 -0
- data/logbench-preview.png +0 -0
- metadata +171 -0
@@ -0,0 +1,488 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module LogBench
|
6
|
+
module App
|
7
|
+
module Renderer
|
8
|
+
class Details
|
9
|
+
include Curses
|
10
|
+
|
11
|
+
def initialize(screen, state, scrollbar, ansi_renderer)
|
12
|
+
self.screen = screen
|
13
|
+
self.state = state
|
14
|
+
self.scrollbar = scrollbar
|
15
|
+
self.ansi_renderer = ansi_renderer
|
16
|
+
end
|
17
|
+
|
18
|
+
def draw
|
19
|
+
detail_win.erase
|
20
|
+
detail_win.box(0, 0)
|
21
|
+
|
22
|
+
draw_header
|
23
|
+
draw_request_details
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_accessor :screen, :state, :scrollbar, :ansi_renderer
|
29
|
+
|
30
|
+
def draw_header
|
31
|
+
detail_win.setpos(0, 2)
|
32
|
+
|
33
|
+
if state.right_pane_focused?
|
34
|
+
detail_win.attron(color_pair(1) | A_BOLD) { detail_win.addstr(" Request Details ") }
|
35
|
+
else
|
36
|
+
detail_win.attron(color_pair(2) | A_DIM) { detail_win.addstr(" Request Details ") }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Show detail filter to the right of the title (always visible when active)
|
40
|
+
if state.detail_filter.present? || state.detail_filter.active?
|
41
|
+
filter_text = "Filter: #{state.detail_filter.cursor_display}"
|
42
|
+
|
43
|
+
# Position filter text to the right, leaving some space
|
44
|
+
filter_x = detail_win.maxx - filter_text.length - 3
|
45
|
+
if filter_x > 20 # Only show if there's enough space
|
46
|
+
detail_win.setpos(0, filter_x)
|
47
|
+
detail_win.attron(color_pair(4)) { detail_win.addstr(filter_text) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def draw_request_details
|
53
|
+
request = state.current_request
|
54
|
+
return unless request
|
55
|
+
|
56
|
+
lines = build_detail_lines(request)
|
57
|
+
visible_height = detail_win.maxy - 2
|
58
|
+
|
59
|
+
adjust_detail_scroll(lines.size, visible_height)
|
60
|
+
|
61
|
+
visible_lines = lines[state.detail_scroll_offset, visible_height] || []
|
62
|
+
visible_lines.each_with_index do |line_data, i|
|
63
|
+
y = i + 1 # Start at row 1 (after border)
|
64
|
+
detail_win.setpos(y, 2)
|
65
|
+
|
66
|
+
# Handle multi-segment lines (for mixed colors)
|
67
|
+
if line_data.is_a?(Hash) && line_data[:segments]
|
68
|
+
line_data[:segments].each do |segment|
|
69
|
+
if segment[:color]
|
70
|
+
detail_win.attron(segment[:color]) { detail_win.addstr(segment[:text]) }
|
71
|
+
else
|
72
|
+
detail_win.addstr(segment[:text])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
elsif line_data.is_a?(Hash) && line_data[:raw_ansi]
|
76
|
+
# Handle lines with raw ANSI codes (like colorized SQL)
|
77
|
+
ansi_renderer.parse_and_render(line_data[:text], detail_win)
|
78
|
+
elsif line_data.is_a?(Hash)
|
79
|
+
# Handle single-color lines
|
80
|
+
if line_data[:color]
|
81
|
+
detail_win.attron(line_data[:color]) { detail_win.addstr(line_data[:text]) }
|
82
|
+
else
|
83
|
+
detail_win.addstr(line_data[:text])
|
84
|
+
end
|
85
|
+
else
|
86
|
+
# Simple string
|
87
|
+
detail_win.addstr(line_data.to_s)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Draw scrollbar if needed
|
92
|
+
if lines.size > visible_height
|
93
|
+
scrollbar.draw(detail_win, visible_height, state.detail_scroll_offset, lines.size)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_detail_lines(request)
|
98
|
+
lines = []
|
99
|
+
max_width = detail_win.maxx - 6 # Leave margin for borders and scrollbar
|
100
|
+
|
101
|
+
# Convert request to log format for compatibility with original implementation
|
102
|
+
log = request_to_log_format(request)
|
103
|
+
|
104
|
+
# Method - separate label and value colors
|
105
|
+
method_color = case log[:method]
|
106
|
+
when "GET" then color_pair(3) | A_BOLD
|
107
|
+
when "POST" then color_pair(4) | A_BOLD
|
108
|
+
when "PUT" then color_pair(5) | A_BOLD
|
109
|
+
when "DELETE" then color_pair(6) | A_BOLD
|
110
|
+
else color_pair(2) | A_BOLD
|
111
|
+
end
|
112
|
+
|
113
|
+
lines << {text: "", color: nil} # Empty line
|
114
|
+
lines << {
|
115
|
+
text: "Method: #{log[:method]}",
|
116
|
+
color: nil,
|
117
|
+
segments: [
|
118
|
+
{text: "Method: ", color: color_pair(1)},
|
119
|
+
{text: log[:method], color: method_color}
|
120
|
+
]
|
121
|
+
}
|
122
|
+
|
123
|
+
# Path - allow multiple lines with proper color separation
|
124
|
+
add_path_lines(lines, log, max_width)
|
125
|
+
add_status_duration_lines(lines, log)
|
126
|
+
add_controller_lines(lines, log)
|
127
|
+
add_request_id_lines(lines, log)
|
128
|
+
add_related_logs_section(lines, log)
|
129
|
+
|
130
|
+
lines
|
131
|
+
end
|
132
|
+
|
133
|
+
def request_to_log_format(request)
|
134
|
+
{
|
135
|
+
method: request.method,
|
136
|
+
path: request.path,
|
137
|
+
status: request.status,
|
138
|
+
duration: request.duration,
|
139
|
+
controller: request.controller,
|
140
|
+
action: request.action,
|
141
|
+
request_id: request.request_id,
|
142
|
+
related_logs: build_related_logs(request)
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_related_logs(request)
|
147
|
+
related = []
|
148
|
+
|
149
|
+
# Add all related logs from the request
|
150
|
+
request.related_logs.each do |log|
|
151
|
+
content = case log
|
152
|
+
when LogBench::Log::QueryEntry, LogBench::Log::CacheEntry, LogBench::Log::CallLineEntry
|
153
|
+
log.content
|
154
|
+
else
|
155
|
+
log.raw_line # Fallback to raw line for other entry types
|
156
|
+
end
|
157
|
+
|
158
|
+
timing = case log
|
159
|
+
when LogBench::Log::QueryEntry, LogBench::Log::CacheEntry
|
160
|
+
log.timing
|
161
|
+
end
|
162
|
+
|
163
|
+
related << {
|
164
|
+
type: log.type,
|
165
|
+
content: content,
|
166
|
+
timing: timing,
|
167
|
+
timestamp: log.timestamp
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
related
|
172
|
+
end
|
173
|
+
|
174
|
+
def add_path_lines(lines, log, max_width)
|
175
|
+
path_prefix = "Path: "
|
176
|
+
remaining_path = log[:path]
|
177
|
+
|
178
|
+
# First line starts after "Path: " (6 characters)
|
179
|
+
first_line_width = max_width - path_prefix.length
|
180
|
+
if remaining_path.length <= first_line_width
|
181
|
+
lines << {
|
182
|
+
text: path_prefix + remaining_path,
|
183
|
+
color: nil,
|
184
|
+
segments: [
|
185
|
+
{text: path_prefix, color: color_pair(1)},
|
186
|
+
{text: remaining_path, color: nil} # Default white color
|
187
|
+
]
|
188
|
+
}
|
189
|
+
else
|
190
|
+
# Split into multiple lines
|
191
|
+
first_chunk = remaining_path[0, first_line_width]
|
192
|
+
lines << {
|
193
|
+
text: path_prefix + first_chunk,
|
194
|
+
color: nil,
|
195
|
+
segments: [
|
196
|
+
{text: path_prefix, color: color_pair(1)},
|
197
|
+
{text: first_chunk, color: nil} # Default white color
|
198
|
+
]
|
199
|
+
}
|
200
|
+
remaining_path = remaining_path[first_line_width..]
|
201
|
+
|
202
|
+
# Continue on subsequent lines
|
203
|
+
while remaining_path.length > 0
|
204
|
+
line_chunk = remaining_path[0, max_width]
|
205
|
+
lines << {text: line_chunk, color: nil} # Default white color
|
206
|
+
remaining_path = remaining_path[max_width..] || ""
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def add_status_duration_lines(lines, log)
|
212
|
+
if log[:status]
|
213
|
+
# Add status color coding
|
214
|
+
status_color = case log[:status]
|
215
|
+
when 200..299 then color_pair(3) # Green
|
216
|
+
when 300..399 then color_pair(4) # Yellow
|
217
|
+
when 400..599 then color_pair(6) # Red
|
218
|
+
else color_pair(2) # Default
|
219
|
+
end
|
220
|
+
|
221
|
+
# Build segments for mixed coloring
|
222
|
+
segments = [
|
223
|
+
{text: "Status: ", color: color_pair(1)},
|
224
|
+
{text: log[:status].to_s, color: status_color}
|
225
|
+
]
|
226
|
+
|
227
|
+
if log[:duration]
|
228
|
+
segments << {text: " | Duration: ", color: color_pair(1)}
|
229
|
+
segments << {text: "#{log[:duration]}ms", color: nil} # Default white color
|
230
|
+
end
|
231
|
+
|
232
|
+
status_text = segments.map { |s| s[:text] }.join
|
233
|
+
lines << {
|
234
|
+
text: status_text,
|
235
|
+
color: nil,
|
236
|
+
segments: segments
|
237
|
+
}
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def add_controller_lines(lines, log)
|
242
|
+
if log[:controller]
|
243
|
+
controller_value = "#{log[:controller]}##{log[:action]}"
|
244
|
+
lines << {
|
245
|
+
text: "Controller: #{controller_value}",
|
246
|
+
color: nil,
|
247
|
+
segments: [
|
248
|
+
{text: "Controller: ", color: color_pair(1)},
|
249
|
+
{text: controller_value, color: nil} # Default white color
|
250
|
+
]
|
251
|
+
}
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def add_request_id_lines(lines, log)
|
256
|
+
if log[:request_id]
|
257
|
+
lines << {
|
258
|
+
text: "Request ID: #{log[:request_id]}",
|
259
|
+
color: nil,
|
260
|
+
segments: [
|
261
|
+
{text: "Request ID: ", color: color_pair(1)},
|
262
|
+
{text: log[:request_id], color: nil} # Default white color
|
263
|
+
]
|
264
|
+
}
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def color_pair(n)
|
269
|
+
screen.color_pair(n)
|
270
|
+
end
|
271
|
+
|
272
|
+
def detail_win
|
273
|
+
screen.detail_win
|
274
|
+
end
|
275
|
+
|
276
|
+
def add_related_logs_section(lines, log)
|
277
|
+
# Related Logs (grouped by request_id) - only show non-HTTP request logs
|
278
|
+
if log[:request_id] && log[:related_logs] && !log[:related_logs].empty?
|
279
|
+
related_logs = log[:related_logs]
|
280
|
+
|
281
|
+
# Sort by timestamp
|
282
|
+
related_logs.sort_by! { |l| l[:timestamp] || Time.at(0) }
|
283
|
+
|
284
|
+
# Apply detail filter to related logs
|
285
|
+
filtered_related_logs = filter_related_logs(related_logs)
|
286
|
+
|
287
|
+
# Calculate query statistics (use original logs for stats)
|
288
|
+
query_stats = calculate_query_stats(related_logs)
|
289
|
+
|
290
|
+
# Add query summary
|
291
|
+
lines << {text: "", color: nil} # Empty line
|
292
|
+
|
293
|
+
# Show filter status in summary if filtering is active
|
294
|
+
summary_title = "Query Summary:"
|
295
|
+
lines << {text: summary_title, color: color_pair(1) | A_BOLD}
|
296
|
+
|
297
|
+
if query_stats[:total_queries] > 0
|
298
|
+
summary_line = " #{query_stats[:total_queries]} queries"
|
299
|
+
if query_stats[:total_time] > 0
|
300
|
+
summary_line += " (#{query_stats[:total_time]}ms total"
|
301
|
+
if query_stats[:cached_queries] > 0
|
302
|
+
summary_line += ", #{query_stats[:cached_queries]} cached"
|
303
|
+
end
|
304
|
+
summary_line += ")"
|
305
|
+
elsif query_stats[:cached_queries] > 0
|
306
|
+
summary_line += " (#{query_stats[:cached_queries]} cached)"
|
307
|
+
end
|
308
|
+
lines << {text: summary_line, color: color_pair(2)}
|
309
|
+
|
310
|
+
# Breakdown by operation type
|
311
|
+
breakdown_parts = []
|
312
|
+
breakdown_parts << "#{query_stats[:select]} SELECT" if query_stats[:select] > 0
|
313
|
+
breakdown_parts << "#{query_stats[:insert]} INSERT" if query_stats[:insert] > 0
|
314
|
+
breakdown_parts << "#{query_stats[:update]} UPDATE" if query_stats[:update] > 0
|
315
|
+
breakdown_parts << "#{query_stats[:delete]} DELETE" if query_stats[:delete] > 0
|
316
|
+
breakdown_parts << "#{query_stats[:transaction]} TRANSACTION" if query_stats[:transaction] > 0
|
317
|
+
breakdown_parts << "#{query_stats[:cache]} CACHE" if query_stats[:cache] > 0
|
318
|
+
|
319
|
+
if !breakdown_parts.empty?
|
320
|
+
breakdown_line = " " + breakdown_parts.join(", ")
|
321
|
+
lines << {text: breakdown_line, color: color_pair(2)}
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
lines << {text: "", color: nil} # Empty line
|
326
|
+
|
327
|
+
# Show filtered logs section
|
328
|
+
if state.detail_filter.present?
|
329
|
+
count_text = "(#{filtered_related_logs.size}/#{related_logs.size} shown)"
|
330
|
+
logs_title_text = "Related Logs #{count_text}:"
|
331
|
+
lines << {
|
332
|
+
text: logs_title_text,
|
333
|
+
color: nil,
|
334
|
+
segments: [
|
335
|
+
{text: "Related Logs ", color: color_pair(1) | A_BOLD},
|
336
|
+
{text: count_text, color: A_DIM},
|
337
|
+
{text: ":", color: color_pair(1) | A_BOLD}
|
338
|
+
]
|
339
|
+
}
|
340
|
+
else
|
341
|
+
lines << {text: "Related Logs:", color: color_pair(1) | A_BOLD}
|
342
|
+
end
|
343
|
+
|
344
|
+
# Use filtered logs for display
|
345
|
+
filtered_related_logs.each do |related|
|
346
|
+
case related[:type]
|
347
|
+
when :sql, :cache
|
348
|
+
render_padded_text_with_spacing(related[:content], lines, extra_empty_lines: 0)
|
349
|
+
else
|
350
|
+
render_padded_text_with_spacing(related[:content], lines, extra_empty_lines: 1)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def calculate_query_stats(related_logs)
|
357
|
+
stats = {
|
358
|
+
total_queries: 0,
|
359
|
+
total_time: 0.0,
|
360
|
+
select: 0,
|
361
|
+
insert: 0,
|
362
|
+
update: 0,
|
363
|
+
delete: 0,
|
364
|
+
transaction: 0,
|
365
|
+
cache: 0,
|
366
|
+
cached_queries: 0
|
367
|
+
}
|
368
|
+
|
369
|
+
related_logs.each do |log|
|
370
|
+
next unless [:sql, :cache].include?(log[:type])
|
371
|
+
|
372
|
+
stats[:total_queries] += 1
|
373
|
+
|
374
|
+
# Extract timing from the content
|
375
|
+
if log[:timing]
|
376
|
+
# Parse timing like "(1.2ms)" or "1.2ms"
|
377
|
+
timing_str = log[:timing].gsub(/[()ms]/, "")
|
378
|
+
timing_value = timing_str.to_f
|
379
|
+
stats[:total_time] += timing_value
|
380
|
+
end
|
381
|
+
|
382
|
+
# Categorize by SQL operation and check for cache
|
383
|
+
content = log[:content].upcase
|
384
|
+
if content.include?("CACHE")
|
385
|
+
stats[:cached_queries] += 1
|
386
|
+
# Still categorize cached queries by their operation type
|
387
|
+
if content.include?("SELECT")
|
388
|
+
stats[:select] += 1
|
389
|
+
elsif content.include?("INSERT")
|
390
|
+
stats[:insert] += 1
|
391
|
+
elsif content.include?("UPDATE")
|
392
|
+
stats[:update] += 1
|
393
|
+
elsif content.include?("DELETE")
|
394
|
+
stats[:delete] += 1
|
395
|
+
elsif content.include?("TRANSACTION") || content.include?("BEGIN") || content.include?("COMMIT") || content.include?("ROLLBACK")
|
396
|
+
stats[:transaction] += 1
|
397
|
+
end
|
398
|
+
elsif content.include?("SELECT")
|
399
|
+
stats[:select] += 1
|
400
|
+
elsif content.include?("INSERT")
|
401
|
+
stats[:insert] += 1
|
402
|
+
elsif content.include?("UPDATE")
|
403
|
+
stats[:update] += 1
|
404
|
+
elsif content.include?("DELETE")
|
405
|
+
stats[:delete] += 1
|
406
|
+
elsif content.include?("TRANSACTION") || content.include?("BEGIN") || content.include?("COMMIT") || content.include?("ROLLBACK") || content.include?("SAVEPOINT")
|
407
|
+
stats[:transaction] += 1
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# Round total time to 1 decimal place
|
412
|
+
stats[:total_time] = stats[:total_time].round(1)
|
413
|
+
|
414
|
+
stats
|
415
|
+
end
|
416
|
+
|
417
|
+
def filter_related_logs(related_logs)
|
418
|
+
# Filter related logs (SQL, cache, etc.) in the detail pane
|
419
|
+
return related_logs unless state.detail_filter.present?
|
420
|
+
|
421
|
+
matched_indices = Set.new
|
422
|
+
|
423
|
+
# First pass: find direct matches
|
424
|
+
related_logs.each_with_index do |log, index|
|
425
|
+
if log[:content] && state.detail_filter.matches?(log[:content])
|
426
|
+
matched_indices.add(index)
|
427
|
+
|
428
|
+
# Add context lines based on log type
|
429
|
+
case log[:type]
|
430
|
+
when :sql_call_line
|
431
|
+
# If match is a sql_call_line, include the line below (the actual SQL query)
|
432
|
+
if index + 1 < related_logs.size
|
433
|
+
matched_indices.add(index + 1)
|
434
|
+
end
|
435
|
+
when :sql, :cache
|
436
|
+
# If match is a sql or cache, include the line above (the call stack line)
|
437
|
+
if index > 0 && related_logs[index - 1][:type] == :sql_call_line
|
438
|
+
matched_indices.add(index - 1)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Return logs in original order
|
445
|
+
matched_indices.to_a.sort.map { |index| related_logs[index] }
|
446
|
+
end
|
447
|
+
|
448
|
+
def render_padded_text_with_spacing(text, lines, extra_empty_lines: 1)
|
449
|
+
# Helper function that renders text with padding, breaking long text into multiple lines
|
450
|
+
content_width = detail_win.maxx - 8 # Account for padding (4 spaces each side)
|
451
|
+
|
452
|
+
# Automatically detect if text contains ANSI codes
|
453
|
+
has_ansi = ansi_renderer.has_ansi_codes?(text)
|
454
|
+
|
455
|
+
text_chunks = if has_ansi
|
456
|
+
# For ANSI text, break it into properly sized chunks
|
457
|
+
ansi_renderer.wrap_ansi_text(text, content_width)
|
458
|
+
else
|
459
|
+
# For plain text, break it into chunks
|
460
|
+
ansi_renderer.wrap_plain_text(text, content_width)
|
461
|
+
end
|
462
|
+
|
463
|
+
# Render each chunk as a separate line with padding
|
464
|
+
text_chunks.each do |chunk|
|
465
|
+
lines << if has_ansi
|
466
|
+
{text: " #{chunk} ", color: nil, raw_ansi: true}
|
467
|
+
else
|
468
|
+
{text: " #{chunk} ", color: nil}
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Add extra empty lines after all chunks
|
473
|
+
extra_empty_lines.times do
|
474
|
+
lines << {text: "", color: nil}
|
475
|
+
end
|
476
|
+
|
477
|
+
text_chunks.length
|
478
|
+
end
|
479
|
+
|
480
|
+
def adjust_detail_scroll(total_lines, visible_height)
|
481
|
+
max_scroll = [total_lines - visible_height, 0].max
|
482
|
+
state.detail_scroll_offset = [state.detail_scroll_offset, max_scroll].min
|
483
|
+
state.detail_scroll_offset = [state.detail_scroll_offset, 0].max
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module App
|
5
|
+
module Renderer
|
6
|
+
class Header
|
7
|
+
include Curses
|
8
|
+
|
9
|
+
# Application info
|
10
|
+
APP_NAME = "LogBench"
|
11
|
+
APP_SUBTITLE = "Rails Log Viewer"
|
12
|
+
DEFAULT_LOG_FILENAME = "development.log"
|
13
|
+
|
14
|
+
# Layout constants
|
15
|
+
TITLE_X_OFFSET = 2
|
16
|
+
FILENAME_X_OFFSET = 15
|
17
|
+
|
18
|
+
# Color constants
|
19
|
+
HEADER_CYAN = 1
|
20
|
+
SUCCESS_GREEN = 3
|
21
|
+
|
22
|
+
def initialize(screen, state)
|
23
|
+
self.screen = screen
|
24
|
+
self.state = state
|
25
|
+
end
|
26
|
+
|
27
|
+
def draw
|
28
|
+
header_win.erase
|
29
|
+
header_win.box(0, 0)
|
30
|
+
|
31
|
+
draw_title
|
32
|
+
draw_file_name
|
33
|
+
draw_stats
|
34
|
+
draw_help_text
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_accessor :screen, :state
|
40
|
+
|
41
|
+
def draw_title
|
42
|
+
header_win.setpos(1, TITLE_X_OFFSET)
|
43
|
+
header_win.attron(color_pair(HEADER_CYAN) | A_BOLD) { header_win.addstr(APP_NAME) }
|
44
|
+
header_win.addstr(" - #{APP_SUBTITLE}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw_file_name
|
48
|
+
header_win.setpos(1, screen.width / 2 - FILENAME_X_OFFSET)
|
49
|
+
header_win.attron(color_pair(SUCCESS_GREEN)) { header_win.addstr(DEFAULT_LOG_FILENAME) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def draw_stats
|
53
|
+
filtered_requests = state.filtered_requests
|
54
|
+
total_requests = state.requests.size
|
55
|
+
|
56
|
+
if state.main_filter.present?
|
57
|
+
# Filter active - show "X found (Y total)"
|
58
|
+
stats_text = "#{filtered_requests.size} found (#{total_requests} total)"
|
59
|
+
header_win.setpos(1, screen.width - stats_text.length - 2)
|
60
|
+
header_win.attron(color_pair(3)) { header_win.addstr(filtered_requests.size.to_s) }
|
61
|
+
header_win.addstr(" found (")
|
62
|
+
header_win.attron(color_pair(3)) { header_win.addstr(total_requests.to_s) }
|
63
|
+
header_win.addstr(" total)")
|
64
|
+
else
|
65
|
+
# No filter active - show simple count
|
66
|
+
stats_text = "Requests: #{total_requests}"
|
67
|
+
header_win.setpos(1, screen.width - stats_text.length - 2)
|
68
|
+
header_win.addstr("Requests: ")
|
69
|
+
header_win.attron(color_pair(3)) { header_win.addstr(total_requests.to_s) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def draw_help_text
|
74
|
+
header_win.setpos(2, 2)
|
75
|
+
header_win.attron(A_DIM) do
|
76
|
+
header_win.addstr("a:Auto-scroll(")
|
77
|
+
header_win.attron(color_pair(3)) { header_win.addstr(state.auto_scroll ? "ON" : "OFF") }
|
78
|
+
header_win.addstr(") | f:Filter | c:Clear | s:Sort(")
|
79
|
+
header_win.attron(color_pair(3)) { header_win.addstr(state.sort.display_name) }
|
80
|
+
header_win.addstr(") | q:Quit")
|
81
|
+
end
|
82
|
+
|
83
|
+
header_win.setpos(3, 2)
|
84
|
+
header_win.attron(A_DIM) do
|
85
|
+
header_win.addstr("←→/hl:Switch Pane | ↑↓/jk:Navigate | g/G:Top/End")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def color_pair(n)
|
90
|
+
screen.color_pair(n)
|
91
|
+
end
|
92
|
+
|
93
|
+
def header_win
|
94
|
+
screen.header_win
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module App
|
5
|
+
module Renderer
|
6
|
+
class Main
|
7
|
+
def initialize(screen, state)
|
8
|
+
self.screen = screen
|
9
|
+
self.state = state
|
10
|
+
self.scrollbar = Scrollbar.new(screen)
|
11
|
+
self.ansi_renderer = Ansi.new(screen)
|
12
|
+
self.header = Header.new(screen, state)
|
13
|
+
self.request_list = RequestList.new(screen, state, scrollbar)
|
14
|
+
self.details = Details.new(screen, state, scrollbar, ansi_renderer)
|
15
|
+
end
|
16
|
+
|
17
|
+
def draw
|
18
|
+
header.draw
|
19
|
+
request_list.draw
|
20
|
+
details.draw
|
21
|
+
screen.refresh_all
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_accessor :screen, :state, :header, :scrollbar, :request_list, :ansi_renderer, :details
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|