onlylogs 0.1.3 → 0.2.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: 5d17b58f1bc49d7f70ea61867d8c8e78be1fc25f1ede62f0b477764c97b78752
4
- data.tar.gz: 3de1a2520cfb58fc74eab4152a43e20dc0082ec5099272efee00761c15ce3e1e
3
+ metadata.gz: baff3b51025f63d5ba3537ebf50f48c75478e64287ea98e9634d19fe070a42f5
4
+ data.tar.gz: 47c2a4a23cfbfe44c25e0fbdbb96413e49aac96fe7ff6695c3440ec00aa0b08b
5
5
  SHA512:
6
- metadata.gz: 7079c7f67be0048382b9029ef636a17c6de654c49ae1c17346cfbfad99e7015a1ffed9a1ef1c4313b45f6cad2bed839dc804f39b616f46bb55d4203c3cde8bc8
7
- data.tar.gz: 4630f69bf7066024413e4251747cc0da907f8d7912eab6eae78fb1c4b9ac3fa4d2bb026e0cc5e9452e9e78c289bf120c82724207426f88e7886b6ea92077d337
6
+ metadata.gz: 1676249dd2d66f2b9b4032473313fac9306c2f05283dfa927c229af55f5ace1f7a740456231d15de5a92b7de5b7fc56a54ecdc643e1adc33e53718ec5e2befce
7
+ data.tar.gz: 73d9f8296c302d3c84d5ae74748a0ca6601c26f00722b33a1fe067f1c1e62810b6e70fac8825e04763de0640759caa785092ba56f8df69f3dc8bee0373150056
@@ -68,7 +68,7 @@ module Onlylogs
68
68
 
69
69
  def cleanup_existing_operations
70
70
  if @batch_sender
71
- @batch_sender.stop
71
+ @batch_sender.stop(send_remaining_lines: false)
72
72
  @batch_sender = nil
73
73
  end
74
74
 
@@ -102,10 +102,7 @@ module Onlylogs
102
102
  # next
103
103
  # end
104
104
 
105
- lines_to_send << {
106
- line_number: log_line.number,
107
- html: render_log_line(log_line)
108
- }
105
+ lines_to_send << render_log_line(log_line)
109
106
  end
110
107
 
111
108
  if lines_to_send.any?
@@ -129,10 +126,21 @@ module Onlylogs
129
126
 
130
127
  @log_watcher_running = false
131
128
 
132
- return unless @log_watcher_thread&.alive?
129
+ # Wait for graceful shutdown
130
+ if @log_watcher_thread&.alive?
131
+ @log_watcher_thread.join(3)
133
132
 
134
- @log_watcher_thread.kill
135
- @log_watcher_thread.join(1)
133
+ # If still alive after 3 seconds, force kill (but log it)
134
+ if @log_watcher_thread.alive?
135
+ Rails.logger.warn "Onlylogs: Force killing watcher thread after timeout"
136
+ @log_watcher_thread.kill
137
+ @log_watcher_thread.join(1)
138
+ end
139
+ end
140
+
141
+ # Clear references to allow GC
142
+ @log_watcher_thread = nil
143
+ @log_file = nil
136
144
  end
137
145
 
138
146
  def read_entire_file_with_filter(file_path, filter = nil, regexp_mode = false, start_position = 0, end_position = nil)
@@ -146,36 +154,38 @@ module Onlylogs
146
154
 
147
155
  line_count = 0
148
156
 
149
- Rails.logger.silence(Logger::ERROR) do
150
- @log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |log_line|
151
- return if @batch_sender.nil?
157
+ begin
158
+ Rails.logger.silence(Logger::ERROR) do
159
+ @log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |log_line|
160
+ return if @batch_sender.nil?
152
161
 
153
- # Add to batch buffer (sender thread will handle sending)
154
- @batch_sender.add_line({
155
- line_number: log_line.number,
156
- html: render_log_line(log_line)
157
- })
162
+ # Add to batch buffer (sender thread will handle sending)
163
+ @batch_sender.add_line(render_log_line(log_line))
158
164
 
159
- line_count += 1
165
+ line_count += 1
166
+ end
160
167
  end
161
- end
162
168
 
163
- # Stop batch sender and flush any remaining lines
164
- @batch_sender.stop
169
+ # Stop batch sender and flush any remaining lines
170
+ @batch_sender.stop
165
171
 
