log_bench 0.2.11 → 0.3.1

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: 988ff524c8e07d752d9105bbe0d58008d8209fb0b58d31e0d398cb574ed97b44
4
- data.tar.gz: 02c7efab76e2203c1fd77d55ad2c30ecdbb50362eae751e83e7616dd0f4919ca
3
+ metadata.gz: 2c3190dc4200c7efbd8150e719d16f20cef759621301a498d943958b5b2e805f
4
+ data.tar.gz: c6dc926d7593d396cc9b4eb298a81009080545fafa56a68031728970c304117d
5
5
  SHA512:
6
- metadata.gz: 9f84d49ef55a3d88345ce5fee35b645298569743cbd04a210e459bef19a2fa39cf212058db3e11665c3e4e0827db65ebbfbdaa1c4d67439f41a710af63bd67fd
7
- data.tar.gz: 659277186d2eac29a7aebd33b15a07237701fb260e0a1890d6f6cd4bdb909a90b9e56fa9b5815dee38b17aaba5c08ec51421cc1013f9e9aef90eaa888c619a5f
6
+ metadata.gz: 205d48a20a22a4fe91aaf5b31daacc304bee3a95431c38e90db5d393fe7f7c3deeec4e3987594a62caa5f5bed5fb49e512ef454db1440561cb9d7c3a57bcf9c6
7
+ data.tar.gz: 16e125666602e1eee53f84542ae82cf6da79d18dd3e03f6ee39090e999e441399c7f8ed7f7a041158c412bce38ec61dc76cdc70ee5eafbf36873f36bb050e907
data/README.md CHANGED
@@ -118,6 +118,8 @@ log_bench log/development.log
118
118
  - **Auto-scroll**: `a` to toggle auto-scroll mode
119
119
  - **Copy**: `y` to copy the selected item to clipboard (request details or SQL query)
120
120
  - **Text selection**: `t` to toggle text selection mode (enables mouse text selection)
121
+ - **Clear requests**: `Ctrl+L` to clear all requests from memory (preserves current position)
122
+ - **Undo clear**: `Ctrl+R` to restore previously cleared requests + any new requests (restores exact position)
121
123
  - **Quit**: `q` to exit
122
124
 
123
125
  ### Filtering
@@ -12,6 +12,9 @@ module LogBench
12
12
  if system("which pbcopy > /dev/null 2>&1")
13
13
  # macOS
14
14
  IO.popen("pbcopy", "w") { |io| io.write(text) }
15
+ elsif system("which wl-copy > /dev/null 2>&1")
16
+ # Linux with wl-copy (wayland)
17
+ IO.popen("wl-copy", "w") { |io| io.write(text) }
15
18
  elsif system("which xclip > /dev/null 2>&1")
16
19
  # Linux with xclip
17
20
  IO.popen("xclip -selection clipboard", "w") { |io| io.write(text) }
@@ -146,7 +146,8 @@ module LogBench
146
146
  def sql_query?(text)
147
147
  # Check for common SQL keywords that indicate this is a SQL query
148
148
  sql_keywords = %w[SELECT INSERT UPDATE DELETE TRANSACTION BEGIN COMMIT ROLLBACK SAVEPOINT]
