log_bench 0.1.3 → 0.1.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: 2984d376970a351d650b414fe6889976c1a9f0b3a572942784e007e974f5feaf
4
- data.tar.gz: 63f7d6f0e7fcf52708434df60eb60f75990d3a8d05e136d76c41440b32e7542f
3
+ metadata.gz: eb8aba2b715d494f771f7bf0b1dda90719ae1a72e36823e86832f6ccbd7966b3
4
+ data.tar.gz: e7f5a7f4322778e74a4de3f33603c5de448e2e021c78e1506cf97507226ffa80
5
5
  SHA512:
6
- metadata.gz: f1792820588962d97177bacfed32730085829c5a9215cc2e4bd802d52c602023944e6b3d6ab47d118662550dce7c915a151d0c100b99549ada3a7533e50c7e85
7
- data.tar.gz: e66b1b2e7075d2a26775f6b87a6348b084a56abeb10b6e27a7917a1f434f1f240d6a484557fb0c298bab7b258c307002347c16539ecbbdd638cf2c79c00048f3
6
+ metadata.gz: 966d732d29bdbeaf30f1428b1f26126733120d2922c4a711a74e629fe78eae088702a51a6d20c311f097e98484bb93334a5eb8387b412b31449934e205013604
7
+ data.tar.gz: 628a28c650da6ed06b837397e3f79c09fc115ad36c2082ebb8dc9207a87e27034b04696a8d3462b2bb6be278a611ae08909ee4ecac3782509dce908dea5dda45
data/README.md CHANGED
@@ -107,7 +107,7 @@ log_bench log/development.log
107
107
 
108
108
  ### Filtering
109
109
 
110
- Press `f` to open the filter dialog.
110
+ Press `f` to open the filter dialog.
111
111
 
112
112
  In the left pane you can filter by:
113
113
 
