onlylogs 0.1.2 → 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: 0f2f05cff2b5f48634427295d3704774b1431c85f5bbba89a5adc9d3a6769c79
4
- data.tar.gz: 16fc0ca2ec651f9523e266ec883a796f2dc94bb46b952b5b6aaf0eb527fd8871
3
+ metadata.gz: baff3b51025f63d5ba3537ebf50f48c75478e64287ea98e9634d19fe070a42f5
4
+ data.tar.gz: 47c2a4a23cfbfe44c25e0fbdbb96413e49aac96fe7ff6695c3440ec00aa0b08b
5
5
  SHA512:
6
- metadata.gz: 7625fcd6b6b275a4008e88d7ec9a085b8277854f759472a7eea0acb98d2b8d81c14023ddb8eae313f74bf95d82b9ce8d134fdc0b52af019d84d560a2e62fb7e8
7
- data.tar.gz: 104a06ec0c73c0885a89cce09dd25597d4c013c2cac9ab9b88f32a77d785aba323885f6eb6cc7a28bfe87c0e0792777d1aae16e6188ffb7bcc589961b8e7a4f6
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
@@ -1,49 +1 @@
1
- <div class="sidebar-layout" data-controller="toggle-class" data-toggle-class-toggle-class="closed">
2
- <aside id="sidebar">
3
- <div class="sidebar-menu">
4
- <%= link_to onlylogs.root_path, class: "btn sidebar-menu__button" do %>
5
- <%= image_tag "onlylogs/logo.png" %>
6
- <% end %>
7
- <div class="sidebar-menu__content">
8
- <div class="sidebar-menu__items">
9
- <div class="sidebar-menu__group">
10
- <div class="sidebar-menu__group-label">Projects</div>
11
- <nav class="sidebar-menu__items">
12
- <%= link_to "All Projects", root_path, class: "btn sidebar-menu__button" %>
13
-
14
- <nav class="sidebar-menu__sub">
15
- <nav class="sidebar-menu__sub text-sm">
16
- <div>
17
- <% if Onlylogs::File.text_file?(drain_file) %>
18
- <%= link_to drain_file.basename, project_path(@project, file_path: Onlylogs::SecureFilePath.encrypt(drain_file.to_s)) %>
19
- <% else %>
20
- <%= drain_file.basename %>
21
- <% end %>
22
- <small>(<%= number_to_human_size(File.size(drain_file)) %>)</small>
23
- <%= link_to project_drain_file_path(@project, Onlylogs::SecureFilePath.encrypt(drain_file.to_s)),
24
- title: t('drain_files.show.title'),
25
- class: 'download-link' do %>
26
- <i class="fa-regular fa-download" aria-label="<%= t('drain_files.show.title') %>"></i>
27
- <% end %>
28
- </div>
29
- </nav>
30
- </nav>
31
- </nav>
32
- </div>
33
- </div>
34
- </div>
35
- </div>
36
-
37
- </aside>
38
- <main id="main">
39
- <header class="flex items-center gap show@md">
40
- <button type="button" class="btn btn--borderless p-1" data-action="toggle-class#toggle">
41
- <i class="fa-regular fa-sidebar"></i>
42
- <span class="sr-only">Toggle Sidebar</span>
43
- </button>
44
- <div class="separator-vertical mi-1" style="--sep-size: 1rem"></div>
45
- </header>
46
-
47
- <%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll } %>
48
- </main>
49
- </div>
1
+ <%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll } %>
@@ -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
  }
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # this is a simple sidecar process that takes log lines sent into the socket and sends them to the drain url in batches.
5
+ # You can also attach it to puma using `plugin :onlylogs_sidecar` in your config/puma.rb file so you don't need to run it manually.
6
+ # The following parameters are availble:
7
+ # - ONLYLOGS_DRAIN_URL: the url to the drain. this is mandatory and must be set.
8
+ # - ONLYLOGS_SIDECAR_SOCKET: the path to the socket file. this is optional and will default to tmp/sockets/onlylogs-sidecar.sock in the current directory.
9
+ # - ONLYLOGS_BATCH_SIZE: the number of lines to batch before sending. this is optional and will default to 100.
10
+ # - ONLYLOGS_FLUSH_INTERVAL: the interval in seconds to flush the batch. this is optional and will default to 0.5 seconds.
11
+
12
+ require "socket"
13
+ require "net/http"
14
+ require "uri"
15
+ require "thread"
16
+ require "fileutils"
17
+
18
+ drain_url = ENV["ONLYLOGS_DRAIN_URL"]
19
+ socket_path = ENV.fetch("ONLYLOGS_SIDECAR_SOCKET", File.expand_path("tmp/sockets/onlylogs-sidecar.sock", Dir.pwd))
20
+ batch_size = ENV.fetch("ONLYLOGS_BATCH_SIZE", "100").to_i
21
+ flush_interval = ENV.fetch("ONLYLOGS_FLUSH_INTERVAL", "0.5").to_f
22
+
23
+ if drain_url.to_s.strip.empty?
24
+ error "[OnlylogsSidecar] ERROR: ONLYLOGS_DRAIN_URL is not set. Exiting."
25
+ exit 1
26
+ end
27
+
28
+ uri = URI.parse(drain_url)
29
+ FileUtils.mkdir_p(File.dirname(socket_path))
30
+ FileUtils.rm_f(socket_path)
31
+
32
+ running = true
33
+ queue = Queue.new
34
+
35
+ trap("INT") { running = false }
36
+ trap("TERM") { running = false }
37
+
38
+ server = UNIXServer.new(socket_path)
39
+
40
+ accept_thread = Thread.new do
41
+ while running
42
+ begin
43
+ client = server.accept_nonblock
44
+ rescue IO::WaitReadable
45
+ IO.select([server])
46
+ retry
47
+ rescue IOError, Errno::EBADF
48
+ break
49
+ end
50
+
51
+ Thread.new(client) do |conn|
52
+ begin
53
+ conn.each_line do |line|
54
+ cleaned = line.to_s.strip
55
+ queue << cleaned unless cleaned.empty?
56
+ end
57
+ rescue => e
58
+ warn "[OnlylogsSidecar] client error: #{e.class}: #{e.message}"
59
+ ensure
60
+ conn.close rescue nil
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def send_batch(uri, lines)
67
+ return if lines.empty?
68
+
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ http.use_ssl = (uri.scheme == "https")
71
+ request = Net::HTTP::Post.new(uri.path)
72
+ request.body = lines.join("\n")
73
+ puts "[OnlylogsSidecar] Sending #{request.body}"
74
+ request.content_type = "text/plain"
75
+ http.read_timeout = 5
76
+ http.open_timeout = 2
77
+ http.start
78
+ http.request(request)
79
+ rescue => e
80
+ warn "[OnlylogsSidecar] HTTP error: #{e.class}: #{e.message}"
81
+ ensure
82
+ http&.finish rescue nil
83
+ end
84
+
85
+ batch = []
86
+ last_flush = Time.now
87
+
88
+ while running || !queue.empty?
89
+ begin
90
+ line = queue.pop(true)
91
+ batch << line if line
92
+ rescue ThreadError
93
+ # queue empty
94
+ sleep 0.01
95
+ end
96
+
97
+ next if batch.empty?
98
+
99
+ if batch.size >= batch_size || (Time.now - last_flush) >= flush_interval
100
+ send_batch(uri, batch)
101
+ batch.clear
102
+ last_flush = Time.now
103
+ end
104
+ end
105
+
106
+ # Final flush
107
+ send_batch(uri, batch) unless batch.empty?
108
+
109
+ running = false
110
+ accept_thread.kill
111
+ server.close rescue nil
112
+ FileUtils.rm_f(socket_path)
@@ -85,10 +85,6 @@ module Onlylogs
85
85
  end