149
- sql_keywords.any? { |keyword| text.upcase.include?(keyword) }
149
+ pattern = /\b(#{sql_keywords.join("|")})\b/i
150
+ text.match?(pattern)
150
151
  end
151
152
  end
152
153
  end
@@ -12,6 +12,8 @@ module LogBench
12
12
  CTRL_D = 4 # Half page down
13
13
  CTRL_U = 21 # Half page up
14
14
  CTRL_C = 3 # Quit
15
+ CTRL_L = 12 # Clear requests
16
+ CTRL_R = 18 # Undo clear requests (restore)
15
17
  ESC = 27 # Escape
16
18
 
17
19
  # UI constants
@@ -134,11 +136,7 @@ module LogBench
134
136
  when "f", "F", "/"
135
137
  state.enter_filter_mode
136
138
  when "c", "C"
137
- if state.left_pane_focused?
138
- state.clear_filter
139
- else
140
- state.clear_detail_filter
141
- end
139
+ state.clear_filter
142
140
  when "s", "S"
143
141
  state.cycle_sort_mode
144
142
  when "q", "Q", CTRL_C
@@ -148,6 +146,10 @@ module LogBench
148
146
  screen.turn_text_selection_mode(state.text_selection_mode?)
149
147
  when "y", "Y"
150
148
  copy_handler.copy_to_clipboard
149
+ when CTRL_L
150
+ state.clear_requests
151
+ when CTRL_R
152
+ state.undo_clear_requests
151
153
  when ESC
152
154
  handle_escape
153
155
  end
@@ -84,7 +84,9 @@ module LogBench
84
84
 
85
85
  header_win.setpos(3, 2)
86
86
  header_win.attron(A_DIM) do
87
- header_win.addstr("←→/hl:Switch Pane | ↑↓/jk/Click:Navigate | g/G:Top/End | y:Copy highlighted")
87
+ header_win.addstr("←→/hl:Pane | ↑↓/jk:Navigate | g/G:Top/End | y:Copy highlighted | Ctrl+L:Clear | Ctrl+R:Restore(")
88
+ header_win.attron(color_pair(3)) { header_win.addstr(state.can_undo_clear? ? "READY" : "N/A") }
89
+ header_win.addstr(")")
88
90
  end
89
91
  end
90
92
 
@@ -82,22 +82,23 @@ module LogBench
82
82
 
83
83
  def setup_colors
84
84
  start_color
85
+ use_default_colors
85
86
  cbreak
86
87
  noecho
87
88
  curs_set(0)
88
89
  stdscr.keypad(true)
89
90
  stdscr.timeout = INPUT_TIMEOUT_MS
90
91
 
91
- # Define color pairs
92
- init_pair(HEADER_CYAN, COLOR_CYAN, COLOR_BLACK) # Header/Cyan
93
- init_pair(DEFAULT_WHITE, COLOR_WHITE, COLOR_BLACK) # Default/White
94
- init_pair(SUCCESS_GREEN, COLOR_GREEN, COLOR_BLACK) # GET/Success/Green
95
- init_pair(WARNING_YELLOW, COLOR_YELLOW, COLOR_BLACK) # POST/Warning/Yellow
96
- init_pair(INFO_BLUE, COLOR_BLUE, COLOR_BLACK) # PUT/Blue
97
- init_pair(ERROR_RED, COLOR_RED, COLOR_BLACK) # DELETE/Error/Red
98
- init_pair(BRIGHT_WHITE, COLOR_WHITE, COLOR_BLACK) # Bold/Bright white
99
- init_pair(BLACK, COLOR_BLACK, COLOR_BLACK) # Black
100
- init_pair(MAGENTA, COLOR_MAGENTA, COLOR_BLACK) # Magenta
92
+ # Define color pairs with transparent background (-1)
93
+ init_pair(HEADER_CYAN, COLOR_CYAN, -1) # Header/Cyan
94
+ init_pair(DEFAULT_WHITE, COLOR_WHITE, -1) # Default/White
95
+ init_pair(SUCCESS_GREEN, COLOR_GREEN, -1) # GET/Success/Green
96
+ init_pair(WARNING_YELLOW, COLOR_YELLOW, -1) # POST/Warning/Yellow
97
+ init_pair(INFO_BLUE, COLOR_BLUE, -1) # PUT/Blue
98
+ init_pair(ERROR_RED, COLOR_RED, -1) # DELETE/Error/Red
99
+ init_pair(BRIGHT_WHITE, COLOR_WHITE, -1) # Bold/Bright white
100
+ init_pair(BLACK, COLOR_BLACK, -1) # Black
101
+ init_pair(MAGENTA, COLOR_MAGENTA, -1) # Magenta
101
102
  init_pair(SELECTION_HIGHLIGHT, COLOR_BLACK, COLOR_CYAN) # Selection highlighting
102
103
  end
103
104
 
@@ -3,7 +3,7 @@
3
3
  module LogBench
4
4
  module App
5
5
  class State
6
- attr_reader :main_filter, :sort, :detail_filter
6
+ attr_reader :main_filter, :sort, :detail_filter, :cleared_requests
7
7
  attr_accessor :requests, :auto_scroll, :scroll_offset, :selected, :detail_scroll_offset, :detail_selected_entry, :text_selection_mode, :update_available, :update_version
8
8
 
9
9
  def initialize
@@ -21,6 +21,7 @@ module LogBench
21
21
  self.sort = Sort.new
22
22
  self.update_available = false
23
23
  self.update_version = nil
24
+ self.cleared_requests = nil
24
25
  end
25
26
 
26
27
  def running?
@@ -58,6 +59,14 @@ module LogBench
58
59
  end
59
60
 
60
61
  def clear_filter
62
+ if left_pane_focused?
63
+ clear_requests_filter
64
+ else
65
+ clear_detail_filter
66
+ end
67
+ end
68
+
69
+ def clear_requests_filter
61
70
  main_filter.clear
62
71
  self.selected = 0
63
72
  self.scroll_offset = 0
@@ -69,6 +78,44 @@ module LogBench
69
78
  self.detail_selected_entry = 0
70
79
  end
71
80
 
81
+ def clear_requests
82
+ if cleared_requests
83
+ cleared_requests[:requests] += requests
84
+ else
85
+ self.cleared_requests = {
86
+ requests: requests,
87
+ selected: selected,
88
+ scroll_offset: scroll_offset,
89
+ detail_scroll_offset: detail_scroll_offset,
90
+ detail_selected_entry: detail_selected_entry
91
+ }
92
+ end
93
+
94
+ self.requests = []
95
+ self.selected = 0
96
+ self.scroll_offset = 0
97
+ self.detail_scroll_offset = 0
98
+ self.detail_selected_entry = 0
99
+ end
100
+
101
+ def undo_clear_requests
102
+ return unless cleared_requests
103
+
104
+ # Append any new requests that came in after the clear to the restored requests
105
+ restored_requests = cleared_requests[:requests] + requests
106
+
107
+ self.requests = restored_requests
108
+ self.selected = cleared_requests[:selected]
109
+ self.scroll_offset = cleared_requests[:scroll_offset]
110
+ self.detail_scroll_offset = cleared_requests[:detail_scroll_offset]
111
+ self.detail_selected_entry = cleared_requests[:detail_selected_entry]
112
+ self.cleared_requests = nil
113
+ end
114
+
115
+ def can_undo_clear?
116
+ !cleared_requests.nil?
117
+ end
118
+
72
119
  def cycle_sort_mode
73
120
  sort.cycle
74
121
  end
@@ -235,7 +282,7 @@ module LogBench
235
282
  private
236
283
 
237
284
  attr_accessor :focused_pane, :running
238
- attr_writer :main_filter, :detail_filter, :sort
285
+ attr_writer :main_filter, :detail_filter, :sort, :cleared_requests
239
286
  end
240
287
  end
241
288
  end
@@ -5,22 +5,12 @@ module LogBench
5
5
  class Entry
6
6
  attr_reader :type, :raw_line, :request_id, :timestamp, :content, :timing
7
7
 
8
- def initialize(raw_line)
9
- self.raw_line = raw_line.strip
10
- self.timestamp = Time.now
11
- self.type = :unknown
12
- parse!
13
- end
14
-
15
- def self.build(raw_line)
16
- new(raw_line) if parseable?(raw_line)
17
- end
18
-
19
- def self.parseable?(line)
20
- data = JSON.parse(line.strip)
21
- data.is_a?(Hash)
22
- rescue JSON::ParserError
23
- false
8
+ def initialize(json_data)
9
+ self.json_data = json_data
10
+ self.timestamp = parse_timestamp(json_data["timestamp"])
11
+ self.request_id = json_data["request_id"]
12
+ self.content = json_data["message"] || ""
13
+ self.type = :other
24
14
  end
25
15
 
26
16
  def http_request?
@@ -33,60 +23,8 @@ module LogBench
33
23
 
34
24
  private
35
25
 
36
- attr_writer :type, :raw_line, :timestamp, :request_id, :content, :timing
37
-
38
- def parse!
39
- parse_json
40
- end
41
-
42
- def parse_json
43
- data = JSON.parse(raw_line)
44
- return false unless data.is_a?(Hash)
45
-
46
- # extract_from_json returns false if log should be discarded
47
- extract_from_json(data)
48
- rescue JSON::ParserError
49
- false
50
- end
51
-
52
- def extract_from_json(data)
53
- # Discard logs without request_id - they can't be correlated
54
- return false unless data["request_id"]
55
-
56
- self.timestamp = parse_timestamp(data["timestamp"])
57
- self.request_id = data["request_id"]
58
- self.content = data["message"] || ""
59
- self.type = determine_json_type(data)
60
- true
61
- end
62
-
63
- def determine_json_type(data)
64
- return :http_request if lograge_request?(data)
65
- return :cache if cache_message?(data)
66
- return :sql if sql_message?(data)
67
- return :sql_call_line if call_stack_message?(data)
68
-
69
- :other
70
- end
71
-
72
- def lograge_request?(data)
73
- data["method"] && data["path"] && data["status"]
74
- end
75
-
76
- def sql_message?(data)
77
- message = data["message"] || ""
78
- %w[SELECT INSERT UPDATE DELETE TRANSACTION BEGIN COMMIT ROLLBACK SAVEPOINT].any? { |op| message.include?(op) }
79
- end
80
-
81
- def cache_message?(data)
82
- message = data["message"] || ""
83
- message.include?("CACHE")
84
- end
85
-
86
- def call_stack_message?(data)
87
- message = data["message"] || ""
88
- message.include?("↳")
89
- end
26
+ attr_writer :type, :timestamp, :request_id, :content, :timing
27
+ attr_accessor :json_data
90
28
 
91
29
  def parse_timestamp(timestamp_str)
92
30
  return Time.now unless timestamp_str
@@ -3,7 +3,8 @@
3
3
  module LogBench
4
4
  module Log
5
5
  class File
6
- attr_reader :path, :last_position
6
+ INACTIVE_SLEEP_TIME = 0.5
7
+ ACTIVE_SLEEP_TIME = 0.01
7
8
 
8
9
  def initialize(path)
9
10
  self.path = find_log_file(path)
@@ -15,42 +16,36 @@ module LogBench
15
16
  collection.requests
16
17
  end
17
18
 
18
- def entries
19
- collection.entries
20
- end
19
+ def watch
20
+ loop do
21
+ new_lines = read_new_lines
21
22
 
22
- def collection
23
- @collection ||= Collection.new(lines)
24
- end
23
+ if new_lines.empty?
24
+ sleep INACTIVE_SLEEP_TIME
25
+ next
26
+ end
25
27
 
26
- def lines
27
- @lines ||= read_lines
28
- end
28
+ new_collection = Collection.new(new_lines)
29
+ yield new_collection unless new_collection.empty?
29
30
 
30
- def reload!
31
- self.lines = nil
32
- self.collection = nil
33
- self.last_position = 0
31
+ sleep ACTIVE_SLEEP_TIME
32
+ end
34
33
  end
35
34
 
36
- def tail(max_lines = 1000)
37
- all_lines = read_lines
38
- recent_lines = all_lines.last(max_lines)
39
- Collection.new(recent_lines)
35
+ def mark_as_read!
36
+ self.last_position = size
40
37
  end
41
38
 
42
- def watch(&block)
43
- return enum_for(:watch) unless block_given?
39
+ private
44
40
 
45
- loop do
46
- new_lines = read_new_lines
47
- next if new_lines.empty?
41
+ attr_accessor :path, :last_position
48
42
 
49
- new_collection = Collection.new(new_lines)
50
- yield new_collection unless new_collection.empty?
43
+ def collection
44
+ @collection ||= Collection.new(lines)
45
+ end
51
46
 
52
- sleep 0.5
53
- end
47
+ def lines
48
+ @lines ||= read_lines
54
49
  end
55
50
 
56
51
  def size
@@ -61,18 +56,6 @@ module LogBench
61
56
  ::File.exist?(path)
62
57
  end
63
58
 
64
- def mtime
65
- ::File.mtime(path)
66
- end
67
-
68
- def mark_as_read!
69
- self.last_position = size
70
- end
71
-
72
- private
73
-
74
- attr_writer :path, :last_position
75
-
76
59
  def read_lines
77
60
  return [] unless exist?
78
61
 
@@ -4,10 +4,13 @@ module LogBench
4
4
  module Log
5
5
  class Parser
6
6
  def self.parse_line(raw_line)
7
- return unless Entry.parseable?(raw_line)
7
+ clean_line = raw_line.encode("UTF-8", invalid: :replace, undef: :replace, replace: "").strip
8
+ data = JSON.parse(clean_line)
9
+ return unless data.is_a?(Hash)
8
10
 
9
- entry = Entry.new(raw_line)
10
- build_specific_entry(entry)
11
+ build_specific_entry(data)
12
+ rescue JSON::ParserError
13
+ nil
11
14
  end
12
15
 
13
16
  def self.parse_lines(lines)
@@ -19,16 +22,18 @@ module LogBench
19
22
  build_requests_from_groups(grouped)
20
23
  end
21
24
 
22
- def self.build_specific_entry(entry)
23
- case entry.type
25
+ def self.build_specific_entry(data)
26
+ case determine_json_type(data)
24
27
  when :http_request
25
- Request.build(entry.raw_line)
26
- when :sql, :cache
27
- QueryEntry.build(entry.raw_line)
28
+ Request.new(data)
29
+ when :sql
30
+ QueryEntry.new(data, cached: false)
31
+ when :cache
32
+ QueryEntry.new(data, cached: true)
28
33
  when :sql_call_line
29
- CallLineEntry.build(entry.raw_line)
34
+ CallLineEntry.new(data)
30
35
  else
31
- entry
36
+ Entry.new(data)
32
37
  end
33
38
  end
34
39
 
@@ -57,6 +62,34 @@ module LogBench
57
62
  def self.find_related_logs(entries)
58
63
  entries.reject { |entry| entry.is_a?(Request) }
59
64
  end
65
+
66
+ def self.determine_json_type(data)
67
+ return :http_request if lograge_request?(data)
68
+ return :cache if cache_message?(data)
69
+ return :sql if sql_message?(data)
70
+ return :sql_call_line if call_stack_message?(data)
71
+
72
+ :other
73
+ end
74
+
75
+ def self.lograge_request?(data)
76
+ data["method"] && data["path"] && data["status"]
77
+ end
78
+
79
+ def self.sql_message?(data)
80
+ message = data["message"] || ""
81
+ %w[SELECT INSERT UPDATE DELETE TRANSACTION BEGIN COMMIT ROLLBACK SAVEPOINT].any? { |op| message.include?(op) }
82
+ end
83
+
84
+ def self.cache_message?(data)
85
+ message = data["message"] || ""
86
+ message.include?("CACHE")
87
+ end
88
+
89
+ def self.call_stack_message?(data)
90
+ message = data["message"] || ""
91
+ message.include?("↳")
92
+ end
60
93
  end
61
94
  end
62
95
  end
@@ -14,21 +14,11 @@ 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, cached: false)
18
- super(raw_line)
17
+ def initialize(json_data, cached: false)
18
+ super(json_data)
19
19
  self.type = cached ? :cache : :sql