@@ -233,5 +233,3 @@ This gem is available as open source under the terms of the [MIT License](LICENS
233
233
  ## Support
234
234
 
235
235
  - 🐛 **Bug reports**: [GitHub Issues](https://github.com/silva96/log_bench/issues)
236
- - 💡 **Feature requests**: [GitHub Discussions](https://github.com/silva96/log_bench/discussions)
237
- - 📖 **Documentation**: [GitHub Wiki](https://github.com/silva96/log_bench/wiki)
data/exe/log_bench CHANGED
@@ -72,6 +72,7 @@ rescue => e
72
72
  puts " - Log file doesn't exist or is empty"
73
73
  puts " - Lograge not configured (see README.md for setup)"
74
74
  puts " - Log format not supported (LogBench requires lograge JSON format)"
75
+ puts " Error backtrace: \n #{e.backtrace.join("\n")}"
75
76
  puts
76
77
  puts "For help: log_bench --help"
77
78
  exit 1
@@ -17,15 +17,25 @@ module LogBench
17
17
  # UI constants
18
18
  DEFAULT_VISIBLE_HEIGHT = 20
19
19
 
20
- def initialize(state)
20
+ def initialize(state, screen, renderer = nil)
21
21
  self.state = state
22
+ self.screen = screen
23
+ self.renderer = renderer
24
+ self.mouse_handler = MouseHandler.new(state, screen)
22
25
  end
23
26
 
24
27
  def handle_input
25
28
  ch = getch
26
29
  return if ch == -1 || ch.nil?
27
30
 
28
- if filter_mode_active?
31
+ # Check if update modal should handle input first
32
+ if renderer&.handle_modal_input(ch)
33
+ return
34
+ end
35
+
36
+ if ch == KEY_MOUSE
37
+ mouse_handler.handle_mouse_input
38
+ elsif filter_mode_active?
29
39
  handle_filter_input(ch)
30
40
  else
31
41
  handle_navigation_input(ch)
@@ -34,7 +44,7 @@ module LogBench
34
44
 
35
45
  private
36
46
 
37
- attr_accessor :state
47
+ attr_accessor :state, :screen, :renderer, :mouse_handler
38
48
 
39
49
  def filter_mode_active?
40
50
  state.filter_mode || state.detail_filter_mode
@@ -44,10 +54,10 @@ module LogBench
44
54
  case ch
45
55
  when 10, 13, 27
46
56
  state.exit_filter_mode
47
- when Curses::KEY_UP, "k", "K"
57
+ when KEY_UP, "k", "K"
48
58
  state.exit_filter_mode
49
59
  state.navigate_up
50
- when Curses::KEY_DOWN, "j", "J"
60
+ when KEY_DOWN, "j", "J"
51
61
  state.exit_filter_mode
52
62
  state.navigate_down
53
63
  when 127, 8 # Backspace
@@ -89,15 +99,15 @@ module LogBench
89
99
 
90
100
  def handle_navigation_input(ch)
91
101
  case ch
92
- when Curses::KEY_LEFT, "h", "H"
102
+ when KEY_LEFT, "h", "H"
93
103
  state.switch_to_left_pane
94
- when Curses::KEY_RIGHT, "l", "L"
104
+ when KEY_RIGHT, "l", "L"
95
105
  state.switch_to_right_pane
96
106
  when TAB
97
107
  toggle_pane_focus
98
- when Curses::KEY_UP, "k", "K"
108
+ when KEY_UP, "k", "K"
99
109
  handle_up_navigation
100
- when Curses::KEY_DOWN, "j", "J"
110
+ when KEY_DOWN, "j", "J"
101
111
  handle_down_navigation
102
112
  when CTRL_F
103
113
  handle_page_down
@@ -125,6 +135,9 @@ module LogBench
125
135
  state.cycle_sort_mode
126
136
  when "q", "Q", CTRL_C
127
137
  state.stop!
138
+ when "t", "T"
139
+ state.toggle_text_selection_mode
140
+ screen.turn_text_selection_mode(state.text_selection_mode?)
128
141
  when ESC
129
142
  handle_escape
130
143
  end
@@ -11,7 +11,7 @@ module LogBench
11
11
  DEFAULT_LOG_PATHS = %w[log/development.log].freeze
12
12
 
13
13
  # Timing
14
- MAIN_LOOP_SLEEP_INTERVAL = 0.05
14
+ MAIN_LOOP_SLEEP_INTERVAL = 1.0 / 1000 # 1ms
15
15
 
16
16
  # Error messages
17
17
  LOG_FILE_NOT_FOUND = "Error: No log file found at %s!"
@@ -27,6 +27,7 @@ module LogBench
27
27
  setup_screen
28
28
  setup_components
29
29
  load_initial_data
30
+ check_for_updates
30
31
  initial_draw
31
32
  start_monitoring
32
33
  main_loop
@@ -55,8 +56,8 @@ module LogBench
55
56
  end
56
57
 
57
58
  def setup_components
58
- self.input_handler = InputHandler.new(state)
59
59
  self.renderer = Renderer::Main.new(screen, state)
60
+ self.input_handler = InputHandler.new(state, screen, renderer)
60
61
  end
61
62
 
62
63
  def load_initial_data
@@ -64,6 +65,11 @@ module LogBench
64
65
  state.requests = log_file.requests
65
66
  end
66
67
 
68
+ def check_for_updates
69
+ latest_version = VersionChecker.check_for_update
70
+ state.set_update_available(latest_version) if latest_version
71
+ end
72
+
67
73
  def initial_draw
68
74
  renderer.draw
69
75
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogBench
4
+ module App
5
+ class MouseHandler
6
+ include Curses
7
+
8
+ # UI constants
9
+ DEFAULT_VISIBLE_HEIGHT = 20
10
+
11
+ def initialize(state, screen)
12
+ self.state = state
13
+ self.screen = screen
14
+ end
15
+
16
+ def handle_mouse_input
17
+ with_warnings_suppressed do
18
+ mouse_event = getmouse
19
+
20
+ return unless mouse_event
21
+
22
+ if mouse_event.bstate & BUTTON1_CLICKED != 0
23
+ handle_mouse_click(mouse_event.x, mouse_event.y)
24
+ end
25
+ end
26
+ rescue
27
+ nil
28
+ end
29
+
30
+ private
31
+
32
+ attr_accessor :state, :screen
33
+
34
+ def handle_mouse_click(x, y)
35
+ if click_in_left_pane?(x, y)
36
+ # Switch to left pane if not already focused
37
+ state.switch_to_left_pane unless state.left_pane_focused?
38
+
39
+ # Convert click coordinates to request index
40
+ request_index = click_to_request_index(y)
41
+ return unless request_index
42
+
43
+ max_index = state.filtered_requests.size - 1
44
+ state.selected = [request_index, max_index].min
45
+ state.auto_scroll = false
46
+ elsif click_in_right_pane?(x, y)
47
+ # Switch to right pane
48
+ state.switch_to_right_pane unless state.right_pane_focused?
49
+ end
50
+ end
51
+
52
+ def click_in_left_pane?(x, y)
53
+ # Left pane spans from x=0 to panel_width
54
+ # Header takes up first HEADER_HEIGHT lines
55
+ # Request list starts at HEADER_HEIGHT + 1 (accounting for border)
56
+ panel_width = screen.panel_width
57
+ header_height = 5 # Screen::HEADER_HEIGHT
58
+
59
+ x >= 0 && x < panel_width && y > header_height
60
+ end
61
+
62
+ def click_in_right_pane?(x, y)
63
+ # Right pane starts after left panel + border width
64
+ # From Screen: panel_width + PANEL_BORDER_WIDTH
65
+ panel_width = screen.panel_width
66
+ border_width = 3 # Screen::PANEL_BORDER_WIDTH
67
+ header_height = 5 # Screen::HEADER_HEIGHT
68
+
69
+ right_pane_start = panel_width + border_width
70
+
71
+ x >= right_pane_start && y > header_height
72
+ end
73
+
74
+ def click_to_request_index(y)
75
+ # Header takes up first 5 lines
76
+ # Request list has 1 line border at top, then 1 line for column headers
77
+ # So actual request rows start at y = 7 (5 header + 1 border + 1 column header)
78
+ header_height = 5
79
+ list_header_offset = 2 # border + column header
80
+
81
+ row_in_list = y - header_height - list_header_offset
82
+ return nil if row_in_list < 0
83
+
84
+ # Convert to actual request index accounting for scroll
85
+ state.scroll_offset + row_in_list
86
+ end
87
+
88
+ def visible_height
89
+ # Approximate visible height for calculations
90
+ DEFAULT_VISIBLE_HEIGHT
91
+ end
92
+
93
+ def with_warnings_suppressed
94
+ old_verbose = $VERBOSE
95
+ $VERBOSE = nil
96
+ yield
97
+ ensure
98
+ $VERBOSE = old_verbose
99
+ end
100
+ end
101
+ end
102
+ end
@@ -77,12 +77,14 @@ module LogBench
77
77
  header_win.attron(color_pair(3)) { header_win.addstr(state.auto_scroll ? "ON" : "OFF") }
78
78
  header_win.addstr(") | f:Filter | c:Clear filter | s:Sort(")
79
79
  header_win.attron(color_pair(3)) { header_win.addstr(state.sort.display_name) }
80
+ header_win.addstr(") | t:Text selection(")
81
+ header_win.attron(color_pair(3)) { header_win.addstr(state.text_selection_mode? ? "ON" : "OFF") }
80
82
  header_win.addstr(") | q:Quit")
81
83
  end
82
84
 
83
85
  header_win.setpos(3, 2)
84
86
  header_win.attron(A_DIM) do
85
- header_win.addstr("←→/hl:Switch Pane | ↑↓/jk:Navigate | g/G:Top/End")
87
+ header_win.addstr("←→/hl:Switch Pane | ↑↓/jk/Click:Navigate | g/G:Top/End")
86
88
  end
87
89
  end
88
90
 
@@ -12,18 +12,27 @@ module LogBench
12
12
  self.header = Header.new(screen, state)
13
13
  self.request_list = RequestList.new(screen, state, scrollbar)
14
14
  self.details = Details.new(screen, state, scrollbar, ansi_renderer)
15
+ self.update_modal = UpdateModal.new(screen, state)
15
16
  end
16
17
 
17
18
  def draw
18
- header.draw
19
- request_list.draw
20
- details.draw
21
- screen.refresh_all
19
+ if update_modal.should_show?
20
+ update_modal.draw
21
+ else
22
+ header.draw
23
+ request_list.draw
24
+ details.draw
25
+ screen.refresh_all
26
+ end
27
+ end
28
+
29
+ def handle_modal_input(ch)
30
+ update_modal.handle_input(ch)
22
31
  end
23
32
 
24
33
  private
25
34
 
26
- attr_accessor :screen, :state, :header, :scrollbar, :request_list, :ansi_renderer, :details
35
+ attr_accessor :screen, :state, :header, :scrollbar, :request_list, :ansi_renderer, :details, :update_modal
27
36
  end
28
37
  end
29
38
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogBench
4
+ module App
5
+ module Renderer
6
+ class UpdateModal
7
+ include Curses
8
+
9
+ # Modal dimensions
10
+ MODAL_WIDTH = 40
11
+ MODAL_HEIGHT = 7
12
+ COUNTDOWN_SECONDS = 5
13
+
14
+ # Color constants
15
+ HEADER_CYAN = 1
16
+ SUCCESS_GREEN = 3
17
+ WARNING_YELLOW = 4
18
+
19
+ def initialize(screen, state)
20
+ self.screen = screen
21
+ self.state = state
22
+ self.countdown = COUNTDOWN_SECONDS
23
+ self.modal_win = nil
24
+ self.last_countdown_update = Time.now
25
+ self.dismissed = false
26
+ end
27
+
28
+ def should_show?
29
+ state.update_available? && !dismissed
30
+ end
31
+
32
+ def draw
33
+ return unless should_show?
34
+
35
+ create_modal_window
36
+ update_countdown_timer
37
+ draw_content
38
+ modal_win&.refresh
39
+ end
40
+
41
+ def handle_input(ch)
42
+ return false unless should_show?
43
+
44
+ # Any key dismisses the modal
45
+ if ch != -1
46
+ dismiss_modal
47
+ return true
48
+ end
49
+
50
+ false
51
+ end
52
+
53
+ private
54
+
55
+ attr_accessor :screen, :state, :countdown, :modal_win, :last_countdown_update, :dismissed
56
+
57
+ def create_modal_window
58
+ return if modal_win
59
+
60
+ # Calculate center position
61
+ center_y = (screen.height - MODAL_HEIGHT) / 2
62
+ center_x = (screen.width - MODAL_WIDTH) / 2
63
+
64
+ # Create modal window
65
+ self.modal_win = Window.new(MODAL_HEIGHT, MODAL_WIDTH, center_y, center_x)
66
+ end
67
+
68
+ def draw_content
69
+ return unless modal_win
70
+
71
+ modal_win.erase
72
+ modal_win.box(0, 0)
73
+
74
+ # Header
75
+ modal_win.setpos(1, 2)
76
+ modal_win.attron(color_pair(HEADER_CYAN) | A_BOLD) { modal_win.addstr("🚀 LogBench Update Available!") }
77
+
78
+ # Version info
79
+ modal_win.setpos(3, 2)
80
+ modal_win.addstr("Current: ")
81
+ modal_win.attron(color_pair(SUCCESS_GREEN)) { modal_win.addstr(LogBench::VERSION) }
82
+ modal_win.addstr(" → Latest: ")
83
+ modal_win.attron(color_pair(SUCCESS_GREEN) | A_BOLD) { modal_win.addstr(state.update_version) }
84
+
85
+ # Instructions with countdown
86
+ modal_win.setpos(5, 2)
87
+ modal_win.addstr("Press any key to continue or wait ")
88
+ modal_win.attron(color_pair(WARNING_YELLOW) | A_BOLD) { modal_win.addstr("#{countdown}s") }
89
+ end
90
+
91
+ def update_countdown_timer
92
+ now = Time.now
93
+ if now - last_countdown_update >= 1.0
94
+ self.countdown -= 1
95
+ self.last_countdown_update = now
96
+
97
+ if countdown <= 0
98
+ dismiss_modal
99
+ end
100
+ end
101
+ end
102
+
103
+ def dismiss_modal
104
+ state.dismiss_update_notification
105
+ modal_win&.close
106
+ self.modal_win = nil
107
+ clear
108
+ end
109
+
110
+ def color_pair(n)
111
+ screen.color_pair(n)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -29,6 +29,7 @@ module LogBench
29
29
  setup_colors
30
30
  clear_screen_immediately
31
31
  setup_windows
32
+ turn_text_selection_mode(false)
32
33
  end
33
34
 
34
35
  def cleanup
@@ -54,6 +55,10 @@ module LogBench
54
55
  Curses.color_pair(n)
55
56
  end
56
57
 
58
+ def turn_text_selection_mode(enabled)
59
+ enabled ? mousemask(0) : mousemask(BUTTON1_CLICKED)
60
+ end
61
+
57
62
  private
58
63
 
59
64
  attr_writer :header_win, :log_win, :panel_width, :detail_win
@@ -4,7 +4,7 @@ module LogBench
4
4
  module App
5
5
  class State
6
6
  attr_reader :main_filter, :sort, :detail_filter
7
- attr_accessor :requests, :auto_scroll, :scroll_offset, :selected, :detail_scroll_offset
7
+ attr_accessor :requests, :auto_scroll, :scroll_offset, :selected, :detail_scroll_offset, :text_selection_mode, :update_available, :update_version
8
8
 
9
9
  def initialize
10
10
  self.requests = []
@@ -14,9 +14,12 @@ module LogBench
14
14
  self.running = true
15
15
  self.focused_pane = :left
16
16
  self.detail_scroll_offset = 0
17
+ self.text_selection_mode = false
17
18
  self.main_filter = Filter.new
18
19
  self.detail_filter = Filter.new
19
20
  self.sort = Sort.new
21
+ self.update_available = false
22
+ self.update_version = nil
20
23
  end
21
24
 
22
25
  def running?
@@ -31,6 +34,28 @@ module LogBench
31
34
  self.auto_scroll = !auto_scroll
32
35
  end
33
36
 
37
+ def toggle_text_selection_mode
38
+ self.text_selection_mode = !text_selection_mode
39
+ end
40
+
41
+ def text_selection_mode?
42
+ text_selection_mode
43
+ end
44
+
45
+ def set_update_available(version)
46
+ self.update_available = true
47
+ self.update_version = version
48
+ end
49
+
50
+ def dismiss_update_notification
51
+ self.update_available = false
52
+ self.update_version = nil
53
+ end
54
+
55
+ def update_available?
56
+ update_available
57
+ end
58
+
34
59
  def clear_filter
35
60
  main_filter.clear
36
61
  self.selected = 0
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogBench
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module LogBench
8
+ class VersionChecker
9
+ # Cache file location
10
+ CACHE_DIR = File.expand_path("~/.cache/log_bench")
11
+ CACHE_FILE = File.join(CACHE_DIR, "version_check.json")
12
+
13
+ # Cache duration (24 hours)
14
+ CACHE_DURATION = 24 * 60 * 60
15
+
16
+ # RubyGems API endpoint
17
+ RUBYGEMS_API_URL = "https://rubygems.org/api/v1/gems/log_bench.json"
18
+
19
+ # Timeout for HTTP requests
20
+ REQUEST_TIMEOUT = 5
21
+
22
+ def self.check_for_update
23
+ new.check_for_update
24
+ end
25
+
26
+ def check_for_update
27
+ return nil unless should_check?
28
+
29
+ latest_version = fetch_latest_version
30
+ return nil unless latest_version
31
+
32
+ update_cache(latest_version)
33
+
34
+ if newer_version_available?(latest_version)
35
+ latest_version
36
+ end
37
+ rescue
38
+ # Silently fail - don't interrupt the user experience
39
+ nil
40
+ end
41
+
42
+ private
43
+
44
+ def should_check?
45
+ return true unless File.exist?(CACHE_FILE)
46
+
47
+ cache_data = read_cache
48
+ return true unless cache_data
49
+
50
+ # Check if cache is expired
51
+ Time.now - Time.parse(cache_data["checked_at"]) > CACHE_DURATION
52
+ rescue
53
+ true
54
+ end
55
+
56
+ def fetch_latest_version
57
+ uri = URI(RUBYGEMS_API_URL)
58
+
59
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: REQUEST_TIMEOUT) do |http|
60
+ request = Net::HTTP::Get.new(uri)
61
+ response = http.request(request)
62
+
63
+ return nil unless response.code == "200"
64
+
65
+ data = JSON.parse(response.body)
66
+ data["version"]
67
+ end
68
+ rescue
69
+ nil
70
+ end
71
+
72
+ def read_cache
73
+ return nil unless File.exist?(CACHE_FILE)
74
+
75
+ JSON.parse(File.read(CACHE_FILE))
76
+ rescue
77
+ nil
78
+ end
79
+
80
+ def update_cache(latest_version)
81
+ FileUtils.mkdir_p(CACHE_DIR)
82
+
83
+ cache_data = {
84
+ "latest_version" => latest_version,
85
+ "checked_at" => Time.now.iso8601
86
+ }
87
+
88
+ File.write(CACHE_FILE, JSON.pretty_generate(cache_data))
89
+ rescue
90
+ # Ignore cache write errors
91
+ nil
92
+ end
93
+
94
+ def newer_version_available?(latest_version)
95
+ Gem::Version.new(latest_version) > Gem::Version.new(LogBench::VERSION)
96
+ rescue
97
+ false
98
+ end
99
+ end
100
+ 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.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamín Silva
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-06-06 00:00:00.000000000 Z
10
+ date: 2025-06-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.14'
54
+ - !ruby/object:Gem::Dependency
55
+ name: net-http
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.6'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.6'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: rake
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -114,12 +128,14 @@ files:
114
128
  - lib/log_bench/app/input_handler.rb
115
129
  - lib/log_bench/app/main.rb
116
130
  - lib/log_bench/app/monitor.rb
131
+ - lib/log_bench/app/mouse_handler.rb
117
132
  - lib/log_bench/app/renderer/ansi.rb
118
133
  - lib/log_bench/app/renderer/details.rb
119
134
  - lib/log_bench/app/renderer/header.rb
120
135
  - lib/log_bench/app/renderer/main.rb
121
136
  - lib/log_bench/app/renderer/request_list.rb
122
137
  - lib/log_bench/app/renderer/scrollbar.rb
138
+ - lib/log_bench/app/renderer/update_modal.rb
123
139
  - lib/log_bench/app/screen.rb
124
140
  - lib/log_bench/app/sort.rb
125
141
  - lib/log_bench/app/state.rb
@@ -134,6 +150,7 @@ files:
134
150
  - lib/log_bench/log/request.rb
135
151
  - lib/log_bench/railtie.rb
136
152
  - lib/log_bench/version.rb
153
+ - lib/log_bench/version_checker.rb
137
154
  - lib/tasks/log_bench.rake
138
155
  - logbench-preview.png
139
156
  homepage: https://github.com/silva96/log_bench