86
86
  end
87
87
 
88
- def self.allowed_files
89
- configuration.allowed_files
90
- end
91
-
92
88
  def self.default_log_file_path
93
89
  configuration.default_log_file_path
94
90
  end
@@ -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.2"
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.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi
@@ -27,7 +27,8 @@ description: This gem includes all the tools needed to view and stream you log f
27
27
  directly on a web interface.
28
28
  email:
29
29
  - alessandro.rodi@renuo.ch
30
- executables: []
30
+ executables:
31
+ - onlylogs_sidecar
31
32
  extensions: []
32
33
  extra_rdoc_files: []
33
34
  files:
@@ -55,29 +56,24 @@ files:
55
56
  - app/javascript/onlylogs/controllers/text_selection_controller.js
56
57
  - app/jobs/onlylogs/application_job.rb
57
58
  - app/models/onlylogs/ansi_color_parser.rb
58
- - app/models/onlylogs/application_record.rb
59
59
  - app/models/onlylogs/batch_sender.rb
60
60
  - app/models/onlylogs/file.rb
61
61
  - app/models/onlylogs/file_path_parser.rb
62
62
  - app/models/onlylogs/grep.rb
63
- - app/models/onlylogs/log_line.rb
64
63
  - app/models/onlylogs/secure_file_path.rb