20
- @cached = cached
21
- end
22
-
23
- def self.build(raw_line)
24
- return unless parseable?(raw_line)
25
-
26
- entry = Entry.new(raw_line)
27
- return unless [:sql, :cache].include?(entry.type)
28
-
29
- # Create QueryEntry for both SQL and CACHE entries
30
- cached = entry.type == :cache
31
- new(raw_line, cached: cached)
20
+ self.timing = extract_timing
21
+ self.operation = extract_operation
32
22
  end
33
23
 
34
24
  def duration_ms
@@ -72,7 +62,7 @@ module LogBench
72
62
  end
73
63
 
74
64
  def cached?
75
- @cached
65
+ type == :cache
76
66
  end
77
67
 
78
68
  def hit?
@@ -81,24 +71,7 @@ module LogBench
81
71
 
82
72
  private
83
73
 
84
- attr_accessor :operation
85
-
86
- def extract_from_json(data)
87
- # Call parent method which checks for request_id
88
- return false unless super
89
-
90
- message = data["message"] || ""
91
- return false unless sql_message?(data) || cache_message?(data)
92
-
93
- self.content = message.strip
94
- extract_timing_and_operation
95
- true
96
- end
97
-
98
- def extract_timing_and_operation
99
- self.timing = extract_timing
100
- self.operation = extract_operation
101
- end
74
+ attr_accessor :operation, :cached
102
75
 
