onlylogs 0.9.0 → 0.11.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 +39 -2
- data/app/channels/onlylogs/logs_channel.rb +90 -70
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +15 -11
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +177 -203
- data/app/javascript/onlylogs/controllers/range_slider_controller.js +96 -0
- data/app/javascript/onlylogs/controllers/text_selection_controller.js +1 -0
- data/app/models/onlylogs/file.rb +13 -2
- data/app/models/onlylogs/grep.rb +14 -6
- data/app/views/onlylogs/logs/index.html.erb +25 -5
- data/app/views/onlylogs/shared/_log_container.html.erb +84 -37
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +247 -49
- data/app/views/onlylogs/shared/_range_slider.html.erb +9 -5
- data/lib/onlylogs/continuous_log_writer.rb +161 -0
- data/lib/onlylogs/formatter.rb +4 -3
- data/lib/onlylogs/http_device.rb +319 -84
- data/lib/onlylogs/http_logger.rb +4 -7
- data/lib/onlylogs/spool.rb +61 -13
- data/lib/onlylogs/version.rb +1 -1
- metadata +3 -1
data/app/models/onlylogs/file.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Onlylogs
|
|
|
8
8
|
self.path = path
|
|
9
9
|
self.last_position = last_position
|
|
10
10
|
validate!
|
|
11
|
+
@inode = ::File.stat(path).ino
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def go_to_position(position)
|
|
@@ -16,11 +17,12 @@ module Onlylogs
|
|
|
16
17
|
self.last_position = position
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# Yields new lines until the file is rotated away from under its name; then
|
|
21
|
+
# returns, since the position no longer means anything on the file now there.
|
|
19
22
|
def watch(&block)
|
|
20
|
-
# return enum_for(:watch) unless block
|
|
21
|
-
|
|
22
23
|
loop do
|
|
23
24
|
sleep 0.5
|
|
25
|
+
return if rotated?
|
|
24
26
|
|
|
25
27
|
new_lines = read_new_lines
|
|
26
28
|
next if new_lines.empty?
|
|
@@ -29,6 +31,15 @@ module Onlylogs
|
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
# A rename rotation puts another inode under the name, a copytruncate rotation
|
|
35
|
+
# keeps the inode but makes the file smaller than what was already read.
|
|
36
|
+
def rotated?
|
|
37
|
+
return true unless exist?
|
|
38
|
+
|
|
39
|
+
stat = ::File.stat(path)
|
|
40
|
+
stat.ino != @inode || stat.size < last_position
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
def size
|
|
33
44
|
::File.size(path)
|
|
34
45
|
end
|
data/app/models/onlylogs/grep.rb
CHANGED
|
@@ -94,7 +94,8 @@ module Onlylogs
|
|
|
94
94
|
reader, writer = IO.pipe
|
|
95
95
|
|
|
96
96
|
begin
|
|
97
|
-
pid = Process.spawn(*bounded(command_args, timeout),
|
|
97
|
+
pid = Process.spawn(*deprioritised(bounded(command_args, timeout)),
|
|
98
|
+
out: writer, err: ::File::NULL, pgroup: true)
|
|
98
99
|
rescue
|
|
99
100
|
reader.close
|
|
100
101
|
raise
|
|
@@ -116,6 +117,12 @@ module Onlylogs
|
|
|
116
117
|
TIMED_OUT_EXIT_STATUS = 124
|
|
117
118
|
KILL_GRACE_PERIOD = 0.5
|
|
118
119
|
|
|
120
|
+
SEARCH_NICENESS = 19
|
|
121
|
+
|
|
122
|
+
def self.deprioritised(command_args)
|
|
123
|
+
["nice", "-n", SEARCH_NICENESS.to_s, *command_args]
|
|
124
|
+
end
|
|
125
|
+
|
|
119
126
|
def self.bounded(command_args, timeout)
|
|
120
127
|
return command_args unless timeout && timeout_command_available?
|
|
121
128
|
|
|
@@ -161,17 +168,18 @@ module Onlylogs
|
|
|
161
168
|
nil
|
|
162
169
|
end
|
|
163
170
|
|
|
171
|
+
MATCH_TIMEOUT = 0.1 # seconds
|
|
172
|
+
|
|
164
173
|
def self.match_line?(line, string, regexp_mode: false)
|
|
165
174
|
# Strip ANSI color codes from the line before matching
|
|
166
175
|
stripped_line = line.gsub(/\e\[[0-9;]*m/, "")
|
|
167
176
|
# Normalize multiple spaces to single spaces
|
|
168
177
|
normalized_line = stripped_line.gsub(/\s+/, " ")
|
|
169
178
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
end
|
|
179
|
+
pattern = regexp_mode ? string : Regexp.escape(string)
|
|
180
|
+
normalized_line.match?(Regexp.new(pattern, timeout: MATCH_TIMEOUT))
|
|
181
|
+
rescue ::RegexpError
|
|
182
|
+
false
|
|
175
183
|
end
|
|
176
184
|
end
|
|
177
185
|
end
|
|
@@ -1,22 +1,42 @@
|
|
|
1
1
|
<style>
|
|
2
2
|
.grid {
|
|
3
3
|
display: grid;
|
|
4
|
-
grid-template-rows:
|
|
4
|
+
grid-template-rows: 38px 1fr;
|
|
5
5
|
height: 100vh;
|
|
6
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
|
7
|
+
font-size: 0.8rem;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
.grid-header {
|
|
9
11
|
display: flex;
|
|
10
12
|
align-items: center;
|
|
11
|
-
|
|
13
|
+
gap: 0.5rem;
|
|
14
|
+
padding: 0 0.65rem;
|
|
15
|
+
background-color: #f3f5f9;
|
|
16
|
+
border-bottom: 1px solid #dde3ec;
|
|
17
|
+
color: #58626f;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
.log-file-dropdown {
|
|
15
|
-
|
|
20
|
+
.log-file-dropdown select {
|
|
21
|
+
font: inherit;
|
|
22
|
+
padding: 0.2rem 0.4rem;
|
|
23
|
+
border: 1px solid #b9c4d3;
|
|
24
|
+
border-radius: 3px;
|
|
25
|
+
background-color: #fff;
|
|
26
|
+
color: #12171f;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
.download-link {
|
|
19
|
-
|
|
30
|
+
color: #1a4fc4;
|
|
31
|
+
text-decoration: none;
|
|
32
|
+
padding: 0.2rem 0.45rem;
|
|
33
|
+
border: 1px solid #9dbcf7;
|
|
34
|
+
border-radius: 3px;
|
|
35
|
+
background-color: #e8f0fe;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.download-link:hover {
|
|
39
|
+
background-color: #d7e5fd;
|
|
20
40
|
}
|
|
21
41
|
</style>
|
|
22
42
|
<div class="grid">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (log_file_path:) %>
|
|
1
|
+
<%# locals: (log_file_path:, live_enabled: true) %>
|
|
2
2
|
<script nonce="<%= request.content_security_policy_nonce %>"
|
|
3
3
|
src="<%= asset_path('onlylogs/clusterize.js') %>"></script>
|
|
4
4
|
<link rel="stylesheet" href="<%= asset_path('onlylogs/clusterize.css') %>" />
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
autoscroll = local_assigns[:autoscroll] != false
|
|
9
9
|
filter = local_assigns[:filter] || ""
|
|
10
10
|
regexp_mode = local_assigns[:regexp_mode] == true
|
|
11
|
-
|
|
11
|
+
# A file nothing writes to any more (no drain attached, a rotated log) has nothing
|
|
12
|
+
# to follow, so live mode is not offered at all and the viewer opens in search.
|
|
13
|
+
mode = live_enabled ? (local_assigns[:mode] || "live") : "static"
|
|
12
14
|
file_size = File.size(log_file_path)
|
|
13
15
|
start_position = local_assigns[:start_position]
|
|
14
16
|
end_position = local_assigns[:end_position]
|
|
@@ -27,14 +29,28 @@
|
|
|
27
29
|
data-log-streamer-auto-scroll-value="<%= autoscroll %>"
|
|
28
30
|
data-log-streamer-regexp-mode-value="<%= regexp_mode %>"
|
|
29
31
|
data-log-streamer-mode-value="<%= mode %>"
|
|
32
|
+
data-log-streamer-live-enabled-value="<%= live_enabled %>"
|
|
30
33
|
data-log-streamer-file-size-value="<%= file_size %>"
|
|
31
|
-
data-action="text-selection:start->log-streamer#pauseForSelection"
|
|
34
|
+
data-action="text-selection:start->log-streamer#pauseForSelection text-selection:search->log-streamer#switchToSearch"
|
|
32
35
|
class="onlylogs-log-container" >
|
|
33
36
|
<div data-log-streamer-target="logLines" data-text-selection-target="logLines" id="scrollArea" class="onlylogs-log-lines clusterize-scroll">
|
|
34
37
|
<div id="contentArea" class="clusterize-content">
|
|
35
38
|
</div>
|
|
36
39
|
</div>
|
|
37
40
|
|
|
41
|
+
<%# Search with an empty query would stream the entire file, so it is not run
|
|
42
|
+
at all until something is typed. This says so instead of looking idle. %>
|
|
43
|
+
<div class="onlylogs-search-placeholder" data-log-streamer-target="searchPlaceholder" hidden>
|
|
44
|
+
<p class="onlylogs-search-placeholder__title">Nothing to search for yet</p>
|
|
45
|
+
<p class="onlylogs-search-placeholder__hint">
|
|
46
|
+
<% if live_enabled %>
|
|
47
|
+
Type a query to search this file, or switch to Live to follow it as it is written.
|
|
48
|
+
<% else %>
|
|
49
|
+
Type a query to search this file.
|
|
50
|
+
<% end %>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
38
54
|
<button type="button"
|
|
39
55
|
data-text-selection-target="button"
|
|
40
56
|
class="onlylogs-context-menu hidden-button"
|
|
@@ -44,32 +60,40 @@
|
|
|
44
60
|
</button>
|
|
45
61
|
|
|
46
62
|
<div class="onlylogs-log-toolbar">
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
<div class="onlylogs-toolbar-row onlylogs-toolbar-row--controls">
|
|
64
|
+
<% if live_enabled %>
|
|
65
|
+
<div class="onlylogs-mode-switch" role="group" aria-label="Log mode">
|
|
66
|
+
<button type="button"
|
|
67
|
+
class="onlylogs-mode-switch__button"
|
|
68
|
+
data-onlylogs-mode-button="live"
|
|
69
|
+
data-log-streamer-target="liveButton"
|
|
70
|
+
data-keyboard-shortcuts-target="liveButton"
|
|
71
|
+
data-action="click->log-streamer#switchToLive"
|
|
72
|
+
aria-pressed="<%= mode == "live" %>"
|
|
73
|
+
title="Follow the file as it is written (L)">
|
|
74
|
+
<span>▶ <u>L</u>ive</span>
|
|
75
|
+
</button>
|
|
76
|
+
<button type="button"
|
|
77
|
+
class="onlylogs-mode-switch__button"
|
|
78
|
+
data-onlylogs-mode-button="static"
|
|
79
|
+
data-log-streamer-target="searchButton"
|
|
80
|
+
data-keyboard-shortcuts-target="searchButton"
|
|
81
|
+
data-action="click->log-streamer#switchToSearch"
|
|
82
|
+
aria-pressed="<%= mode != "live" %>"
|
|
83
|
+
title="Search what is already in the file (S)">
|
|
84
|
+
<span>🔍 <u>S</u>earch</span>
|
|
85
|
+
</button>
|
|
64
86
|
</div>
|
|
87
|
+
<% end %>
|
|
65
88
|
<div>
|
|
66
89
|
<label class="toolbar-label">
|
|
67
|
-
|
|
90
|
+
<span data-onlylogs-mode="live">Follow:</span>
|
|
91
|
+
<span data-onlylogs-mode="static">Query:</span>
|
|
68
92
|
<div class="filter-input-wrapper">
|
|
69
93
|
<input type="text"
|
|
70
94
|
name="filter"
|
|
71
95
|
value="<%= filter %>"
|
|
72
|
-
placeholder="
|
|
96
|
+
placeholder="<%= mode == "live" ? "follow lines matching…" : "search the whole file…" %>"
|
|
73
97
|
data-log-streamer-target="filterInput"
|
|
74
98
|
data-text-selection-target="filterInput"
|
|
75
99
|
data-action="input->log-streamer#applyFilter">
|
|
@@ -82,7 +106,28 @@
|
|
|
82
106
|
</div>
|
|
83
107
|
</label>
|
|
84
108
|
</div>
|
|
85
|
-
<div
|
|
109
|
+
<div>
|
|
110
|
+
<label class="toolbar-label">
|
|
111
|
+
<input id="regexpMode" type="checkbox" <%= regexp_mode ? "checked" : "" %> name="regexpMode" data-log-streamer-target="regexpMode" data-text-selection-target="regexpMode" data-action="change->log-streamer#toggleRegexpMode">
|
|
112
|
+
<span><u>R</u>egexp</span>
|
|
113
|
+
</label>
|
|
114
|
+
</div>
|
|
115
|
+
<div data-onlylogs-mode="live">
|
|
116
|
+
<label class="toolbar-label">
|
|
117
|
+
<input id="autoscroll" type="checkbox" <%= autoscroll ? "checked" : "" %> name="autoscroll" data-log-streamer-target="autoscroll" data-keyboard-shortcuts-target="autoscroll" data-action="change->log-streamer#toggleAutoScroll">
|
|
118
|
+
<span><u>A</u>utoscroll</span>
|
|
119
|
+
</label>
|
|
120
|
+
</div>
|
|
121
|
+
<div data-onlylogs-mode="live">
|
|
122
|
+
<button type="button"
|
|
123
|
+
data-log-streamer-target="searchWholeFileButton"
|
|
124
|
+
class="search-whole-file-button"
|
|
125
|
+
data-action="click->log-streamer#searchWholeFile"
|
|
126
|
+
title="Search this text across the whole file">
|
|
127
|
+
Search whole file →
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="range-slider-wrapper" data-onlylogs-mode="static">
|
|
86
131
|
<%= render "onlylogs/shared/range_slider",
|
|
87
132
|
start_name: "start_position",
|
|
88
133
|
end_name: "end_position",
|
|
@@ -105,28 +150,30 @@
|
|
|
105
150
|
class="stop-search-button hidden-button"
|
|
106
151
|
data-action="click->log-streamer#stopSearch"
|
|
107
152
|
title="Stop current search">
|
|
108
|
-
|
|
153
|
+
■ Stop
|
|
109
154
|
</button>
|
|
110
155
|
</div>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="onlylogs-toolbar-row onlylogs-toolbar-row--status">
|
|
159
|
+
<span data-log-streamer-target="results" class="results-text">Results: 0</span>
|
|
160
|
+
<span data-log-streamer-target="message" class="status-message"></span>
|
|
161
|
+
<div class="onlylogs-toolbar-row__end">
|
|
162
|
+
<% unless Onlylogs.ripgrep_enabled? %>
|
|
163
|
+
<span class="grep-warning" title="Search is slow. Install ripgrep for better performance">⚠️</span>
|
|
164
|
+
<% end %>
|
|
165
|
+
<span class="file-size" title="File size: <%= number_to_human_size(File.size(log_file_path)) %>">
|
|
166
|
+
<%= number_to_human_size(File.size(log_file_path)) %>
|
|
167
|
+
</span>
|
|
122
168
|
<button type="button"
|
|
123
169
|
data-log-streamer-target="clearButton"
|
|
124
170
|
class="clear-logs-button"
|
|
125
171
|
data-action="click->log-streamer#clearLogs"
|
|
172
|
+
aria-label="Clear all log lines"
|
|
126
173
|
title="Clear all log lines">
|
|
127
|
-
|
|
174
|
+
🗑
|
|
128
175
|
</button>
|
|
176
|
+
<span data-log-streamer-target="websocketStatus" class="websocket-status websocket-status--disconnected" title="WebSocket Disconnected">🔴</span>
|
|
129
177
|
</div>
|
|
130
|
-
<div data-log-streamer-target="websocketStatus" class="websocket-status websocket-status--disconnected" title="WebSocket Disconnected">🔴</div>
|
|
131
178
|
</div>
|
|
132
179
|
</div>
|