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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3d418b0832252146580022464c8b6401766ee056fb89642c42840d723a835e9
4
- data.tar.gz: 8f98119ed45ffe24bbffc947388598dd90be1643ed87b25bb1981e3e81effcd5
3
+ metadata.gz: 202c43226cc39fb8457f7c2bce99e17a048aa680bf9594f9186520d34f2852ea
4
+ data.tar.gz: 8fcd799173130062385dd69ca0c4145d904470af1f95644776c467a06de83438
5
5
  SHA512:
6
- metadata.gz: 47681831e5f88acea414d1f1825849ce715a4b227e9180c3d290e98de6571ca886c26e889cb053c32dd267bd80a7ae32ca9a2f36286767afb238c754953382e3
7
- data.tar.gz: b261d76dd28d336a516f3245e6533551e6d6003e743ffc774686f12a7ef6b48852caf90eb8cc516ba569a47cea5644d08b41e9e4e645bb9181e456746099c371
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, "k", "K"
57
+ when KEY_UP
58
58
  state.exit_filter_mode
59
59
  state.navigate_up
60
- when KEY_DOWN, "j", "J"
60
+ when KEY_DOWN
61
61
  state.exit_filter_mode
62
62
  state.navigate_down
63
63
  when 127, 8 # Backspace
@@ -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(log_file_path, state)
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(log_file_path, state)
7
- self.log_file_path = log_file_path
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 :log_file_path, :state, :thread, :running
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 = build_detail_lines(request)
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
- max_width = detail_win.maxx - 6 # Leave margin for borders and scrollbar
101
-
102
- # Convert request to log format for compatibility with original implementation
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 log[:method]
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: #{log[:method]}",
141
+ text: "Method: #{request.method}",
117
142
  color: nil,
118
143
  segments: [
119
144
  {text: "Method: ", color: color_pair(1)},
120
- {text: log[:method], color: method_color}
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, log, max_width)
126
- add_status_duration_lines(lines, log)
127
- add_controller_lines(lines, log)
128
- add_request_id_lines(lines, log)
129
- add_params_lines(lines, log, max_width)
130
- add_related_logs_section(lines, log)
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 request_to_log_format(request)
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 = log[: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, log)
203
- if log[:status]
197
+ def add_status_duration_lines(lines, request)
198
+ if request.status
204
199
  # Add status color coding
205
- status_color = case log[:status]
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: log[:status].to_s, color: status_color}
210
+ {text: request.status.to_s, color: status_color}
216
211
  ]
217
212
 
218
- if log[:duration]
213
+ if request.duration
219
214
  segments << {text: " | Duration: ", color: color_pair(1)}
220
- segments << {text: "#{log[:duration]}ms", color: nil} # Default white color
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, log)
233
- if log[:controller]
234
- controller_value = "#{log[:controller]}##{log[:action]}"
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, log, max_width)
247
- return unless log[:params]
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(log[: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, log)
321
- if log[:request_id]
315
+ def add_request_id_lines(lines, request)
316
+ if request.request_id
322
317
  lines << {
323
- text: "Request ID: #{log[: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: log[:request_id], color: nil} # Default white color
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, log)
336
+ def add_related_logs_section(lines, request)
342
337
  # Related Logs (grouped by request_id) - only show non-HTTP request logs
343
- if log[:request_id] && log[:related_logs] && !log[:related_logs].empty?
344
- related_logs = log[: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
- # Calculate query statistics (use original logs for stats)
353
- query_stats = calculate_query_stats(related_logs)
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
- summary_line = " #{query_stats[:total_queries]} queries"
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
- summary_line += " (#{query_stats[:total_time]}ms total"
366
- if query_stats[:cached_queries] > 0
367
- summary_line += ", #{query_stats[:cached_queries]} cached"
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
- summary_line += " (#{query_stats[:cached_queries]} cached)"
363
+ summary_parts << "(#{query_stats[:cached_queries]} cached)"
372
364
  end
373
- lines << {text: summary_line, color: color_pair(2)}
374
-
375
- # Breakdown by operation type
376
- breakdown_parts = []
377
- breakdown_parts << "#{query_stats[:select]} SELECT" if query_stats[:select] > 0
378
- breakdown_parts << "#{query_stats[:insert]} INSERT" if query_stats[:insert] > 0
379
- breakdown_parts << "#{query_stats[:update]} UPDATE" if query_stats[:update] > 0
380
- breakdown_parts << "#{query_stats[:delete]} DELETE" if query_stats[:delete] > 0
381
- breakdown_parts << "#{query_stats[:transaction]} TRANSACTION" if query_stats[:transaction] > 0
382
- breakdown_parts << "#{query_stats[:cache]} CACHE" if query_stats[:cache] > 0
383
-
384
- if !breakdown_parts.empty?
385
- breakdown_line = " " + breakdown_parts.join(", ")
386
- lines << {text: breakdown_line, color: color_pair(2)}
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[:type]
403
+ case related.type
412
404
  when :sql, :cache
413
- render_padded_text_with_spacing(related[:content], lines, extra_empty_lines: 0)
405
+ render_padded_text_with_spacing(related.content, lines, extra_empty_lines: 0)
414
406
  else
415
- render_padded_text_with_spacing(related[:content], lines, extra_empty_lines: 1)
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 calculate_query_stats(related_logs)
413
+ def build_query_stats_from_request(request)
414
+ # Use memoized methods from request object for better performance
422
415
  stats = {
423
- total_queries: 0,
424
- total_time: 0.0,
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
- related_logs.each do |log|
435
- next unless [:sql, :cache].include?(log[:type])
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
- # Categorize by SQL operation and check for cache
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
- if log[:content] && state.detail_filter.matches?(log[:content])
491
- matched_indices.add(index)
492
-
493
- # Add context lines based on log type
494
- case log[:type]
495
- when :sql_call_line
496
- # If match is a sql_call_line, include the line below (the actual SQL query)
497
- if index + 1 < related_logs.size
498
- matched_indices.add(index + 1)
499
- end
500
- when :sql, :cache
501
- # If match is a sql or cache, include the line above (the call stack line)
502
- if index > 0 && related_logs[index - 1][:type] == :sql_call_line
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.to_a.sort.map { |index| related_logs[index] }
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 do
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 = [state.detail_scroll_offset, max_scroll].min
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 request_index == state.selected
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, request_index)
119
- draw_path_column(request, request_index)
120
- draw_status_column(request, request_index)
121
- draw_duration_column(request, request_index)
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, request_index)
125
- method_color = method_color_for(request.method)
125
+ def draw_method_badge(request, is_selected)
126
+ method_text = " #{request.method.ljust(7)} "
126
127
 
127
- if request_index == state.selected
128
- log_win.attron(color_pair(10) | A_DIM) do
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
- log_win.attron(color_pair(method_color) | A_BOLD) do
133
- log_win.addstr(" #{request.method.ljust(7)} ")
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, request_index)
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 request_index == state.selected
143
- log_win.attron(color_pair(10) | A_DIM) do
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(path.ljust(path_width))
144
+ log_win.addstr(path_text)
148
145
  end
