onlylogs 0.1.3 → 0.3.0

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: 54d4a10b4637cbbcf3daafb56170213a8bba06696127e2e87def0d71e50adc2d
4
+ data.tar.gz: 36fb3bca8a7520c90396f5791485c6566c63a8b9b07abb9e4611dca7982ac2ee
5
5
  SHA512:
6
- metadata.gz: 7079c7f67be0048382b9029ef636a17c6de654c49ae1c17346cfbfad99e7015a1ffed9a1ef1c4313b45f6cad2bed839dc804f39b616f46bb55d4203c3cde8bc8
7
- data.tar.gz: 4630f69bf7066024413e4251747cc0da907f8d7912eab6eae78fb1c4b9ac3fa4d2bb026e0cc5e9452e9e78c289bf120c82724207426f88e7886b6ea92077d337
6
+ metadata.gz: 43e0af4fc1f92eb0a5b8c4ef7cea17310811bb816b942438dc72c1182c70ef76b4201c6d53d9d8f0f560bcf698b9e5f8119556404ca6da6917ff87ac6267142a
7
+ data.tar.gz: 6a6f1ac76f19c9c718064012ba3b3d9556ddd0b27b13c6ba1d58d48454fa454c8d3dbe564841cd20d9b7f14621d872d0bdc160ab6cfa1795e27725be6d6d2f7c
data/README.md CHANGED
@@ -12,7 +12,13 @@ stream them to https://onlylogs.io and continue enjoying the same features.
12
12
  > [!IMPORTANT]
13
13
  > https://onlylogs.io is still in beta. Send us an email to a@renuo.ch if you want access to the platform.
14
14
 
15
- ## Installation
15
+ ## Installation as self-hosted
16
+
17
+ If you already have a disk, you can just keep there also your log files (as well as you probably already do).
18
+
19
+ This section explains how to setup onlylogs to self host your logs and access them directly from your Rails app.
20
+
21
+ If instead you want to stream your logs to https://onlylogs.io, head to the onlylogs.io instructions page.
16
22
 
17
23
  Add this line to your application's Gemfile:
18
24
 
@@ -63,6 +69,18 @@ Please be sure to secure them properly.
63
69
  > [!IMPORTANT]
64
70
  > By default, onlylogs endpoints are completely inaccessible until basic auth credentials are configured.
65
71
 
72
+ ### Notes about Docker
73
+
74
+ If your app is running in a Docker container, for example with Kamal, remember to mount your logs folder:
75
+
76
+ ```yaml
77
+ # config/deploy.yml
78
+ volumes:
79
+ - "storage:/rails/storage"
80
+ - "cache:/rails/tmp/cache"
81
+ - "logs:/rails/log"
82
+ ```
83
+
66
84
  ### Basic Authentication Setup
67
85
 
68
86
  Credentials can be configured using environment variables, Rails credentials, or programmatically.
@@ -254,6 +272,18 @@ Onlylogs.configure do |config|
254
272
  end
255
273
  ```
256
274
 
275
+ ### Filtering Log Lines with a Denylist
276
+
277
+ The `Onlylogs::Formatter` supports a denylist: an array of regular expressions that prevents matching lines from being logged. This is useful for filtering out noisy or irrelevant entries like health checks or asset requests.
278
+
279
+ ```ruby
280
+ # config/environments/production.rb
281
+ config.logger = Onlylogs::Logger.new(Rails.root.join("log", "production.log"))
282
+ config.logger.formatter.denylist = [/health_check/, /ping/, /\.css\z/]
283
+ ```
284
+
285
+ Any log message matching at least one pattern in the denylist will be silently dropped.
286
+
257
287
  ## Development & Contributing
258
288
 
259
289
  You are more than welcome to help and contribute to this package.
@@ -292,6 +322,23 @@ For testing how onlylogs behaves under production-like network conditions, you c
292
322
  ./bin/simulate_latency disable
293
323
  ```
294
324
 
325
+ ### Performance Testing
326
+
327
+ Performance tests require large log files that are not included in the repository. You can download them using the provided script:
328
+
329
+ ```bash
330
+ bin/download_performance_fixtures
331
+ ```
332
+
333
+ Once the fixtures are downloaded, you can run the performance tests locally:
334
+
335
+ ```bash
336
+ bin/rails test test/models/onlylogs/grep_performance_test.rb
337
+ ```
338
+
339
+ > [!NOTE]
340
+ > Performance tests are automatically skipped in CI environments or if the large fixture files are missing.
341
+
295
342
  ### Plans for the future
