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 +4 -4
- data/README.md +48 -1
- data/app/channels/onlylogs/logs_channel.rb +39 -29
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +9 -48
- data/app/models/onlylogs/ansi_color_parser.rb +1 -1
- data/app/models/onlylogs/batch_sender.rb +17 -5
- data/app/models/onlylogs/file.rb +20 -64
- data/app/models/onlylogs/file_path_parser.rb +18 -7
- data/app/models/onlylogs/grep.rb +8 -10
- data/app/views/layouts/onlylogs/application.html.erb +3 -1
- data/app/views/onlylogs/shared/_log_container.html.erb +17 -16
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +14 -5
- data/config/importmap.rb +0 -1
- data/lib/onlylogs/configuration.rb +6 -6
- data/lib/onlylogs/engine.rb +13 -18
- data/lib/onlylogs/formatter.rb +8 -0
- data/lib/onlylogs/socket_logger.rb +1 -2
- data/lib/onlylogs/version.rb +1 -1
- data/lib/onlylogs.rb +3 -1
- metadata +1 -4
- data/app/models/onlylogs/application_record.rb +0 -5
- data/app/models/onlylogs/log_line.rb +0 -24
- data/lib/onlylogs/log_silencer_middleware.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54d4a10b4637cbbcf3daafb56170213a8bba06696127e2e87def0d71e50adc2d
|
|
4
|
+
data.tar.gz: 36fb3bca8a7520c90396f5791485c6566c63a8b9b07abb9e4611dca7982ac2ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
129
|
+
# Wait for graceful shutdown
|
|
130
|
+
if @log_watcher_thread&.alive?
|
|
131
|
+
@log_watcher_thread.join(3)
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
165
|
+
line_count += 1
|
|
166
|
+
end
|
|
160
167
|
end
|
|
161
|
-
end
|
|
162
168
|
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
# Stop batch sender and flush any remaining lines
|
|
170
|
+
@batch_sender.stop
|
|
165
171
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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", "
|
|
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
|
-
|
|
184
|
-
|
|
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 (
|
|
292
|
-
this.clusterize.append(
|
|
293
|
-
this.#
|
|
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
|
-
#
|
|
316
|
+
#updateResultsDisplay() {
|
|
345
317
|
const resultsCount = this.clusterize.getRowsAmount();
|
|
346
|
-
|
|
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
|
-
|
|
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, {
|
data/app/models/onlylogs/file.rb
CHANGED
|
@@ -2,12 +2,11 @@ module Onlylogs
|
|
|
2
2
|
class Error < StandardError; end
|
|
3
3
|
|
|
4
4
|
class File
|
|
5
|
-
attr_reader :path, :last_position
|
|
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 |
|
|
62
|
-
yield
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
if
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 % {
|
|
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
|
-
|
|
62
|
-
|
|
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|
|
data/app/models/onlylogs/grep.rb
CHANGED
|
@@ -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
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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="
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 =
|
|
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)
|
data/lib/onlylogs/engine.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
data/lib/onlylogs/formatter.rb
CHANGED
|
@@ -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
|
|
data/lib/onlylogs/version.rb
CHANGED
data/lib/onlylogs.rb
CHANGED
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.
|
|
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,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
|