166
- # Send completion message
167
- if line_count >= Onlylogs.max_line_matches
168
- transmit({ action: "finish", content: "Search finished. Search results limit reached." })
169
- else
170
- transmit({ action: "finish", content: "Search finished." })
172
+ # Send completion message
173
+ if line_count >= Onlylogs.max_line_matches
174
+ transmit({ action: "finish", content: "Search finished. Search results limit reached." })
175
+ else
176
+ transmit({ action: "finish", content: "Search finished." })
177
+ end
178
+ ensure
179
+ # Always cleanup even if interrupted or error occurs
180
+ @batch_sender&.stop
181
+ @batch_sender = nil
182
+ @log_file = nil
183
+ @log_watcher_running = false
171
184
  end
172
-
173
- @log_watcher_running = false
174
185
  end
175
186
 
176
187
  def render_log_line(log_line)
177
- "<pre data-line-number=\"#{log_line.number}\">" \
178
- "<span class=\"line-number\">#{log_line.parsed_number}</span>#{log_line.parsed_text}</pre>"
188
+ "<pre>#{FilePathParser.parse(AnsiColorParser.parse(ERB::Util.html_escape(log_line)))}</pre>"
179
189
  end
180
190
  end
181
191
  end
@@ -5,7 +5,6 @@ export default class LogStreamerController extends Controller {
5
5
  static values = {
6
6
  filePath: { type: String },
7
7
  cursorPosition: { type: Number, default: 0 },
8
- lastLineNumber: { type: Number, default: 0 },
9
8
  autoScroll: { type: Boolean, default: true },
10
9
  autoStart: { type: Boolean, default: true },
11
10
  filter: { type: String, default: '' },
@@ -13,7 +12,7 @@ export default class LogStreamerController extends Controller {
13
12
  regexpMode: { type: Boolean, default: false }
14
13
  };
15
14
 
16
- static targets = ["logLines", "filterInput", "lineRange", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton"];
15
+ static targets = ["logLines", "filterInput", "results", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton"];
17
16
 
18
17
  connect() {
19
18
  this.consumer = createConsumer();
@@ -21,8 +20,6 @@ export default class LogStreamerController extends Controller {
21
20
  this.subscription = null;
22
21
  this.isRunning = false;
23
22
  this.reconnectTimeout = null;
24
- this.minLineNumber = null;
25
- this.maxLineNumber = 0;
26
23
  this.isSearchFinished = true;
27
24
 
28
25
  // Initialize clusterize
@@ -83,12 +80,8 @@ export default class LogStreamerController extends Controller {
83
80
  }
84
81
 
85
82
  clear() {
86
- this.minLineNumber = null;
87
- this.maxLineNumber = 0;
88
-
89
83
  this.clusterize.clear();
90
-
91
- this.#updateLineRangeDisplay();
84
+ this.#updateResultsDisplay();
92
85
  }
93
86
 
94
87
  toggleAutoScroll() {
@@ -180,9 +173,8 @@ export default class LogStreamerController extends Controller {
180
173
  }
181
174
 
182
175
  stopSearch() {
183
- if (this.subscription && this.isRunning) {
184
- this.subscription.perform('stop_watcher');
185
- }
176
+ console.log("stop search");
177
+ this.subscription.perform('stop_watcher');
186
178
  }
187
179
 
188
180
  clearLogs() {
@@ -244,7 +236,6 @@ export default class LogStreamerController extends Controller {
244
236
  #handleConnected() {
245
237
  this.subscription.perform('initialize_watcher', {
246
238
  cursor_position: this.cursorPositionValue,
247
- last_line_number: this.lastLineNumberValue,
248
239
  file_path: this.filePathValue,
249
240
  filter: this.filterInputTarget.value,
250
241
  mode: this.modeValue,
@@ -273,24 +264,10 @@ export default class LogStreamerController extends Controller {
273
264
 
274
265
  #handleLogLines(lines) {
275
266
  try {
276
- const newLines = [];
277
-
278
- lines.forEach(line => {
279
- const { line_number, html } = line;
280
-
281
- if (this.minLineNumber === null || line_number < this.minLineNumber) {
282
- this.minLineNumber = line_number;
283
- }
284
- this.maxLineNumber = Math.max(this.maxLineNumber, line_number);
285
-
286
- // Add to new lines array for clusterize
287
- newLines.push(html);
288
- });
289
-
290
267
  // Append new lines to clusterize
291
- if (newLines.length > 0) {
292
- this.clusterize.append(newLines);
293
- this.#updateLineRangeDisplay();
268
+ if (lines.length > 0) {
269
+ this.clusterize.append(lines);
270
+ this.#updateResultsDisplay();
294
271
  this.scroll();
295
272
  }
296
273
 
@@ -313,13 +290,8 @@ export default class LogStreamerController extends Controller {
313
290
  }
314
291
 
315
292
  #handleFinish(message) {
316
- // Display the finish message without loading icon
317
293
  this.messageTarget.innerHTML = message;
318
-
319
- // Mark search as finished
320
294
  this.isSearchFinished = true;
321
-
322
- // Update stop button visibility (should hide it)
323
295
  this.updateStopButtonVisibility();
324
296
  }
325
297
 
@@ -341,19 +313,9 @@ export default class LogStreamerController extends Controller {
341
313
  this.messageTarget.innerHTML = '';
342
314
  }
343
315
 
344
- #updateLineRangeDisplay() {
316
+ #updateResultsDisplay() {
345
317
  const resultsCount = this.clusterize.getRowsAmount();
346
- let lineRangeText;
347
-
348
- if (this.minLineNumber === null || this.maxLineNumber === 0) {
349
- lineRangeText = `No lines`;
350
- } else if (this.minLineNumber === this.maxLineNumber) {
351
- lineRangeText = `Line ${this.#formatNumber(this.minLineNumber)}`;
352
- } else {
353
- lineRangeText = `Lines ${this.#formatNumber(this.minLineNumber)} - ${this.#formatNumber(this.maxLineNumber)}`;
354
- }
355
-
356
- this.lineRangeTarget.textContent = `${lineRangeText} | Results: ${this.#formatNumber(resultsCount)}`;
318
+ this.resultsTarget.textContent = `Results: ${this.#formatNumber(resultsCount)}`;
357
319
  }
358
320
 
359
321
  #formatNumber(number) {
@@ -393,7 +355,6 @@ export default class LogStreamerController extends Controller {
393
355
  filePath: this.filePathValue,
394
356
  cursorPosition: this.cursorPositionValue,
395
357
  lineCount: this.clusterize.getRowsAmount(),
396
- maxLineNumber: this.maxLineNumber,
397
358
  connected: this.subscription && this.subscription.identifier
398
359
  };
399
360
  }
@@ -23,14 +23,26 @@ module Onlylogs
23
23
  end
24
24
  end
25
25
 
26
- def stop
26
+ def stop(send_remaining_lines: true)
27
27
  return unless @running
28
28
 
29
29
  @running = false
30
- @sender_thread&.join(0.1)
31
-
30
+
31
+ # Wait longer for graceful shutdown
32
+ if @sender_thread&.alive?
33
+ @sender_thread.join(0.5)
34
+ end
35
+
32
36
  # Send any remaining lines
33
37
  send_batch
38
+ if send_remaining_lines
39
+ send_batch
40
+ else
41
+ @mutex.synchronize { @buffer.clear }
42
+ end
43
+
44
+ # Clear thread reference to allow GC
45
+ @sender_thread = nil
34
46
  end
35
47
 
36
48
  def add_line(line_data)
@@ -2,12 +2,11 @@ module Onlylogs
2
2
  class Error < StandardError; end
3
3
 
4
4
  class File
5
- attr_reader :path, :last_position, :last_line_number
5
+ attr_reader :path, :last_position
6
6
 
7
7
  def initialize(path, last_position: 0)
8
8
  self.path = path
9
9
  self.last_position = last_position
10
- self.last_line_number = 0
11
10
  validate!
12
11
  end
13
12
 
@@ -15,7 +14,6 @@ module Onlylogs
15
14
  return if position < 0
16
15
 
17
16
  self.last_position = position
18
- self.last_line_number = 0
19
17
  end
20
18
 
21
19
  def watch(&block)
@@ -58,14 +56,14 @@ module Onlylogs
58
56
  end
59
57
 
60
58
  def grep(filter, regexp_mode: false, start_position: 0, end_position: nil, &block)
61
- Grep.grep(filter, path, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |line_number, content|
62
- yield Onlylogs::LogLine.new(line_number, content)
59
+ Grep.grep(filter, path, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |content|
60
+ yield content
63
61
  end
64
62
  end
65
63
 
66
64
  private
67
65
 
68
- attr_writer :path, :last_position, :last_line_number
66
+ attr_writer :path, :last_position
69
67
 
70
68
  def read_new_lines
71
69
  return [] unless exist?
@@ -73,74 +71,32 @@ module Onlylogs
73
71
  current_size = ::File.size(path)
74
72
  return [] if current_size <= last_position
75
73
 
76
- # Read new content from last_position to end of file
77
- new_content = ""
74
+ lines = []
75
+
78
76
  ::File.open(path, "rb") do |file|
79
77
  file.seek(last_position)
80
- new_content = file.read
81
- end
82
-
83
- return [] if new_content.empty?
84
78
 
85
- # Split into lines, handling incomplete lines
86
- lines = new_content.lines(chomp: true)
87
-
88
- # If we're not at the beginning of the file, check if we're at a line boundary
89
- first_line_removed = false
90
- if last_position > 0
91
- # Read one character before to see if it was a newline
92
- ::File.open(path, "rb") do |file|
79
+ # Skip first line if we're mid-line (not at start or after newline)
80
+ if last_position > 0
93
81
  file.seek(last_position - 1)
94
- char_before = file.read(1)
95
- # If the character before wasn't a newline, we're in the middle of a line
96
- if char_before != "\n" && lines.any?
97
- # Remove the first line as it's incomplete
98
- lines.shift
99
- first_line_removed = true
100
- end
82
+ skip_first = (file.read(1) != "\n")
83
+ file.seek(last_position)
84
+ file.gets if skip_first # Consume incomplete line
101
85
  end
102
- end
103
86
 
104
- # Check if the last line is complete (ends with newline)
105
- last_line_incomplete = lines.any? && !new_content.end_with?("\n")
106
- if last_line_incomplete
107
- # Remove the last line as it's incomplete
108
- lines.pop
109
- end
110
-
111
- # Update position to end of last complete line
112
- if lines.any?
113
- # Find the position after the last complete line
114
- ::File.open(path, "rb") do |file|
115
- file.seek(last_position)
116
- # Read and count newlines to find where complete lines end
117
- newline_count = 0
118
- # If we removed the first line, we need to count one extra newline
119
- # to account for the incomplete first line
120
- target_newlines = lines.length + (first_line_removed ? 1 : 0)
121
- while newline_count < target_newlines
122
- char = file.read(1)
123
- break unless char
124
-
125
- newline_count += 1 if char == "\n"
87
+ # Read complete lines using gets (memory efficient, no buffer needed)
88
+ while (line = file.gets)
89
+ if line.end_with?("\n")
90
+ # Complete line - store as simple string
91
+ lines << line.chomp
92
+ self.last_position = file.pos
93
+ else
94
+ # Incomplete line at EOF - skip it
95
+ break
126
96
  end
127
- self.last_position = file.tell
128
97
  end
129
- elsif last_line_incomplete
130
- # If we had lines but removed the last incomplete one,
131
- # position should be at the start of the incomplete line
132
- self.last_position = current_size - new_content.lines.last.length
133
- elsif first_line_removed
134
- # If we removed the first line but have no complete lines,
135
- # position should be at the end of the file since we consumed all content
136
- self.last_position = current_size
137
- else
138
- # No lines at all, position at end of file
139
- self.last_position = current_size
140
98
  end
141
99
 
142
- lines = lines.map.with_index { |line, index| Onlylogs::LogLine.new(self.last_line_number + index, line) }
143
- self.last_line_number += lines.length
144
100
  lines
145
101
  end
146
102
 
@@ -21,16 +21,14 @@ module Onlylogs
21
21
 
22
22
  IO.popen(command_args, err: "/dev/null") do |io|
23
23
  io.each_line do |line|
24
- # Parse each line as it comes in - super_grep returns grep output with line numbers (format: line_number:content)
25
- if match = line.strip.match(/^(\d+):(.*)/)
26
- line_number = match[1].to_i
27
- content = match[2]
28
-
29
- if block_given?
30
- yield line_number, content
31
- else
32
- results << [ line_number, content ]
33
- end
24
+ # Line numbers are no longer outputted by super_grep/super_ripgrep
25
+ # Use String.new to create a copy and prevent memory retention from IO buffers
26
+ content = String.new(line.chomp)
27
+
28
+ if block_given?
29
+ yield content
30
+ else
31
+ results << content
34
32
  end
35
33
  end
36
34
  end
@@ -83,7 +83,7 @@
83
83
  </button>
84
84
  </div>
85
85
  <div>
86
- <span data-log-streamer-target="lineRange" style="color: #666;">No lines</span>
86
+ <span data-log-streamer-target="results" style="color: #666;">Results: 0</span>
87
87
  </div>
88
88
  <div data-log-streamer-target="message"></div>
89
89
  <% unless Onlylogs.ripgrep_enabled? %>
@@ -26,14 +26,23 @@
26
26
 
27
27
  .clusterize-content {
28
28
  outline: 0;
29
+ counter-reset: line-number;
29
30
  }
30
31
 
31
- .line-number {
32
- color: #aaa;
33
- user-select: none;
34
- margin-right: 0.5em;
32
+ pre {
33
+ counter-increment: line-number;
34
+
35
+ &::before {
36
+ content: counter(line-number);
37
+ color: #aaa;
38
+ user-select: none;
39
+ margin-right: 0.5em;
40
+ display: inline-block;
41
+ text-align: right;
42
+ min-width: 3.5em;
43
+ }
35
44
  }
36
-
45
+
37
46
  .color-success {
38
47
  color: green;
39
48
  }
@@ -25,15 +25,5 @@ module Onlylogs
25
25
  end
26
26
  end
27
27
  end
28
-
29
- # initializer "onlylogs.add_log_silencer" do |app|
30
- # silenced_routes = ['/onlylogs']
31
- #
32
- # app.middleware.insert_before(
33
- # Rails::Rack::Logger,
34
- # Onlylogs::LogSilencerMiddleware,
35
- # paths_to_silence: silenced_routes
36
- # )
37
- # end
38
28
  end
39
29
  end
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onlylogs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi
@@ -56,12 +56,10 @@ files:
56
56
  - app/javascript/onlylogs/controllers/text_selection_controller.js
57
57
  - app/jobs/onlylogs/application_job.rb
58
58
  - app/models/onlylogs/ansi_color_parser.rb
59
- - app/models/onlylogs/application_record.rb
60
59
  - app/models/onlylogs/batch_sender.rb
61
60
  - app/models/onlylogs/file.rb
62
61
  - app/models/onlylogs/file_path_parser.rb
63
62
  - app/models/onlylogs/grep.rb
64
- - app/models/onlylogs/log_line.rb
65
63
  - app/models/onlylogs/secure_file_path.rb
66
64
  - app/views/home/show.html.erb
67
65
  - app/views/layouts/onlylogs/application.html.erb
@@ -76,7 +74,6 @@ files:
76
74
  - lib/onlylogs/configuration.rb
77
75
  - lib/onlylogs/engine.rb
78
76
  - lib/onlylogs/formatter.rb
79
- - lib/onlylogs/log_silencer_middleware.rb
80
77
  - lib/onlylogs/logger.rb
81
78
  - lib/onlylogs/socket_logger.rb
82
79
  - lib/onlylogs/version.rb
@@ -1,5 +0,0 @@
1
- module Onlylogs
2
- class ApplicationRecord < ActiveRecord::Base
3
- self.abstract_class = true
4
- end
5
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Onlylogs
4
- class LogLine
5
- attr_reader :number, :text
6
-
7
- def initialize(number, text)
8
- @number = number
9
- @text = text
10
- end
11
-
12
- def parsed_number
13
- number.to_s.reverse.gsub(/(\d{3})(?=\d)/, "\\1'").reverse.rjust(7)
14
- end
15
-
16
- def parsed_text
17
- FilePathParser.parse(AnsiColorParser.parse(ERB::Util.html_escape(text)))
18
- end
19
-
20
- def to_a
21
- [number, text]
22
- end
23
- end
24
- end
@@ -1,26 +0,0 @@
1
- module Onlylogs
2
- class LogSilencerMiddleware
3
- def initialize(app, paths_to_silence:)
4
- @app = app
5
- # Ensure paths are an array for flexibility
6
- @paths_to_silence = Array(paths_to_silence)
7
- end
8
-
9
- def call(env)
10
- if silence_request?(env)
11
- Rails.logger.silence do
12
- @app.call(env)
13
- end
14
- else
15
- @app.call(env)
16
- end
17
- end
18
-
19
- private
20
-
21
- def silence_request?(env)
22
- request_path = env['PATH_INFO']
23
- @paths_to_silence.any? { |path| request_path.start_with?(path) }
24
- end
25
- end
26
- end