log_bench 0.2.3 → 0.2.5
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/lib/log_bench/app/input_handler.rb +3 -3
- data/lib/log_bench/app/main.rb +4 -3
- data/lib/log_bench/app/monitor.rb +3 -5
- data/lib/log_bench/app/renderer/details.rb +131 -167
- data/lib/log_bench/app/renderer/request_list.rb +35 -40
- data/lib/log_bench/log/call_line_entry.rb +0 -9
- data/lib/log_bench/log/collection.rb +0 -8
- data/lib/log_bench/log/entry.rb +0 -9
- data/lib/log_bench/log/file.rb +4 -0
- data/lib/log_bench/log/parser.rb +1 -3
- data/lib/log_bench/log/query_entry.rb +46 -32
- data/lib/log_bench/log/request.rb +27 -22
- data/lib/log_bench/version.rb +1 -1
- metadata +2 -3
- data/lib/log_bench/log/cache_entry.rb +0 -79
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 202c43226cc39fb8457f7c2bce99e17a048aa680bf9594f9186520d34f2852ea
|
4
|
+
data.tar.gz: 8fcd799173130062385dd69ca0c4145d904470af1f95644776c467a06de83438
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b73a0d54830c2e30567e3b3eaab18d6f3086ee12e6851c1236c62a205986e4aafee729e2114baf128e8d72f94e58831a2c1fc30e24f45aa5666fbffd3407df4
|
7
|
+
data.tar.gz: 78b435a7bbd1b7d9e15ea1e462b3ce4b3267d4d00e75cd4dec7a3859ebf1e45acfd92fdca53855677d3eaaf04e691cf96ed8bd6a56a30e43ca8b28ff3a815e2b
|
@@ -52,12 +52,12 @@ module LogBench
|
|
52
52
|
|
53
53
|
def handle_filter_input(ch)
|
54
54
|
case ch
|
55
|
-
when 10, 13, 27
|
55
|
+
when 10, 13, 27 # Enter, Return, Escape
|
56
56
|
state.exit_filter_mode
|
57
|
-
when KEY_UP
|
57
|
+
when KEY_UP
|
58
58
|
state.exit_filter_mode
|
59
59
|
state.navigate_up
|
60
|
-
when KEY_DOWN
|
60
|
+
when KEY_DOWN
|
61
61
|
state.exit_filter_mode
|
62
62
|
state.navigate_down
|
63
63
|
when 127, 8 # Backspace
|
data/lib/log_bench/app/main.rb
CHANGED
@@ -37,7 +37,7 @@ module LogBench
|
|
37
37
|
|
38
38
|
private
|
39
39
|
|
40
|
-
attr_accessor :log_file_path, :state, :screen, :monitor, :input_handler, :renderer
|
40
|
+
attr_accessor :log_file_path, :log_file, :state, :screen, :monitor, :input_handler, :renderer
|
41
41
|
|
42
42
|
def find_log_file(path)
|
43
43
|
candidates = [path] + DEFAULT_LOG_PATHS
|
@@ -67,8 +67,9 @@ module LogBench
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def load_initial_data
|
70
|
-
log_file = Log::File.new(log_file_path)
|
70
|
+
self.log_file = Log::File.new(log_file_path)
|
71
71
|
state.requests = log_file.requests
|
72
|
+
log_file.mark_as_read!
|
72
73
|
end
|
73
74
|
|
74
75
|
def check_for_updates
|
@@ -81,7 +82,7 @@ module LogBench
|
|
81
82
|
end
|
82
83
|
|
83
84
|
def start_monitoring
|
84
|
-
self.monitor = Monitor.new(
|
85
|
+
self.monitor = Monitor.new(log_file, state)
|
85
86
|
monitor.start
|
86
87
|
end
|
87
88
|
|
@@ -3,8 +3,8 @@
|
|
3
3
|
module LogBench
|
4
4
|
module App
|
5
5
|
class Monitor
|
6
|
-
def initialize(
|
7
|
-
self.
|
6
|
+
def initialize(log_file, state)
|
7
|
+
self.log_file = log_file
|
8
8
|
self.state = state
|
9
9
|
self.running = false
|
10
10
|
end
|
@@ -23,11 +23,9 @@ module LogBench
|
|
23
23
|
|
24
24
|
private
|
25
25
|
|
26
|
-
attr_accessor :
|
26
|
+
attr_accessor :log_file, :state, :thread, :running
|
27
27
|
|
28
28
|
def monitor_loop
|
29
|
-
log_file = Log::File.new(log_file_path)
|
30
|
-
|
31
29
|
loop do
|
32
30
|
break unless running
|
33
31
|
|
@@ -14,6 +14,8 @@ module LogBench
|
|
14
14
|
self.state = state
|
15
15
|
self.scrollbar = scrollbar
|
16
16
|
self.ansi_renderer = ansi_renderer
|
17
|
+
self.cached_lines = nil
|
18
|
+
self.cache_key = nil
|
17
19
|
end
|
18
20
|
|
19
21
|
def draw
|
@@ -26,7 +28,7 @@ module LogBench
|
|
26
28
|
|
27
29
|
private
|
28
30
|
|
29
|
-
attr_accessor :screen, :state, :scrollbar, :ansi_renderer
|
31
|
+
attr_accessor :screen, :state, :scrollbar, :ansi_renderer, :cached_lines, :cache_key
|
30
32
|
|
31
33
|
def draw_header
|
32
34
|
detail_win.setpos(0, 2)
|
@@ -54,7 +56,7 @@ module LogBench
|
|
54
56
|
request = state.current_request
|
55
57
|
return unless request
|
56
58
|
|
57
|
-
lines =
|
59
|
+
lines = get_cached_detail_lines(request)
|
58
60
|
visible_height = detail_win.maxy - 2
|
59
61
|
|
60
62
|
adjust_detail_scroll(lines.size, visible_height)
|
@@ -95,15 +97,38 @@ module LogBench
|
|
95
97
|
end
|
96
98
|
end
|
97
99
|
|
100
|
+
def get_cached_detail_lines(request)
|
101
|
+
current_cache_key = build_cache_key(request)
|
102
|
+
|
103
|
+
# Return cached lines if cache is still valid
|
104
|
+
if cached_lines && cache_key == current_cache_key
|
105
|
+
return cached_lines
|
106
|
+
end
|
107
|
+
|
108
|
+
# Cache is invalid, rebuild lines
|
109
|
+
self.cached_lines = build_detail_lines(request)
|
110
|
+
self.cache_key = current_cache_key
|
111
|
+
cached_lines
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_cache_key(request)
|
115
|
+
# Cache key includes factors that affect the rendered output
|
116
|
+
[
|
117
|
+
request.request_id,
|
118
|
+
request.related_logs.size,
|
119
|
+
state.detail_filter.display_text,
|
120
|
+
detail_win.maxx # Window width affects text wrapping
|
121
|
+
]
|
122
|
+
end
|
123
|
+
|
98
124
|
def build_detail_lines(request)
|
99
125
|
lines = []
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
log = request_to_log_format(request)
|
126
|
+
# Cache window width to avoid repeated method calls
|
127
|
+
window_width = detail_win.maxx
|
128
|
+
max_width = window_width - 6 # Leave margin for borders and scrollbar
|
104
129
|
|
105
130
|
# Method - separate label and value colors
|
106
|
-
method_color = case
|
131
|
+
method_color = case request.method
|
107
132
|
when "GET" then color_pair(3) | A_BOLD
|
108
133
|
when "POST" then color_pair(4) | A_BOLD
|
109
134
|
when "PUT" then color_pair(5) | A_BOLD
|
@@ -113,58 +138,28 @@ module LogBench
|
|
113
138
|
|
114
139
|
lines << EMPTY_LINE
|
115
140
|
lines << {
|
116
|
-
text: "Method: #{
|
141
|
+
text: "Method: #{request.method}",
|
117
142
|
color: nil,
|
118
143
|
segments: [
|
119
144
|
{text: "Method: ", color: color_pair(1)},
|
120
|
-
{text:
|
145
|
+
{text: request.method, color: method_color}
|
121
146
|
]
|
122
147
|
}
|
123
148
|
|
124
149
|
# Path - allow multiple lines with proper color separation
|
125
|
-
add_path_lines(lines,
|
126
|
-
add_status_duration_lines(lines,
|
127
|
-
add_controller_lines(lines,
|
128
|
-
add_request_id_lines(lines,
|
129
|
-
add_params_lines(lines,
|
130
|
-
add_related_logs_section(lines,
|
150
|
+
add_path_lines(lines, request, max_width)
|
151
|
+
add_status_duration_lines(lines, request)
|
152
|
+
add_controller_lines(lines, request)
|
153
|
+
add_request_id_lines(lines, request)
|
154
|
+
add_params_lines(lines, request, max_width)
|
155
|
+
add_related_logs_section(lines, request)
|
131
156
|
|
132
157
|
lines
|
133
158
|
end
|
134
159
|
|
135
|
-
def
|
136
|
-
{
|
137
|
-
method: request.method,
|
138
|
-
path: request.path,
|
139
|
-
status: request.status,
|
140
|
-
duration: request.duration,
|
141
|
-
controller: request.controller,
|
142
|
-
action: request.action,
|
143
|
-
params: request.params,
|
144
|
-
request_id: request.request_id,
|
145
|
-
related_logs: build_related_logs(request)
|
146
|
-
}
|
147
|
-
end
|
148
|
-
|
149
|
-
def build_related_logs(request)
|
150
|
-
related = []
|
151
|
-
|
152
|
-
# Add all related logs from the request
|
153
|
-
request.related_logs.each do |log|
|
154
|
-
related << {
|
155
|
-
type: log.type,
|
156
|
-
content: log.content,
|
157
|
-
timing: log.timing,
|
158
|
-
timestamp: log.timestamp
|
159
|
-
}
|
160
|
-
end
|
161
|
-
|
162
|
-
related
|
163
|
-
end
|
164
|
-
|
165
|
-
def add_path_lines(lines, log, max_width)
|
160
|
+
def add_path_lines(lines, request, max_width)
|
166
161
|
path_prefix = "Path: "
|
167
|
-
remaining_path =
|
162
|
+
remaining_path = request.path
|
168
163
|
|
169
164
|
# First line starts after "Path: " (6 characters)
|
170
165
|
first_line_width = max_width - path_prefix.length
|
@@ -199,10 +194,10 @@ module LogBench
|
|
199
194
|
end
|
200
195
|
end
|
201
196
|
|
202
|
-
def add_status_duration_lines(lines,
|
203
|
-
if
|
197
|
+
def add_status_duration_lines(lines, request)
|
198
|
+
if request.status
|
204
199
|
# Add status color coding
|
205
|
-
status_color = case
|
200
|
+
status_color = case request.status
|
206
201
|
when 200..299 then color_pair(3) # Green
|
207
202
|
when 300..399 then color_pair(4) # Yellow
|
208
203
|
when 400..599 then color_pair(6) # Red
|
@@ -212,12 +207,12 @@ module LogBench
|
|
212
207
|
# Build segments for mixed coloring
|
213
208
|
segments = [
|
214
209
|
{text: "Status: ", color: color_pair(1)},
|
215
|
-
{text:
|
210
|
+
{text: request.status.to_s, color: status_color}
|
216
211
|
]
|
217
212
|
|
218
|
-
if
|
213
|
+
if request.duration
|
219
214
|
segments << {text: " | Duration: ", color: color_pair(1)}
|
220
|
-
segments << {text: "#{
|
215
|
+
segments << {text: "#{request.duration}ms", color: nil} # Default white color
|
221
216
|
end
|
222
217
|
|
223
218
|
status_text = segments.map { |s| s[:text] }.join
|
@@ -229,9 +224,9 @@ module LogBench
|
|
229
224
|
end
|
230
225
|
end
|
231
226
|
|
232
|
-
def add_controller_lines(lines,
|
233
|
-
if
|
234
|
-
controller_value = "#{
|
227
|
+
def add_controller_lines(lines, request)
|
228
|
+
if request.controller
|
229
|
+
controller_value = "#{request.controller}##{request.action}"
|
235
230
|
lines << {
|
236
231
|
text: "Controller: #{controller_value}",
|
237
232
|
color: nil,
|
@@ -243,8 +238,8 @@ module LogBench
|
|
243
238
|
end
|
244
239
|
end
|
245
240
|
|
246
|
-
def add_params_lines(lines,
|
247
|
-
return unless
|
241
|
+
def add_params_lines(lines, request, max_width)
|
242
|
+
return unless request.params
|
248
243
|
|
249
244
|
lines << EMPTY_LINE
|
250
245
|
lines << {
|
@@ -255,7 +250,7 @@ module LogBench
|
|
255
250
|
]
|
256
251
|
}
|
257
252
|
|
258
|
-
params_text = format_params(
|
253
|
+
params_text = format_params(request.params)
|
259
254
|
indent = " "
|
260
255
|
|
261
256
|
# Split the params text into lines that fit within the available width
|
@@ -317,14 +312,14 @@ module LogBench
|
|
317
312
|
end
|
318
313
|
end
|
319
314
|
|
320
|
-
def add_request_id_lines(lines,
|
321
|
-
if
|
315
|
+
def add_request_id_lines(lines, request)
|
316
|
+
if request.request_id
|
322
317
|
lines << {
|
323
|
-
text: "Request ID: #{
|
318
|
+
text: "Request ID: #{request.request_id}",
|
324
319
|
color: nil,
|
325
320
|
segments: [
|
326
321
|
{text: "Request ID: ", color: color_pair(1)},
|
327
|
-
{text:
|
322
|
+
{text: request.request_id, color: nil} # Default white color
|
328
323
|
]
|
329
324
|
}
|
330
325
|
end
|
@@ -338,19 +333,16 @@ module LogBench
|
|
338
333
|
screen.detail_win
|
339
334
|
end
|
340
335
|
|
341
|
-
def add_related_logs_section(lines,
|
336
|
+
def add_related_logs_section(lines, request)
|
342
337
|
# Related Logs (grouped by request_id) - only show non-HTTP request logs
|
343
|
-
if
|
344
|
-
related_logs =
|
345
|
-
|
346
|
-
# Sort by timestamp
|
347
|
-
related_logs.sort_by! { |l| l[:timestamp] || Time.at(0) }
|
338
|
+
if request.request_id && request.related_logs && !request.related_logs.empty?
|
339
|
+
related_logs = request.related_logs
|
348
340
|
|
349
341
|
# Apply detail filter to related logs
|
350
342
|
filtered_related_logs = filter_related_logs(related_logs)
|
351
343
|
|
352
|
-
#
|
353
|
-
query_stats =
|
344
|
+
# Use memoized query statistics from request object
|
345
|
+
query_stats = build_query_stats_from_request(request)
|
354
346
|
|
355
347
|
# Add query summary
|
356
348
|
lines << EMPTY_LINE
|
@@ -360,30 +352,30 @@ module LogBench
|
|
360
352
|
lines << {text: summary_title, color: color_pair(1) | A_BOLD}
|
361
353
|
|
362
354
|
if query_stats[:total_queries] > 0
|
363
|
-
|
355
|
+
# Build summary line with string interpolation
|
356
|
+
summary_parts = ["#{query_stats[:total_queries]} queries"]
|
357
|
+
|
364
358
|
if query_stats[:total_time] > 0
|
365
|
-
|
366
|
-
if query_stats[:cached_queries] > 0
|
367
|
-
|
368
|
-
end
|
369
|
-
summary_line += ")"
|
359
|
+
time_part = "#{query_stats[:total_time].round(1)}ms total"
|
360
|
+
time_part += ", #{query_stats[:cached_queries]} cached" if query_stats[:cached_queries] > 0
|
361
|
+
summary_parts << "(#{time_part})"
|
370
362
|
elsif query_stats[:cached_queries] > 0
|
371
|
-
|
363
|
+
summary_parts << "(#{query_stats[:cached_queries]} cached)"
|
372
364
|
end
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
breakdown_parts
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
lines << {text:
|
365
|
+
|
366
|
+
lines << {text: " #{summary_parts.join(" ")}", color: color_pair(2)}
|
367
|
+
|
368
|
+
# Breakdown by operation type - build array efficiently
|
369
|
+
breakdown_parts = [
|
370
|
+
("#{query_stats[:select]} SELECT" if query_stats[:select] > 0),
|
371
|
+
("#{query_stats[:insert]} INSERT" if query_stats[:insert] > 0),
|
372
|
+
("#{query_stats[:update]} UPDATE" if query_stats[:update] > 0),
|
373
|
+
("#{query_stats[:delete]} DELETE" if query_stats[:delete] > 0),
|
374
|
+
("#{query_stats[:transaction]} TRANSACTION" if query_stats[:transaction] > 0)
|
375
|
+
].compact
|
376
|
+
|
377
|
+
unless breakdown_parts.empty?
|
378
|
+
lines << {text: " #{breakdown_parts.join(", ")}", color: color_pair(2)}
|
387
379
|
end
|
388
380
|
end
|
389
381
|
|
@@ -408,77 +400,56 @@ module LogBench
|
|
408
400
|
|
409
401
|
# Use filtered logs for display
|
410
402
|
filtered_related_logs.each do |related|
|
411
|
-
case related
|
403
|
+
case related.type
|
412
404
|
when :sql, :cache
|
413
|
-
render_padded_text_with_spacing(related
|
405
|
+
render_padded_text_with_spacing(related.content, lines, extra_empty_lines: 0)
|
414
406
|
else
|
415
|
-
render_padded_text_with_spacing(related
|
407
|
+
render_padded_text_with_spacing(related.content, lines, extra_empty_lines: 1)
|
416
408
|
end
|
417
409
|
end
|
418
410
|
end
|
419
411
|
end
|
420
412
|
|
421
|
-
def
|
413
|
+
def build_query_stats_from_request(request)
|
414
|
+
# Use memoized methods from request object for better performance
|
422
415
|
stats = {
|
423
|
-
total_queries:
|
424
|
-
total_time:
|
416
|
+
total_queries: request.query_count,
|
417
|
+
total_time: request.total_query_time,
|
418
|
+
cached_queries: request.cached_query_count,
|
425
419
|
select: 0,
|
426
420
|
insert: 0,
|
427
421
|
update: 0,
|
428
422
|
delete: 0,
|
429
|
-
transaction: 0
|
430
|
-
cache: 0,
|
431
|
-
cached_queries: 0
|
423
|
+
transaction: 0
|
432
424
|
}
|
433
425
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
stats[:total_queries] += 1
|
438
|
-
|
439
|
-
# Extract timing from the content
|
440
|
-
if log[:timing]
|
441
|
-
# Parse timing like "(1.2ms)" or "1.2ms"
|
442
|
-
timing_str = log[:timing].gsub(/[()ms]/, "")
|
443
|
-
timing_value = timing_str.to_f
|
444
|
-
stats[:total_time] += timing_value
|
445
|
-
end
|
426
|
+
# Categorize by operation type for breakdown
|
427
|
+
request.related_logs.each do |log|
|
428
|
+
next unless [:sql, :cache].include?(log.type)
|
446
429
|
|
447
|
-
|
448
|
-
content = log[:content].upcase
|
449
|
-
if content.include?("CACHE")
|
450
|
-
stats[:cached_queries] += 1
|
451
|
-
# Still categorize cached queries by their operation type
|
452
|
-
if content.include?("SELECT")
|
453
|
-
stats[:select] += 1
|
454
|
-
elsif content.include?("INSERT")
|
455
|
-
stats[:insert] += 1
|
456
|
-
elsif content.include?("UPDATE")
|
457
|
-
stats[:update] += 1
|
458
|
-
elsif content.include?("DELETE")
|
459
|
-
stats[:delete] += 1
|
460
|
-
elsif content.include?("TRANSACTION") || content.include?("BEGIN") || content.include?("COMMIT") || content.include?("ROLLBACK")
|
461
|
-
stats[:transaction] += 1
|
462
|
-
end
|
463
|
-
elsif content.include?("SELECT")
|
464
|
-
stats[:select] += 1
|
465
|
-
elsif content.include?("INSERT")
|
466
|
-
stats[:insert] += 1
|
467
|
-
elsif content.include?("UPDATE")
|
468
|
-
stats[:update] += 1
|
469
|
-
elsif content.include?("DELETE")
|
470
|
-
stats[:delete] += 1
|
471
|
-
elsif content.include?("TRANSACTION") || content.include?("BEGIN") || content.include?("COMMIT") || content.include?("ROLLBACK") || content.include?("SAVEPOINT")
|
472
|
-
stats[:transaction] += 1
|
473
|
-
end
|
430
|
+
categorize_sql_operation(log, stats)
|
474
431
|
end
|
475
432
|
|
476
|
-
# Round total time to 1 decimal place
|
477
|
-
stats[:total_time] = stats[:total_time].round(1)
|
478
|
-
|
479
433
|
stats
|
480
434
|
end
|
481
435
|
|
436
|
+
def categorize_sql_operation(log, stats)
|
437
|
+
# Use unified QueryEntry for both SQL and CACHE entries
|
438
|
+
return unless log.is_a?(LogBench::Log::QueryEntry)
|
439
|
+
|
440
|
+
if log.select?
|
441
|
+
stats[:select] += 1
|
442
|
+
elsif log.insert?
|
443
|
+
stats[:insert] += 1
|
444
|
+
elsif log.update?
|
445
|
+
stats[:update] += 1
|
446
|
+
elsif log.delete?
|
447
|
+
stats[:delete] += 1
|
448
|
+
elsif log.transaction? || log.begin? || log.commit? || log.rollback? || log.savepoint?
|
449
|
+
stats[:transaction] += 1
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
482
453
|
def filter_related_logs(related_logs)
|
483
454
|
# Filter related logs (SQL, cache, etc.) in the detail pane
|
484
455
|
return related_logs unless state.detail_filter.present?
|
@@ -487,27 +458,25 @@ module LogBench
|
|
487
458
|
|
488
459
|
# First pass: find direct matches
|
489
460
|
related_logs.each_with_index do |log, index|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
matched_indices.add(index - 1)
|
504
|
-
end
|
461
|
+
next unless log.content && state.detail_filter.matches?(log.content)
|
462
|
+
|
463
|
+
matched_indices.add(index)
|
464
|
+
|
465
|
+
# Add context lines based on log type
|
466
|
+
case log.type
|
467
|
+
when :sql_call_line
|
468
|
+
# If match is a sql_call_line, include the line below (the actual SQL query)
|
469
|
+
matched_indices.add(index + 1) if index + 1 < related_logs.size
|
470
|
+
when :sql, :cache
|
471
|
+
# If match is a sql or cache, include the line above (the call stack line)
|
472
|
+
if index > 0 && related_logs[index - 1].type == :sql_call_line
|
473
|
+
matched_indices.add(index - 1)
|
505
474
|
end
|
506
475
|
end
|
507
476
|
end
|
508
477
|
|
509
|
-
# Return logs in original order
|
510
|
-
matched_indices.
|
478
|
+
# Return logs in original order - optimize array operations
|
479
|
+
matched_indices.sort.map { |index| related_logs[index] }
|
511
480
|
end
|
512
481
|
|
513
482
|
def render_padded_text_with_spacing(text, lines, extra_empty_lines: 1)
|
@@ -535,17 +504,12 @@ module LogBench
|
|
535
504
|
end
|
536
505
|
|
537
506
|
# Add extra empty lines after all chunks
|
538
|
-
extra_empty_lines.times
|
539
|
-
lines << EMPTY_LINE
|
540
|
-
end
|
541
|
-
|
542
|
-
text_chunks.length
|
507
|
+
extra_empty_lines.times { lines << EMPTY_LINE }
|
543
508
|
end
|
544
509
|
|
545
510
|
def adjust_detail_scroll(total_lines, visible_height)
|
546
511
|
max_scroll = [total_lines - visible_height, 0].max
|
547
|
-
state.detail_scroll_offset =
|
548
|
-
state.detail_scroll_offset = [state.detail_scroll_offset, 0].max
|
512
|
+
state.detail_scroll_offset = state.detail_scroll_offset.clamp(0, max_scroll)
|
549
513
|
end
|
550
514
|
end
|
551
515
|
end
|
@@ -107,75 +107,70 @@ module LogBench
|
|
107
107
|
|
108
108
|
def draw_row(request, request_index, y_position)
|
109
109
|
log_win.setpos(y_position, 1)
|
110
|
+
is_selected = request_index == state.selected
|
110
111
|
|
111
|
-
if
|
112
|
+
if is_selected
|
112
113
|
log_win.attron(color_pair(10) | A_DIM) do
|
113
114
|
log_win.addstr(" " * (screen.panel_width - 4))
|
114
115
|
end
|
115
116
|
log_win.setpos(y_position, 1)
|
116
117
|
end
|
117
118
|
|
118
|
-
draw_method_badge(request,
|
119
|
-
draw_path_column(request,
|
120
|
-
draw_status_column(request,
|
121
|
-
draw_duration_column(request,
|
119
|
+
draw_method_badge(request, is_selected)
|
120
|
+
draw_path_column(request, is_selected)
|
121
|
+
draw_status_column(request, is_selected)
|
122
|
+
draw_duration_column(request, is_selected)
|
122
123
|
end
|
123
124
|
|
124
|
-
def draw_method_badge(request,
|
125
|
-
|
125
|
+
def draw_method_badge(request, is_selected)
|
126
|
+
method_text = " #{request.method.ljust(7)} "
|
126
127
|
|
127
|
-
if
|
128
|
-
log_win.attron(color_pair(10) | A_DIM)
|
129
|
-
log_win.addstr(" #{request.method.ljust(7)} ")
|
130
|
-
end
|
128
|
+
if is_selected
|
129
|
+
log_win.attron(color_pair(10) | A_DIM) { log_win.addstr(method_text) }
|
131
130
|
else
|
132
|
-
|
133
|
-
|
134
|
-
end
|
131
|
+
method_color = method_color_for(request.method)
|
132
|
+
log_win.attron(color_pair(method_color) | A_BOLD) { log_win.addstr(method_text) }
|
135
133
|
end
|
136
134
|
end
|
137
135
|
|
138
|
-
def draw_path_column(request,
|
136
|
+
def draw_path_column(request, is_selected)
|
139
137
|
path_width = screen.panel_width - 27
|
140
138
|
path = request.path[0, path_width] || ""
|
139
|
+
path_text = path.ljust(path_width)
|
141
140
|
|
142
|
-
if
|
143
|
-
log_win.attron(color_pair(10) | A_DIM)
|
144
|
-
log_win.addstr(path.ljust(path_width))
|
145
|
-
end
|
141
|
+
if is_selected
|
142
|
+
log_win.attron(color_pair(10) | A_DIM) { log_win.addstr(path_text) }
|
146
143
|
else
|
147
|
-
log_win.addstr(
|
144
|
+
log_win.addstr(path_text)
|
148
145
|
end
|
149
146
|
end
|
150
147
|
|
151
|
-
def draw_status_column(request,
|
148
|
+
def draw_status_column(request, is_selected)
|
149
|
+
return unless request.status
|
150
|
+
|
152
151
|
status_col_start = screen.panel_width - 14
|
152
|
+
status_text = "#{request.status.to_s.rjust(3)} "
|
153
153
|
|
154
|
-
|
154
|
+
log_win.setpos(log_win.cury, status_col_start)
|
155
|
+
if is_selected
|
156
|
+
log_win.attron(color_pair(10) | A_DIM) { log_win.addstr(status_text) }
|
157
|
+
else
|
155
158
|
status_color = status_color_for(request.status)
|
156
|
-
|
157
|
-
|
158
|
-
log_win.setpos(log_win.cury, status_col_start)
|
159
|
-
if request_index == state.selected
|
160
|
-
log_win.attron(color_pair(10) | A_DIM) { log_win.addstr(status_text + " ") }
|
161
|
-
else
|
162
|
-
log_win.attron(color_pair(status_color)) { log_win.addstr(status_text + " ") }
|
163
|
-
end
|
159
|
+
log_win.attron(color_pair(status_color)) { log_win.addstr(status_text) }
|
164
160
|
end
|
165
161
|
end
|
166
162
|
|
167
|
-
def draw_duration_column(request,
|
168
|
-
|
163
|
+
def draw_duration_column(request, is_selected)
|
164
|
+
return unless request.duration
|
169
165
|
|
170
|
-
|
171
|
-
|
166
|
+
duration_col_start = screen.panel_width - 9
|
167
|
+
duration_text = "#{request.duration.to_i}ms".ljust(6) + " "
|
172
168
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
end
|
169
|
+
log_win.setpos(log_win.cury, duration_col_start)
|
170
|
+
if is_selected
|
171
|
+
log_win.attron(color_pair(10) | A_DIM) { log_win.addstr(duration_text) }
|
172
|
+
else
|
173
|
+
log_win.attron(A_DIM) { log_win.addstr(duration_text) }
|
179
174
|
end
|
180
175
|
end
|
181
176
|
|
@@ -17,15 +17,6 @@ module LogBench
|
|
17
17
|
new(raw_line)
|
18
18
|
end
|
19
19
|
|
20
|
-
def to_h
|
21
|
-
super.merge(
|
22
|
-
content: content,
|
23
|
-
file_path: file_path,
|
24
|
-
line_number: line_number,
|
25
|
-
method_name: method_name
|
26
|
-
)
|
27
|
-
end
|
28
|
-
|
29
20
|
private
|
30
21
|
|
31
22
|
attr_accessor :file_path, :line_number, :method_name
|
@@ -27,14 +27,6 @@ module LogBench
|
|
27
27
|
entries.select { |entry| entry.is_a?(Request) }
|
28
28
|
end
|
29
29
|
|
30
|
-
def queries
|
31
|
-
entries.flat_map(&:queries)
|
32
|
-
end
|
33
|
-
|
34
|
-
def cache_operations
|
35
|
-
entries.flat_map(&:cache_operations)
|
36
|
-
end
|
37
|
-
|
38
30
|
def filter_by_method(method)
|
39
31
|
filtered_requests = requests.select { |req| req.method == method.upcase }
|
40
32
|
create_collection_from_requests(filtered_requests)
|
data/lib/log_bench/log/entry.rb
CHANGED
@@ -31,15 +31,6 @@ module LogBench
|
|
31
31
|
!http_request?
|
32
32
|
end
|
33
33
|
|
34
|
-
def to_h
|
35
|
-
{
|
36
|
-
raw: raw_line,
|
37
|
-
timestamp: timestamp,
|
38
|
-
request_id: request_id,
|
39
|
-
type: type
|
40
|
-
}
|
41
|
-
end
|
42
|
-
|
43
34
|
private
|
44
35
|
|
45
36
|
attr_writer :type, :raw_line, :timestamp, :request_id, :content, :timing
|
data/lib/log_bench/log/file.rb
CHANGED
data/lib/log_bench/log/parser.rb
CHANGED
@@ -23,10 +23,8 @@ module LogBench
|
|
23
23
|
case entry.type
|
24
24
|
when :http_request
|
25
25
|
Request.build(entry.raw_line)
|
26
|
-
when :sql
|
26
|
+
when :sql, :cache
|
27
27
|
QueryEntry.build(entry.raw_line)
|
28
|
-
when :cache
|
29
|
-
CacheEntry.build(entry.raw_line)
|
30
28
|
when :sql_call_line
|
31
29
|
CallLineEntry.build(entry.raw_line)
|
32
30
|
else
|
@@ -14,24 +14,25 @@ module LogBench
|
|
14
14
|
SAVEPOINT = "SAVEPOINT"
|
15
15
|
SQL_OPERATIONS = [SELECT, INSERT, UPDATE, DELETE, TRANSACTION, BEGIN_TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT].freeze
|
16
16
|
|
17
|
-
def initialize(raw_line)
|
18
|
-
super
|
19
|
-
self.type = :sql
|
17
|
+
def initialize(raw_line, cached: false)
|
18
|
+
super(raw_line)
|
19
|
+
self.type = cached ? :cache : :sql
|
20
|
+
@cached = cached
|
20
21
|
end
|
21
22
|
|
22
23
|
def self.build(raw_line)
|
23
24
|
return unless parseable?(raw_line)
|
24
25
|
|
25
26
|
entry = Entry.new(raw_line)
|
26
|
-
return unless entry.type
|
27
|
+
return unless [:sql, :cache].include?(entry.type)
|
27
28
|
|
28
|
-
|
29
|
+
# Create QueryEntry for both SQL and CACHE entries
|
30
|
+
cached = entry.type == :cache
|
31
|
+
new(raw_line, cached: cached)
|
29
32
|
end
|
30
33
|
|
31
34
|
def duration_ms
|
32
|
-
|
33
|
-
|
34
|
-
timing.gsub(/[()ms]/, "").to_f
|
35
|
+
@duration_ms ||= calculate_duration_ms
|
35
36
|
end
|
36
37
|
|
37
38
|
def select?
|
@@ -70,28 +71,24 @@ module LogBench
|
|
70
71
|
operation == SAVEPOINT
|
71
72
|
end
|
72
73
|
|
73
|
-
def
|
74
|
-
|
74
|
+
def cached?
|
75
|
+
@cached
|
75
76
|
end
|
76
77
|
|
77
|
-
def
|
78
|
-
|
79
|
-
content: content,
|
80
|
-
timing: timing,
|
81
|
-
operation: operation,
|
82
|
-
duration_ms: duration_ms,
|
83
|
-
has_ansi: has_ansi_codes?(content)
|
84
|
-
)
|
78
|
+
def hit?
|
79
|
+
cached? && content.include?("CACHE")
|
85
80
|
end
|
86
81
|
|
87
82
|
private
|
88
83
|
|
84
|
+
attr_accessor :operation
|
85
|
+
|
89
86
|
def extract_from_json(data)
|
90
87
|
# Call parent method which checks for request_id
|
91
88
|
return false unless super
|
92
89
|
|
93
90
|
message = data["message"] || ""
|
94
|
-
return false unless sql_message?(data)
|
91
|
+
return false unless sql_message?(data) || cache_message?(data)
|
95
92
|
|
96
93
|
self.content = message.strip
|
97
94
|
extract_timing_and_operation
|
@@ -99,31 +96,48 @@ module LogBench
|
|
99
96
|
end
|
100
97
|
|
101
98
|
def extract_timing_and_operation
|
102
|
-
|
103
|
-
self.
|
104
|
-
self.operation = extract_operation(clean_content)
|
99
|
+
self.timing = extract_timing
|
100
|
+
self.operation = extract_operation
|
105
101
|
end
|
106
102
|
|
107
|
-
def extract_timing
|
108
|
-
match =
|
103
|
+
def extract_timing
|
104
|
+
match = clean_content.match(/\(([0-9.]+ms)\)/)
|
109
105
|
match ? match[1] : nil
|
110
106
|
end
|
111
107
|
|
112
|
-
def extract_operation
|
113
|
-
SQL_OPERATIONS.find { |op|
|
108
|
+
def extract_operation
|
109
|
+
SQL_OPERATIONS.find { |op| clean_content.include?(op) }
|
114
110
|
end
|
115
111
|
|
116
|
-
def
|
117
|
-
|
112
|
+
def clean_content
|
113
|
+
@clean_content ||= content&.gsub(/\e\[[0-9;]*m/, "") || ""
|
118
114
|
end
|
119
115
|
|
120
|
-
def has_ansi_codes?
|
121
|
-
|
116
|
+
def has_ansi_codes?
|
117
|
+
@has_ansi_codes ||= content&.match?(/\e\[[0-9;]*m/) || false
|
122
118
|
end
|
123
119
|
|
124
|
-
|
120
|
+
def calculate_duration_ms
|
121
|
+
return 0.0 unless timing
|
125
122
|
|
126
|
-
|
123
|
+
timing.gsub(/[()ms]/, "").to_f
|
124
|
+
end
|
125
|
+
|
126
|
+
def clear_memoized_values
|
127
|
+
@duration_ms = nil
|
128
|
+
@clean_content = nil
|
129
|
+
@has_ansi_codes = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def content=(value)
|
133
|
+
super
|
134
|
+
clear_memoized_values
|
135
|
+
end
|
136
|
+
|
137
|
+
def timing=(value)
|
138
|
+
super
|
139
|
+
clear_memoized_values
|
140
|
+
end
|
127
141
|
end
|
128
142
|
end
|
129
143
|
end
|
@@ -3,8 +3,7 @@
|
|
3
3
|
module LogBench
|
4
4
|
module Log
|
5
5
|
class Request < Entry
|
6
|
-
attr_reader :method, :path, :status, :duration, :controller, :action, :params
|
7
|
-
attr_accessor :related_logs
|
6
|
+
attr_reader :method, :path, :status, :duration, :controller, :action, :params, :related_logs
|
8
7
|
|
9
8
|
def initialize(raw_line)
|
10
9
|
super
|
@@ -21,28 +20,34 @@ module LogBench
|
|
21
20
|
end
|
22
21
|
|
23
22
|
def add_related_log(log_entry)
|
24
|
-
|
25
|
-
|
23
|
+
if log_entry.related_log?
|
24
|
+
related_logs << log_entry
|
25
|
+
clear_memoized_values
|
26
|
+
end
|
26
27
|
end
|
27
28
|
|
28
29
|
def queries
|
29
|
-
related_logs.select { |log| log.is_a?(QueryEntry) }
|
30
|
+
@queries ||= related_logs.select { |log| log.is_a?(QueryEntry) }
|
30
31
|
end
|
31
32
|
|
32
33
|
def cache_operations
|
33
|
-
related_logs.select { |log| log.is_a?(
|
34
|
+
@cache_operations ||= related_logs.select { |log| log.is_a?(QueryEntry) && log.cached? }
|
35
|
+
end
|
36
|
+
|
37
|
+
def sql_queries
|
38
|
+
@sql_queries ||= related_logs.select { |log| log.is_a?(QueryEntry) && !log.cached? }
|
34
39
|
end
|
35
40
|
|
36
41
|
def query_count
|
37
|
-
queries.size
|
42
|
+
@query_count ||= queries.size
|
38
43
|
end
|
39
44
|
|
40
45
|
def total_query_time
|
41
|
-
queries.sum(&:duration_ms)
|
46
|
+
@total_query_time ||= queries.sum(&:duration_ms)
|
42
47
|
end
|
43
48
|
|
44
49
|
def cached_query_count
|
45
|
-
cache_operations.size
|
50
|
+
@cached_query_count ||= cache_operations.size
|
46
51
|
end
|
47
52
|
|
48
53
|
def success?
|
@@ -57,23 +62,23 @@ module LogBench
|
|
57
62
|
status && status >= 500
|
58
63
|
end
|
59
64
|
|
60
|
-
def to_h
|
61
|
-
super.merge(
|
62
|
-
method: method,
|
63
|
-
path: path,
|
64
|
-
status: status,
|
65
|
-
duration: duration,
|
66
|
-
controller: controller,
|
67
|
-
action: action,
|
68
|
-
params: params,
|
69
|
-
related_logs: related_logs.map(&:to_h)
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
65
|
private
|
74
66
|
|
75
67
|
attr_writer :method, :path, :status, :duration, :controller, :action, :params
|
76
68
|
|
69
|
+
def related_logs=(value)
|
70
|
+
@related_logs = value
|
71
|
+
clear_memoized_values
|
72
|
+
end
|
73
|
+
|
74
|
+
def clear_memoized_values
|
75
|
+
@queries = nil
|
76
|
+
@cache_operations = nil
|
77
|
+
@query_count = nil
|
78
|
+
@total_query_time = nil
|
79
|
+
@cached_query_count = nil
|
80
|
+
end
|
81
|
+
|
77
82
|
def extract_from_json(data)
|
78
83
|
return false unless super
|
79
84
|
|
data/lib/log_bench/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: log_bench
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamín Silva
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-07-
|
10
|
+
date: 2025-07-10 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: zeitwerk
|
@@ -143,7 +143,6 @@ files:
|
|
143
143
|
- lib/log_bench/configuration_validator.rb
|
144
144
|
- lib/log_bench/current.rb
|
145
145
|
- lib/log_bench/json_formatter.rb
|
146
|
-
- lib/log_bench/log/cache_entry.rb
|
147
146
|
- lib/log_bench/log/call_line_entry.rb
|
148
147
|
- lib/log_bench/log/collection.rb
|
149
148
|
- lib/log_bench/log/entry.rb
|
@@ -1,79 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module LogBench
|
4
|
-
module Log
|
5
|
-
class CacheEntry < Entry
|
6
|
-
SQL_OPERATIONS = %w[SELECT INSERT UPDATE DELETE].freeze
|
7
|
-
|
8
|
-
def initialize(raw_line)
|
9
|
-
super
|
10
|
-
self.type = :cache
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.build(raw_line)
|
14
|
-
return unless parseable?(raw_line)
|
15
|
-
|
16
|
-
entry = Entry.new(raw_line)
|
17
|
-
return unless entry.type == :cache
|
18
|
-
|
19
|
-
new(raw_line)
|
20
|
-
end
|
21
|
-
|
22
|
-
def duration_ms
|
23
|
-
return 0.0 unless timing
|
24
|
-
|
25
|
-
timing.gsub(/[()ms]/, "").to_f
|
26
|
-
end
|
27
|
-
|
28
|
-
def hit?
|
29
|
-
content.include?("CACHE")
|
30
|
-
end
|
31
|
-
|
32
|
-
def miss?
|
33
|
-
!hit?
|
34
|
-
end
|
35
|
-
|
36
|
-
def to_h
|
37
|
-
super.merge(
|
38
|
-
content: content,
|
39
|
-
timing: timing,
|
40
|
-
operation: operation,
|
41
|
-
duration_ms: duration_ms,
|
42
|
-
hit: hit?
|
43
|
-
)
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
attr_writer :operation
|
49
|
-
|
50
|
-
def extract_from_json(data)
|
51
|
-
super
|
52
|
-
message = data["message"] || ""
|
53
|
-
return unless cache_message?(data)
|
54
|
-
|
55
|
-
self.content = message.strip
|
56
|
-
extract_timing_and_operation
|
57
|
-
end
|
58
|
-
|
59
|
-
def extract_timing_and_operation
|
60
|
-
clean_content = remove_ansi_codes(content)
|
61
|
-
self.timing = extract_timing(clean_content)
|
62
|
-
self.operation = extract_operation(clean_content)
|
63
|
-
end
|
64
|
-
|
65
|
-
def extract_timing(text)
|
66
|
-
match = text.match(/\(([0-9.]+ms)\)/)
|
67
|
-
match ? match[1] : nil
|
68
|
-
end
|
69
|
-
|
70
|
-
def extract_operation(text)
|
71
|
-
SQL_OPERATIONS.find { |op| text.include?(op) }
|
72
|
-
end
|
73
|
-
|
74
|
-
def remove_ansi_codes(text)
|
75
|
-
text.gsub(/\e\[[0-9;]*m/, "")
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|