296
343
 
297
344
  We believe that by simply analysing your logs you can also have a fancy errors report.
@@ -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
  }
@@ -35,7 +35,7 @@ module Onlylogs
35
35
  }.freeze
36
36
 
37
37
  # Pre-built closing span (frozen for better performance)
38
- CLOSING_SPAN = '</span>'.freeze
38
+ CLOSING_SPAN = "</span>".freeze
39
39
 
40
40
  def self.parse(string)
41
41
  return string if string.blank?
@@ -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)
@@ -43,13 +55,13 @@ module Onlylogs
43
55
 
44
56
  def send_batch
45
57
  lines_to_send = nil
46
-
58
+
47
59
  @mutex.synchronize do
48
60
  return if @buffer.empty?
49
61
  lines_to_send = @buffer.dup
50
62
  @buffer.clear
51
63
  end
52
-
64
+
53
65
  return if lines_to_send.empty?
54
66
 
55
67
  @channel.send(:transmit, {
@@ -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
 
@@ -9,10 +9,10 @@ module Onlylogs
9
9
  { symbols: [ :atom ], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
10
10
  { symbols: [ :emacs, :emacsclient ], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
11
11
  { symbols: [ :idea ], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
12
- { symbols: [ :macvim, :mvim ], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
13
- { symbols: [ :rubymine ], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
12
+ { symbols: [ :macvim, :mvim, :vim ], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
13
+ { symbols: [ :rubymine, :mine ], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
14
14
  { symbols: [ :sublime, :subl, :st ], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
15
- { symbols: [ :textmate, :txmt, :tm ], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
15
+ { symbols: [ :textmate, :txmt, :tm, :mate ], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
16
16
  { symbols: [ :vscode, :code ], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
17
17
  { symbols: [ :vscodium, :codium ], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" }
18
18
  ].freeze
@@ -46,7 +46,11 @@ module Onlylogs
46
46
 
47
47
  def self.for_formatting_string(formatting_string)
48
48
  new proc { |file, line|
49
- formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
49
+ formatting_string % {
50
+ file: URI.encode_www_form_component(file),
51
+ file_unencoded: file,
52
+ line: line
53
+ }
50
54
  }
51
55
  end
52
56
 
@@ -57,11 +61,18 @@ module Onlylogs
57
61
  # Cache for the editor instance
58
62
  @cached_editor_instance = nil
59
63
 
64
+ def self.clear_editor_cache
65
+ @cached_editor_instance = nil
66
+ end
67
+
60
68
  def self.cached_editor_instance
61
- return @cached_editor_instance if @cached_editor_instance
62
- @cached_editor_instance = editor_from_symbol(Onlylogs.editor)
69
+ @cached_editor_instance ||= if ENV["ONLYLOGS_EDITOR_URL"]
70
+ for_formatting_string(ENV["ONLYLOGS_EDITOR_URL"])
71
+ else
72
+ editor_from_symbol(Onlylogs.editor)
73
+ end
63
74
  end
64
-
75
+
65
76
 
66
77
  def self.editor_from_symbol(symbol)
67
78
  KNOWN_EDITORS.each do |preset|
@@ -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
@@ -7,7 +7,9 @@
7
7
 
8
8
  <%= yield :head %>
9
9
 
10
- <%= javascript_importmap_tags "application", importmap: Onlylogs.importmap %>
10
+ <% if defined?(Importmap) %>
11
+ <%= javascript_importmap_tags "application", importmap: Onlylogs.importmap %>
12
+ <% end %>
11
13
 
12
14
  <style>
13
15
  html, body {
@@ -1,32 +1,33 @@
1
1
  <%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true) %>
2
- <script src="https://cdn.jsdelivr.net/npm/clusterize.js@0.18.1/clusterize.min.js"></script>
2
+ <script nonce="<%= request.content_security_policy_nonce %>"
3
+ src="https://cdn.jsdelivr.net/npm/clusterize.js@0.18.1/clusterize.min.js"></script>
3
4
  <%= render "onlylogs/shared/log_container_styles" %>
4
5
 
5
6
  <%
6
7
  mode = filter.blank? ? "live" : "search"
7
8
  cursor_position = mode == "search" ? 0 : [File.size(log_file_path) - (tail * 100), 0].max
8
-
9
+
9
10
  raise SecurityError, "File path not allowed" unless Onlylogs.allowed_file_path?(log_file_path)
10
-
11
+
11
12
  encrypted_log_file_path = Onlylogs::SecureFilePath.encrypt(log_file_path)
12
13
  %>
13
14
 
14
15
  <div data-controller="log-streamer text-selection keyboard-shortcuts"
15
- data-log-streamer-file-path-value="<%= encrypted_log_file_path %>"
16
+ data-log-streamer-file-path-value="<%= encrypted_log_file_path %>"
16
17
  data-log-streamer-cursor-position-value="<%= cursor_position %>"
17
18
  data-log-streamer-filter-value="<%= filter %>"
18
- data-log-streamer-auto-scroll-value="<%= autoscroll %>"
19
+ data-log-streamer-auto-scroll-value="<%= autoscroll %>"
19
20
  data-log-streamer-mode-value="<%= mode %>"
20
21
  class="onlylogs-log-container" >
21
22
  <div data-log-streamer-target="logLines" data-text-selection-target="logLines" id="scrollArea" class="onlylogs-log-lines clusterize-scroll">
22
23
  <div id="contentArea" class="clusterize-content">
23
24
  </div>
24
25
  </div>
25
-
26
- <button type="button"
27
- data-text-selection-target="button"
28
- class="onlylogs-context-menu"
29
- data-action="click->text-selection#searchSelectedText"
26
+
27
+ <button type="button"
28
+ data-text-selection-target="button"
29
+ class="onlylogs-context-menu"
30
+ data-action="click->text-selection#searchSelectedText"
30
31
  title="Search selected text"
31
32
  style="display: none;">
32
33
  🔍 Search
@@ -55,15 +56,15 @@
55
56
  <label style="margin-bottom: 0;">
56
57
  Filter:
57
58
  <div style="display: inline-flex; align-items: center; position: relative;">
58
- <input type="text"
59
- name="filter"
59
+ <input type="text"
60
+ name="filter"
60
61
  value="<%= filter %>"
61
62
  placeholder="Enter filter text..."
62
63
  data-log-streamer-target="filterInput"
63
64
  data-text-selection-target="filterInput"
64
65
  data-action="input->log-streamer#applyFilter"
65
66
  style="padding-right: 1.5rem;">
66
- <button type="button"
67
+ <button type="button"
67
68
  data-action="click->log-streamer#clearFilter"
68
69
  class="clear-filter-button"
69
70
  title="Clear filter">
@@ -73,7 +74,7 @@
73
74
  </label>
74
75
  </div>
75
76
  <div>
76
- <button type="button"
77
+ <button type="button"
77
78
  data-log-streamer-target="stopButton"
78
79
  data-action="click->log-streamer#stopSearch"
79
80
  class="stop-search-button"
@@ -83,7 +84,7 @@
83
84
  </button>
84
85
  </div>
85
86
  <div>
86
- <span data-log-streamer-target="lineRange" style="color: #666;">No lines</span>
87
+ <span data-log-streamer-target="results" style="color: #666;">Results: 0</span>
87
88
  </div>
88
89
  <div data-log-streamer-target="message"></div>
89
90
  <% unless Onlylogs.ripgrep_enabled? %>
@@ -93,7 +94,7 @@
93
94
  <%= number_to_human_size(File.size(log_file_path)) %>
94
95
  </div>
95
96
  <div style="width:67px">
96
- <button type="button"
97
+ <button type="button"
97
98
  data-log-streamer-target="clearButton"
98
99
  data-action="click->log-streamer#clearLogs"
99
100
  class="clear-logs-button"
@@ -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
  }
data/config/importmap.rb CHANGED
@@ -3,4 +3,3 @@ pin "@rails/actioncable", to: "actioncable.esm.js"
3
3
  pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
4
4
  pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
5
5
  pin_all_from Onlylogs::Engine.root.join("app/javascript/onlylogs/controllers"), under: "controllers", to: "onlylogs/controllers"
6
-
@@ -14,7 +14,7 @@ module Onlylogs
14
14
  @parent_controller = nil
15
15
  @disable_basic_authentication = false
16
16
  @ripgrep_enabled = default_ripgrep_enabled
17
- @editor = default_editor
17
+ @editor = nil
18
18
  @max_line_matches = 100000
19
19
  end
20
20
 
@@ -26,20 +26,20 @@ module Onlylogs
26
26
  if (credentials_editor = Rails.application.credentials.dig(:onlylogs, :editor))
27
27
  return credentials_editor
28
28
  end
29
-
29
+
30
30
  # 2. Check environment variables (ONLYLOGS_EDITOR > RAILS_EDITOR > EDITOR)
31
31
  if ENV["ONLYLOGS_EDITOR"]
32
32
  return ENV["ONLYLOGS_EDITOR"].to_sym
33
33
  end
34
-
34
+
35
35
  if ENV["RAILS_EDITOR"]
36
36
  return ENV["RAILS_EDITOR"].to_sym
37
37
  end
38
-
38
+
39
39
  if ENV["EDITOR"]
40
40
  return ENV["EDITOR"].to_sym
41
41
  end
42
-
42
+
43
43
  # 3. Default fallback
44
44
  :vscode
45
45
  end
@@ -114,7 +114,7 @@ module Onlylogs
114
114
  end
115
115
 
116
116
  def self.editor
117
- configuration.default_editor
117
+ configuration.editor || configuration.default_editor
118
118
  end
119
119
 
120
120
  def self.editor=(editor_symbol)
@@ -1,4 +1,8 @@
1
- require "importmap-rails"
1
+ begin
2
+ require "importmap-rails"
3
+ rescue LoadError
4
+ # importmap-rails is optional
5
+ end
2
6
 
3
7
  module Onlylogs
4
8
  class Engine < ::Rails::Engine
@@ -14,26 +18,17 @@ module Onlylogs
14
18
  app.config.assets.precompile += %w[ onlylogs_manifest ]
15
19
  end
16
20
 
21
+ if defined?(Importmap)
22
+ initializer "onlylogs.importmap", after: "importmap" do |app|
23
+ Onlylogs.importmap.draw(root.join("config/importmap.rb"))
24
+ if app.config.importmap.sweep_cache && app.config.reloading_enabled?
25
+ Onlylogs.importmap.cache_sweeper(watches: root.join("app/javascript"))
17
26
 
18
- initializer "onlylogs.importmap", after: "importmap" do |app|
19
- Onlylogs.importmap.draw(root.join("config/importmap.rb"))
20
- if app.config.importmap.sweep_cache && app.config.reloading_enabled?
21
- Onlylogs.importmap.cache_sweeper(watches: root.join("app/javascript"))
22
-
23
- ActiveSupport.on_load(:action_controller_base) do
24
- before_action { Onlylogs.importmap.cache_sweeper.execute_if_updated }
27
+ ActiveSupport.on_load(:action_controller_base) do
28
+ before_action { Onlylogs.importmap.cache_sweeper.execute_if_updated }
29
+ end
25
30
  end
26
31
  end
27
32
  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
33
  end
39
34
  end
@@ -2,8 +2,16 @@ module Onlylogs
2
2
  class Formatter < ActiveSupport::Logger::SimpleFormatter
3
3
  include ActiveSupport::TaggedLogging::Formatter
4
4
 
5
+ attr_accessor :denylist
6
+
7
+ def initialize
8
+ super
9
+ @denylist = []
10
+ end
11
+
5
12
  def call(severity, time, progname, msg)
6
13
  return nil if "Onlylogs::LogsChannel".in?(msg)
14
+ return nil if denylist.any? { |pattern| pattern.match?(msg) }
7
15
  tags = [ time.iso8601, severity[0].upcase ]
8
16
  push_tags tags
9
17
  str = super
@@ -18,7 +18,6 @@ module Onlylogs
18
18
  end
19
19
 
20
20
  def add(severity, message = nil, progname = nil, &block)
21
-
22
21
  if message.nil?
23
22
  if block_given?
24
23
  message = block.call
@@ -44,7 +43,7 @@ module Onlylogs
44
43
  puts "Onlylogs::SocketLogger error: #{e.message}"
45
44
  reconnect_socket
46
45
  rescue => e
47
- puts"Onlylogs::SocketLogger unexpected error: #{e.class}: #{e.message}"
46
+ puts "Onlylogs::SocketLogger unexpected error: #{e.class}: #{e.message}"
48
47
  reconnect_socket
49
48
  end
50
49
 
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.1.3"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/onlylogs.rb CHANGED
@@ -13,5 +13,7 @@ require "onlylogs/socket_logger"
13
13
  # loader.setup
14
14
 
15
15
  module Onlylogs
16
- mattr_accessor :importmap, default: Importmap::Map.new
16
+ if defined?(Importmap)
17
+ mattr_accessor :importmap, default: Importmap::Map.new
18
+ end
17
19
  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.3.0
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