149
146
  end
150
147
 
151
- def draw_status_column(request, request_index)
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
- if request.status
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
- status_text = request.status.to_s.rjust(3)
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, request_index)
168
- duration_col_start = screen.panel_width - 9
163
+ def draw_duration_column(request, is_selected)
164
+ return unless request.duration
169
165
 
170
- if request.duration
171
- duration_text = "#{request.duration.to_i}ms".ljust(6)
166
+ duration_col_start = screen.panel_width - 9
167
+ duration_text = "#{request.duration.to_i}ms".ljust(6) + " "
172
168
 
173
- log_win.setpos(log_win.cury, duration_col_start)
174
- if request_index == state.selected
175
- log_win.attron(color_pair(10) | A_DIM) { log_win.addstr(duration_text + " ") }
176
- else
177
- log_win.attron(A_DIM) { log_win.addstr(duration_text + " ") }
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)
@@ -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
@@ -65,6 +65,10 @@ module LogBench
65
65
  ::File.mtime(path)
66
66
  end
67
67
 
68
+ def mark_as_read!
69
+ self.last_position = size
70
+ end
71
+
68
72
  private
69
73
 
70
74
  attr_writer :path, :last_position
@@ -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 == :sql
27
+ return unless [:sql, :cache].include?(entry.type)
27
28
 
28
- new(raw_line)
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
- return 0.0 unless timing
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 slow?(threshold_ms = 100)
74
- duration_ms > threshold_ms
74
+ def cached?
75
+ @cached
75
76
  end
76
77
 
77
- def to_h
78
- super.merge(
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
- clean_content = remove_ansi_codes(content)
103
- self.timing = extract_timing(clean_content)
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(text)
108
- match = text.match(/\(([0-9.]+ms)\)/)
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(text)
113
- SQL_OPERATIONS.find { |op| text.include?(op) }
108
+ def extract_operation
109
+ SQL_OPERATIONS.find { |op| clean_content.include?(op) }
114
110
  end
115
111
 
116
- def remove_ansi_codes(text)
117
- text.gsub(/\e\[[0-9;]*m/, "")
112
+ def clean_content
113
+ @clean_content ||= content&.gsub(/\e\[[0-9;]*m/, "") || ""
118
114
  end
119
115
 
120
- def has_ansi_codes?(text)
121
- text.match?(/\e\[[0-9;]*m/)
116
+ def has_ansi_codes?
117
+ @has_ansi_codes ||= content&.match?(/\e\[[0-9;]*m/) || false
122
118
  end
123
119
 
124
- private
120
+ def calculate_duration_ms
121
+ return 0.0 unless timing
125
122
 
126
- attr_accessor :operation
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
- related_logs << log_entry if log_entry.related_log?
25
- self.related_logs = related_logs.sort_by(&:timestamp)
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?(CacheEntry) }
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBench
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.5"
5
5
  end
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.3
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-08 00:00:00.000000000 Z
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