onlylogs 0.8.0 → 0.10.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 +27 -1
- data/app/channels/onlylogs/logs_channel.rb +87 -69
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +15 -11
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +170 -202
- 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/grep.rb +25 -7
- data/app/views/onlylogs/logs/index.html.erb +25 -5
- data/app/views/onlylogs/shared/_log_container.html.erb +71 -35
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +246 -49
- data/app/views/onlylogs/shared/_range_slider.html.erb +9 -5
- data/lib/onlylogs/configuration.rb +13 -1
- 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/grep.rb
CHANGED
|
@@ -29,7 +29,11 @@ module Onlylogs
|
|
|
29
29
|
->(line) { [nil, line] }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
matches = 0
|
|
33
|
+
|
|
34
|
+
ActiveSupport::Notifications.instrument("search.onlylogs", file_path: file_path,
|
|
35
|
+
query: pattern, regexp: regexp_mode, start_position: start_position,
|
|
36
|
+
end_position: end_position, max_matches: max_matches) do |payload|
|
|
33
37
|
each_output_line(command_args, timeout: timeout) do |line|
|
|
34
38
|
byte_offset, content = parse_line.call(line.chomp)
|
|
35
39
|
|
|
@@ -37,6 +41,7 @@ module Onlylogs
|
|
|
37
41
|
content = String.new(content, encoding: Encoding::UTF_8).scrub
|
|
38
42
|
|
|
39
43
|
result = {byte_offset: byte_offset, content: content}
|
|
44
|
+
matches += 1
|
|
40
45
|
|
|
41
46
|
if block_given?
|
|
42
47
|
yield result
|
|
@@ -44,7 +49,12 @@ module Onlylogs
|
|
|
44
49
|
results << result
|
|
45
50
|
end
|
|
46
51
|
end
|
|
52
|
+
rescue TimeoutError
|
|
53
|
+
payload[:timed_out] = true
|
|
54
|
+
raise
|
|
47
55
|
ensure
|
|
56
|
+
payload[:matches] = matches
|
|
57
|
+
payload[:timed_out] ||= false
|
|
48
58
|
drop_page_cache(file_path)
|
|
49
59
|
end
|
|
50
60
|
|
|
@@ -84,7 +94,8 @@ module Onlylogs
|
|
|
84
94
|
reader, writer = IO.pipe
|
|
85
95
|
|
|
86
96
|
begin
|
|
87
|
-
pid = Process.spawn(*bounded(command_args, timeout),
|
|
97
|
+
pid = Process.spawn(*deprioritised(bounded(command_args, timeout)),
|
|
98
|
+
out: writer, err: ::File::NULL, pgroup: true)
|
|
88
99
|
rescue
|
|
89
100
|
reader.close
|
|
90
101
|
raise
|
|
@@ -106,6 +117,12 @@ module Onlylogs
|
|
|
106
117
|
TIMED_OUT_EXIT_STATUS = 124
|
|
107
118
|
KILL_GRACE_PERIOD = 0.5
|
|
108
119
|
|
|
120
|
+
SEARCH_NICENESS = 19
|
|
121
|
+
|
|
122
|
+
def self.deprioritised(command_args)
|
|
123
|
+
["nice", "-n", SEARCH_NICENESS.to_s, *command_args]
|
|
124
|
+
end
|
|
125
|
+
|
|
109
126
|
def self.bounded(command_args, timeout)
|
|
110
127
|
return command_args unless timeout && timeout_command_available?
|
|
111
128
|
|
|
@@ -151,17 +168,18 @@ module Onlylogs
|
|
|
151
168
|
nil
|
|
152
169
|
end
|
|
153
170
|
|
|
171
|
+
MATCH_TIMEOUT = 0.1 # seconds
|
|
172
|
+
|
|
154
173
|
def self.match_line?(line, string, regexp_mode: false)
|
|
155
174
|
# Strip ANSI color codes from the line before matching
|
|
156
175
|
stripped_line = line.gsub(/\e\[[0-9;]*m/, "")
|
|
157
176
|
# Normalize multiple spaces to single spaces
|
|
158
177
|
normalized_line = stripped_line.gsub(/\s+/, " ")
|
|
159
178
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
end
|
|
179
|
+
pattern = regexp_mode ? string : Regexp.escape(string)
|
|
180
|
+
normalized_line.match?(Regexp.new(pattern, timeout: MATCH_TIMEOUT))
|
|
181
|
+
rescue ::RegexpError
|
|
182
|
+
false
|
|
165
183
|
end
|
|
166
184
|
end
|
|
167
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">
|
|
@@ -28,13 +28,20 @@
|
|
|
28
28
|
data-log-streamer-regexp-mode-value="<%= regexp_mode %>"
|
|
29
29
|
data-log-streamer-mode-value="<%= mode %>"
|
|
30
30
|
data-log-streamer-file-size-value="<%= file_size %>"
|
|
31
|
-
data-action="text-selection:start->log-streamer#pauseForSelection"
|
|
31
|
+
data-action="text-selection:start->log-streamer#pauseForSelection text-selection:search->log-streamer#switchToSearch"
|
|
32
32
|
class="onlylogs-log-container" >
|
|
33
33
|
<div data-log-streamer-target="logLines" data-text-selection-target="logLines" id="scrollArea" class="onlylogs-log-lines clusterize-scroll">
|
|
34
34
|
<div id="contentArea" class="clusterize-content">
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
|
+
<%# Search with an empty query would stream the entire file, so it is not run
|
|
39
|
+
at all until something is typed. This says so instead of looking idle. %>
|
|
40
|
+
<div class="onlylogs-search-placeholder" data-log-streamer-target="searchPlaceholder" hidden>
|
|
41
|
+
<p class="onlylogs-search-placeholder__title">Nothing to search for yet</p>
|
|
42
|
+
<p class="onlylogs-search-placeholder__hint">Type a query to search this file, or switch to Live to follow it as it is written.</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
38
45
|
<button type="button"
|
|
39
46
|
data-text-selection-target="button"
|
|
40
47
|
class="onlylogs-context-menu hidden-button"
|
|
@@ -44,32 +51,38 @@
|
|
|
44
51
|
</button>
|
|
45
52
|
|
|
46
53
|
<div class="onlylogs-log-toolbar">
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
<div class="onlylogs-toolbar-row onlylogs-toolbar-row--controls">
|
|
55
|
+
<div class="onlylogs-mode-switch" role="group" aria-label="Log mode">
|
|
56
|
+
<button type="button"
|
|
57
|
+
class="onlylogs-mode-switch__button"
|
|
58
|
+
data-onlylogs-mode-button="live"
|
|
59
|
+
data-log-streamer-target="liveButton"
|
|
60
|
+
data-keyboard-shortcuts-target="liveButton"
|
|
61
|
+
data-action="click->log-streamer#switchToLive"
|
|
62
|
+
aria-pressed="<%= mode == "live" %>"
|
|
63
|
+
title="Follow the file as it is written (L)">
|
|
64
|
+
<span>▶ <u>L</u>ive</span>
|
|
65
|
+
</button>
|
|
66
|
+
<button type="button"
|
|
67
|
+
class="onlylogs-mode-switch__button"
|
|
68
|
+
data-onlylogs-mode-button="static"
|
|
69
|
+
data-log-streamer-target="searchButton"
|
|
70
|
+
data-keyboard-shortcuts-target="searchButton"
|
|
71
|
+
data-action="click->log-streamer#switchToSearch"
|
|
72
|
+
aria-pressed="<%= mode != "live" %>"
|
|
73
|
+
title="Search what is already in the file (S)">
|
|
74
|
+
<span>🔍 <u>S</u>earch</span>
|
|
75
|
+
</button>
|
|
64
76
|
</div>
|
|
65
77
|
<div>
|
|
66
78
|
<label class="toolbar-label">
|
|
67
|
-
|
|
79
|
+
<span data-onlylogs-mode="live">Follow:</span>
|
|
80
|
+
<span data-onlylogs-mode="static">Query:</span>
|
|
68
81
|
<div class="filter-input-wrapper">
|
|
69
82
|
<input type="text"
|
|
70
83
|
name="filter"
|
|
71
84
|
value="<%= filter %>"
|
|
72
|
-
placeholder="
|
|
85
|
+
placeholder="<%= mode == "live" ? "follow lines matching…" : "search the whole file…" %>"
|
|
73
86
|
data-log-streamer-target="filterInput"
|
|
74
87
|
data-text-selection-target="filterInput"
|
|
75
88
|
data-action="input->log-streamer#applyFilter">
|
|
@@ -82,7 +95,28 @@
|
|
|
82
95
|
</div>
|
|
83
96
|
</label>
|
|
84
97
|
</div>
|
|
85
|
-
<div
|
|
98
|
+
<div>
|
|
99
|
+
<label class="toolbar-label">
|
|
100
|
+
<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">
|
|
101
|
+
<span><u>R</u>egexp</span>
|
|
102
|
+
</label>
|
|
103
|
+
</div>
|
|
104
|
+
<div data-onlylogs-mode="live">
|
|
105
|
+
<label class="toolbar-label">
|
|
106
|
+
<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">
|
|
107
|
+
<span><u>A</u>utoscroll</span>
|
|
108
|
+
</label>
|
|
109
|
+
</div>
|
|
110
|
+
<div data-onlylogs-mode="live">
|
|
111
|
+
<button type="button"
|
|
112
|
+
data-log-streamer-target="searchWholeFileButton"
|
|
113
|
+
class="search-whole-file-button"
|
|
114
|
+
data-action="click->log-streamer#searchWholeFile"
|
|
115
|
+
title="Search this text across the whole file">
|
|
116
|
+
Search whole file →
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="range-slider-wrapper" data-onlylogs-mode="static">
|
|
86
120
|
<%= render "onlylogs/shared/range_slider",
|
|
87
121
|
start_name: "start_position",
|
|
88
122
|
end_name: "end_position",
|
|
@@ -105,28 +139,30 @@
|
|
|
105
139
|
class="stop-search-button hidden-button"
|
|
106
140
|
data-action="click->log-streamer#stopSearch"
|
|
107
141
|
title="Stop current search">
|
|
108
|
-
|
|
142
|
+
■ Stop
|
|
109
143
|
</button>
|
|
110
144
|
</div>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="onlylogs-toolbar-row onlylogs-toolbar-row--status">
|
|
148
|
+
<span data-log-streamer-target="results" class="results-text">Results: 0</span>
|
|
149
|
+
<span data-log-streamer-target="message" class="status-message"></span>
|
|
150
|
+
<div class="onlylogs-toolbar-row__end">
|
|
151
|
+
<% unless Onlylogs.ripgrep_enabled? %>
|
|
152
|
+
<span class="grep-warning" title="Search is slow. Install ripgrep for better performance">⚠️</span>
|
|
153
|
+
<% end %>
|
|
154
|
+
<span class="file-size" title="File size: <%= number_to_human_size(File.size(log_file_path)) %>">
|
|
155
|
+
<%= number_to_human_size(File.size(log_file_path)) %>
|
|
156
|
+
</span>
|
|
122
157
|
<button type="button"
|
|
123
158
|
data-log-streamer-target="clearButton"
|
|
124
159
|
class="clear-logs-button"
|
|
125
160
|
data-action="click->log-streamer#clearLogs"
|
|
161
|
+
aria-label="Clear all log lines"
|
|
126
162
|
title="Clear all log lines">
|
|
127
|
-
|
|
163
|
+
🗑
|
|
128
164
|
</button>
|
|
165
|
+
<span data-log-streamer-target="websocketStatus" class="websocket-status websocket-status--disconnected" title="WebSocket Disconnected">🔴</span>
|
|
129
166
|
</div>
|
|
130
|
-
<div data-log-streamer-target="websocketStatus" class="websocket-status websocket-status--disconnected" title="WebSocket Disconnected">🔴</div>
|
|
131
167
|
</div>
|
|
132
168
|
</div>
|