65
64
  - app/views/home/show.html.erb
66
65
  - app/views/layouts/onlylogs/application.html.erb
67
66
  - app/views/onlylogs/logs/index.html.erb
68
67
  - app/views/onlylogs/shared/_log_container.html.erb
69
68
  - app/views/onlylogs/shared/_log_container_styles.html.erb
69
+ - bin/onlylogs_sidecar
70
70
  - config/importmap.rb
71
- - config/puma_plugins/vector.rb
72
71
  - config/routes.rb
73
- - config/udp_logger.rb
74
- - config/vector.toml
75
72
  - db/migrate/20250902112548_create_books.rb
76
73
  - lib/onlylogs.rb
77
74
  - lib/onlylogs/configuration.rb
78
75
  - lib/onlylogs/engine.rb
79
76
  - lib/onlylogs/formatter.rb
80
- - lib/onlylogs/log_silencer_middleware.rb
81
77
  - lib/onlylogs/logger.rb
82
78
  - lib/onlylogs/socket_logger.rb
83
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,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "puma/plugin"
4
- require "rbconfig"
5
- require "timeout"
6
- require "shellwords"
7
-
8
- Puma::Plugin.create do
9
- def start(launcher)
10
- @launcher = launcher
11
- @events = launcher.events
12
- @options = launcher.config.options
13
- @vector_pid = nil
14
-
15
- setup_paths
16
- start_vector
17
- register_hooks
18
- end
19
-
20
- private
21
-
22
- def setup_paths
23
- @app_root = @options[:directory] || Dir.pwd
24
- @vector_bin = env_or_option("ONLYLOGS_VECTOR_BIN", :onlylogs_vector_bin, "vector")
25
- @vector_config = env_or_option(
26
- "ONLYLOGS_VECTOR_CONFIG",
27
- :onlylogs_vector_config,
28
- File.expand_path("../vector.toml", __dir__)
29
- )
30
- @vector_args = env_or_option("ONLYLOGS_VECTOR_ARGS", :onlylogs_vector_args, "")
31
- @dsn = env_or_option("ONLYLOGS_DSN", :onlylogs_dsn, "https://onlylogs.io/drain/testmac")
32
- end
33
-
34
- def register_hooks
35
- events = @launcher.events
36
- events.register(:on_restart) { restart_vector }
37
- at_exit { stop_vector }
38
- end
39
-
40
- def env_or_option(env_key, option_key, default)
41
- ENV.fetch(env_key, @options.fetch(option_key, default))
42
- end
43
-
44
- def start_vector
45
- stop_vector if @vector_pid
46
-
47
- unless File.exist?(@vector_config)
48
- warn "Vector config not found at #{@vector_config}; skipping start"
49
- return
50
- end
51
-
52
- args = [ @vector_bin, "--config", @vector_config ]
53
- args += Shellwords.split(@vector_args.to_s) unless @vector_args.to_s.empty?
54
-
55
- info "Starting Vector sidecar (config: #{@vector_config}, dsn: #{@dsn})"
56
- env = { "ONLYLOGS_DSN" => @dsn }
57
- @vector_pid = Process.spawn(env, *args, chdir: @app_root, pgroup: true)
58
- rescue Errno::ENOENT => e
59
- error "Unable to start Vector sidecar: #{e.message}"
60
- end
61
-
62
- def restart_vector
63
- info "Restarting Vector sidecar"
64
- start_vector
65
- end
66
-
67
- def stop_vector
68
- return unless @vector_pid
69
-
70
- info "Stopping Vector sidecar"
71
- pgid = Process.getpgid(@vector_pid)
72
- Process.kill("TERM", -pgid)
73
- Timeout.timeout(5) { Process.wait(@vector_pid) }
74
- rescue Errno::ESRCH, Errno::ECHILD
75
- # already stopped
76
- rescue Timeout::Error
77
- warn "Vector sidecar did not stop in time, killing"
78
- Process.kill("KILL", -pgid) rescue nil
79
- ensure
80
- @vector_pid = nil
81
- end
82
-
83
- def info(message)
84
- @events.log("[VectorSidecar] #{message}")
85
- end
86
-
87
- def warn(message)
88
- @events.log("[VectorSidecar][WARN] #{message}")
89
- end
90
-
91
- def error(message)
92
- @events.error("[VectorSidecar][ERROR] #{message}")
93
- end
94
- end
data/config/udp_logger.rb DELETED
@@ -1,40 +0,0 @@
1
- # udp_logger.rb
2
- require "logger"
3
- require "socket"
4
-
5
- class UdpLogger < Logger
6
- def initialize(host: "127.0.0.1", port: 6000, local_fallback: $stdout)
7
- # Use a normal Logger underneath so we still see logs locally
8
- super(local_fallback)
9
-
10
- @udp_host = host
11
- @udp_port = port
12
- @socket = UDPSocket.new
13
- end
14
-
15
- # Override Logger#add, which all the level methods delegate to
16
- def add(severity, message = nil, progname = nil, &block)
17
- # Same semantics as Logger:
18
- if message.nil?
19
- if block_given?
20
- message = block.call
21
- else
22
- message = progname
23
- progname = nil
24
- end
25
- end
26
-
27
- # Send plain text over UDP to Vector
28
- begin
29
- payload = message.to_s
30
- @socket.send(payload, 0, @udp_host, @udp_port)
31
- rescue => e
32
- # Swallow UDP errors so logging never crashes the app
33
- warn "UDP logger error: #{e.class}: #{e.message}"
34
- end
35
-
36
- # Also log locally (stdout / file) via normal Logger behavior
37
- super(severity, message, progname, &block)
38
- end
39
- end
40
-
data/config/vector.toml DELETED
@@ -1,32 +0,0 @@
1
- # Where Vector keeps internal state (buffers, etc.)
2
- data_dir = "/usr/local/var/lib/vector"
3
-
4
- # --- 1) SOURCE: UDP socket listening on localhost:6000 ---
5
-
6
- [sources.udp_logs]
7
- type = "socket"
8
- mode = "udp" # UDP mode
9
- address = "127.0.0.1:6000"
10
-
11
- # --- 2) SINK: console (for debugging, optional but very handy) ---
12
-
13
- [sinks.console]
14
- type = "console"
15
- inputs = ["udp_logs"]
16
- encoding.codec = "json"
17
- target = "stdout"
18
-
19
- # --- 3) SINK: Onlylogs HTTP drain ---
20
-
21
- [sinks.onlylogs]
22
- type = "http"
23
- inputs = ["udp_logs"] # consume events from udp_logs
24
- method = "post"
25
- uri = "${ONLYLOGS_DSN}"
26
-
27
- encoding.codec = "text"
28
-
29
- #[sinks.onlylogs.request]
30
- # Adjust headers if Onlylogs expects something specific
31
- #headers.Content-Type = "application/json"
32
-
@@ -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