103
76
  def extract_timing
104
77
  match = clean_content.match(/\(([0-9.]+ms)\)/)
@@ -5,18 +5,17 @@ module LogBench
5
5
  class Request < Entry
6
6
  attr_reader :method, :path, :status, :duration, :controller, :action, :params, :related_logs
7
7
 
8
- def initialize(raw_line)
8
+ def initialize(json_data)
9
9
  super
10
+ self.type = :http_request
10
11
  self.related_logs = []
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.http_request?
18
-
19
- new(raw_line)
12
+ self.method = json_data["method"]
13
+ self.path = json_data["path"]
14
+ self.status = json_data["status"]
15
+ self.duration = json_data["duration"]
16
+ self.controller = json_data["controller"]
17
+ self.action = json_data["action"]
18
+ self.params = parse_params(json_data["params"])
20
19
  end
21
20
 
22
21
  def add_related_log(log_entry)
@@ -79,20 +78,6 @@ module LogBench
79
78
  @cached_query_count = nil
80
79
  end
81
80
 
82
- def extract_from_json(data)
83
- return false unless super
84
-
85
- self.method = data["method"]
86
- self.path = data["path"]
87
- self.status = data["status"]
88
- self.duration = data["duration"]
89
- self.controller = data["controller"]
90
- self.action = data["action"]
91
- self.request_id = data["request_id"]
92
- self.params = parse_params(data["params"])
93
- true
94
- end
95
-
96
81
  def parse_params(params_data)
97
82
  return nil unless params_data
98
83
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBench
4
- VERSION = "0.2.11"
4
+ VERSION = "0.3.1"
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.11
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamín Silva
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-09-03 00:00:00.000000000 Z
10
+ date: 2025-09-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk