onlylogs 0.5.3 → 0.7.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/app/assets/config/onlylogs_manifest.js +2 -0
- data/app/assets/javascripts/onlylogs/clusterize.js +342 -0
- data/app/assets/stylesheets/onlylogs/clusterize.css +35 -0
- data/app/channels/onlylogs/logs_channel.rb +110 -32
- data/app/controllers/onlylogs/logs_controller.rb +0 -7
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +431 -53
- data/app/models/onlylogs/grep.rb +31 -4
- data/app/views/onlylogs/logs/index.html.erb +1 -1
- data/app/views/onlylogs/shared/_log_container.html.erb +44 -21
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +193 -13
- data/app/views/onlylogs/shared/_range_slider.html.erb +47 -0
- data/bin/super_grep +4 -1
- data/bin/super_ripgrep +10 -2
- data/lib/onlylogs/engine.rb +1 -1
- data/lib/onlylogs/http_device.rb +317 -0
- data/lib/onlylogs/http_logger.rb +19 -316
- data/lib/onlylogs/multi_device.rb +30 -0
- data/lib/onlylogs/socket_device.rb +60 -0
- data/lib/onlylogs/socket_logger.rb +17 -63
- data/lib/onlylogs/version.rb +1 -1
- data/lib/onlylogs.rb +3 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19813b5a5884acae634a3516e4cd23dc67fb5cb359062491a15d331f35de2448
|
|
4
|
+
data.tar.gz: 158e9b56b481744b67a6d92b0835f1443f769b0f264149fb71fe1b69fc86e250
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b2c8cd637017af2c310b06a406d75c0a70659244f6b93dfcb12b0f470337d87c8254e257fa4e4810faea6b12e0f2ad27a60b40d2c3423cc98deb21cf56df39d
|
|
7
|
+
data.tar.gz: d2b9af11af98db9c2d92142553a1a7cc3eb915ddc398ed91b34a04d201e4cbda3dbc926bd007fef335071cbfc77e91ce22b3ff272c3eb7ffa8b607b032841cc6
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/* Clusterize.js - v1.0.0 - 2023-01-22
|
|
2
|
+
http://NeXTs.github.com/Clusterize.js/
|
|
3
|
+
Copyright (c) 2015 Denis Lukov; Licensed MIT */
|
|
4
|
+
|
|
5
|
+
;(function(name, definition) {
|
|
6
|
+
if (typeof module != 'undefined') module.exports = definition();
|
|
7
|
+
else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
|
|
8
|
+
else this[name] = definition();
|
|
9
|
+
}('Clusterize', function() {
|
|
10
|
+
"use strict"
|
|
11
|
+
|
|
12
|
+
// detect ie9 and lower
|
|
13
|
+
// https://gist.github.com/padolsey/527683#comment-786682
|
|
14
|
+
var ie = (function(){
|
|
15
|
+
for( var v = 3,
|
|
16
|
+
el = document.createElement('b'),
|
|
17
|
+
all = el.all || [];
|
|
18
|
+
el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
|
|
19
|
+
all[0];
|
|
20
|
+
){}
|
|
21
|
+
return v > 4 ? v : document.documentMode;
|
|
22
|
+
}()),
|
|
23
|
+
is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
|
|
24
|
+
var Clusterize = function(data) {
|
|
25
|
+
if( ! (this instanceof Clusterize))
|
|
26
|
+
return new Clusterize(data);
|
|
27
|
+
var self = this;
|
|
28
|
+
|
|
29
|
+
var defaults = {
|
|
30
|
+
rows_in_block: 50,
|
|
31
|
+
blocks_in_cluster: 4,
|
|
32
|
+
tag: null,
|
|
33
|
+
show_no_data_row: true,
|
|
34
|
+
no_data_class: 'clusterize-no-data',
|
|
35
|
+
no_data_text: 'No data',
|
|
36
|
+
keep_parity: true,
|
|
37
|
+
callbacks: {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// public parameters
|
|
41
|
+
self.options = {};
|
|
42
|
+
var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
|
|
43
|
+
for(var i = 0, option; option = options[i]; i++) {
|
|
44
|
+
self.options[option] = typeof data[option] != 'undefined' && data[option] != null
|
|
45
|
+
? data[option]
|
|
46
|
+
: defaults[option];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var elems = ['scroll', 'content'];
|
|
50
|
+
for(var i = 0, elem; elem = elems[i]; i++) {
|
|
51
|
+
self[elem + '_elem'] = data[elem + 'Id']
|
|
52
|
+
? document.getElementById(data[elem + 'Id'])
|
|
53
|
+
: data[elem + 'Elem'];
|
|
54
|
+
if( ! self[elem + '_elem'])
|
|
55
|
+
throw new Error("Error! Could not find " + elem + " element");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// tabindex forces the browser to keep focus on the scrolling list, fixes #11
|
|
59
|
+
if( ! self.content_elem.hasAttribute('tabindex'))
|
|
60
|
+
self.content_elem.setAttribute('tabindex', 0);
|
|
61
|
+
|
|
62
|
+
// private parameters
|
|
63
|
+
var rows = isArray(data.rows)
|
|
64
|
+
? data.rows
|
|
65
|
+
: self.fetchMarkup(),
|
|
66
|
+
cache = {},
|
|
67
|
+
scroll_top = self.scroll_elem.scrollTop;
|
|
68
|
+
|
|
69
|
+
// append initial data
|
|
70
|
+
self.insertToDOM(rows, cache);
|
|
71
|
+
|
|
72
|
+
// restore the scroll position
|
|
73
|
+
self.scroll_elem.scrollTop = scroll_top;
|
|
74
|
+
|
|
75
|
+
// adding scroll handler
|
|
76
|
+
var last_cluster = false,
|
|
77
|
+
scroll_debounce = 0,
|
|
78
|
+
pointer_events_set = false,
|
|
79
|
+
scrollEv = function() {
|
|
80
|
+
// fixes scrolling issue on Mac #3
|
|
81
|
+
if (is_mac) {
|
|
82
|
+
if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
|
|
83
|
+
pointer_events_set = true;
|
|
84
|
+
clearTimeout(scroll_debounce);
|
|
85
|
+
scroll_debounce = setTimeout(function () {
|
|
86
|
+
self.content_elem.style.pointerEvents = 'auto';
|
|
87
|
+
pointer_events_set = false;
|
|
88
|
+
}, 50);
|
|
89
|
+
}
|
|
90
|
+
if (last_cluster != (last_cluster = self.getClusterNum(rows)))
|
|
91
|
+
self.insertToDOM(rows, cache);
|
|
92
|
+
if (self.options.callbacks.scrollingProgress)
|
|
93
|
+
self.options.callbacks.scrollingProgress(self.getScrollProgress());
|
|
94
|
+
},
|
|
95
|
+
resize_debounce = 0,
|
|
96
|
+
resizeEv = function() {
|
|
97
|
+
clearTimeout(resize_debounce);
|
|
98
|
+
resize_debounce = setTimeout(self.refresh, 100);
|
|
99
|
+
}
|
|
100
|
+
on('scroll', self.scroll_elem, scrollEv);
|
|
101
|
+
on('resize', window, resizeEv);
|
|
102
|
+
|
|
103
|
+
// public methods
|
|
104
|
+
self.destroy = function(clean) {
|
|
105
|
+
off('scroll', self.scroll_elem, scrollEv);
|
|
106
|
+
off('resize', window, resizeEv);
|
|
107
|
+
self.html((clean ? self.generateEmptyRow() : rows).join(''));
|
|
108
|
+
}
|
|
109
|
+
self.refresh = function(force) {
|
|
110
|
+
if(self.getRowsHeight(rows) || force) self.update(rows);
|
|
111
|
+
}
|
|
112
|
+
self.update = function(new_rows) {
|
|
113
|
+
rows = isArray(new_rows)
|
|
114
|
+
? new_rows
|
|
115
|
+
: [];
|
|
116
|
+
var scroll_top = self.scroll_elem.scrollTop;
|
|
117
|
+
// fixes #39
|
|
118
|
+
if(rows.length * self.options.item_height < scroll_top) {
|
|
119
|
+
self.scroll_elem.scrollTop = 0;
|
|
120
|
+
last_cluster = 0;
|
|
121
|
+
}
|
|
122
|
+
self.insertToDOM(rows, cache);
|
|
123
|
+
self.scroll_elem.scrollTop = scroll_top;
|
|
124
|
+
}
|
|
125
|
+
self.clear = function() {
|
|
126
|
+
self.update([]);
|
|
127
|
+
}
|
|
128
|
+
self.getRowsAmount = function() {
|
|
129
|
+
return rows.length;
|
|
130
|
+
}
|
|
131
|
+
self.prune = function(count) {
|
|
132
|
+
if(count <= 0 || rows.length <= count) return;
|
|
133
|
+
rows = rows.slice(count);
|
|
134
|
+
var scroll_top = self.scroll_elem.scrollTop;
|
|
135
|
+
self.insertToDOM(rows, cache);
|
|
136
|
+
self.scroll_elem.scrollTop = Math.max(0, scroll_top - (count * self.options.item_height));
|
|
137
|
+
}
|
|
138
|
+
self.getScrollProgress = function() {
|
|
139
|
+
return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var add = function(where, _new_rows) {
|
|
143
|
+
var new_rows = isArray(_new_rows)
|
|
144
|
+
? _new_rows
|
|
145
|
+
: [];
|
|
146
|
+
if( ! new_rows.length) return;
|
|
147
|
+
rows = where == 'append'
|
|
148
|
+
? rows.concat(new_rows)
|
|
149
|
+
: new_rows.concat(rows);
|
|
150
|
+
self.insertToDOM(rows, cache);
|
|
151
|
+
}
|
|
152
|
+
self.append = function(rows) {
|
|
153
|
+
add('append', rows);
|
|
154
|
+
}
|
|
155
|
+
self.prepend = function(rows) {
|
|
156
|
+
add('prepend', rows);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Clusterize.prototype = {
|
|
161
|
+
constructor: Clusterize,
|
|
162
|
+
// fetch existing markup
|
|
163
|
+
fetchMarkup: function() {
|
|
164
|
+
var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
|
|
165
|
+
while (rows_nodes.length) {
|
|
166
|
+
rows.push(rows_nodes.shift().outerHTML);
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
},
|
|
170
|
+
// get tag name, content tag name, tag height, calc cluster height
|
|
171
|
+
exploreEnvironment: function(rows, cache) {
|
|
172
|
+
var opts = this.options;
|
|
173
|
+
opts.content_tag = this.content_elem.tagName.toLowerCase();
|
|
174
|
+
if( ! rows.length) return;
|
|
175
|
+
if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
|
|
176
|
+
if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]);
|
|
177
|
+
if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
|
|
178
|
+
this.getRowsHeight(rows);
|
|
179
|
+
},
|
|
180
|
+
getRowsHeight: function(rows) {
|
|
181
|
+
var opts = this.options,
|
|
182
|
+
prev_item_height = opts.item_height;
|
|
183
|
+
opts.cluster_height = 0;
|
|
184
|
+
if( ! rows.length) return;
|
|
185
|
+
var nodes = this.content_elem.children;
|
|
186
|
+
if( ! nodes.length) return;
|
|
187
|
+
var node = nodes[Math.floor(nodes.length / 2)];
|
|
188
|
+
opts.item_height = node.offsetHeight;
|
|
189
|
+
// consider table's border-spacing
|
|
190
|
+
if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
|
|
191
|
+
opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;
|
|
192
|
+
// consider margins (and margins collapsing)
|
|
193
|
+
if(opts.tag != 'tr') {
|
|
194
|
+
var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;
|
|
195
|
+
var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;
|
|
196
|
+
opts.item_height += Math.max(marginTop, marginBottom);
|
|
197
|
+
}
|
|
198
|
+
opts.block_height = opts.item_height * opts.rows_in_block;
|
|
199
|
+
opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;
|
|
200
|
+
opts.cluster_height = opts.blocks_in_cluster * opts.block_height;
|
|
201
|
+
return prev_item_height != opts.item_height;
|
|
202
|
+
},
|
|
203
|
+
// get current cluster number
|
|
204
|
+
getClusterNum: function (rows) {
|
|
205
|
+
var opts = this.options;
|
|
206
|
+
opts.scroll_top = this.scroll_elem.scrollTop;
|
|
207
|
+
var cluster_divider = opts.cluster_height - opts.block_height;
|
|
208
|
+
var current_cluster = Math.floor(opts.scroll_top / cluster_divider);
|
|
209
|
+
var max_cluster = Math.floor((rows.length * opts.item_height) / cluster_divider);
|
|
210
|
+
return Math.min(current_cluster, max_cluster);
|
|
211
|
+
},
|
|
212
|
+
// generate empty row if no data provided
|
|
213
|
+
generateEmptyRow: function() {
|
|
214
|
+
var opts = this.options;
|
|
215
|
+
if( ! opts.tag || ! opts.show_no_data_row) return [];
|
|
216
|
+
var empty_row = document.createElement(opts.tag),
|
|
217
|
+
no_data_content = document.createTextNode(opts.no_data_text), td;
|
|
218
|
+
empty_row.className = opts.no_data_class;
|
|
219
|
+
if(opts.tag == 'tr') {
|
|
220
|
+
td = document.createElement('td');
|
|
221
|
+
// fixes #53
|
|
222
|
+
td.colSpan = 100;
|
|
223
|
+
td.appendChild(no_data_content);
|
|
224
|
+
}
|
|
225
|
+
empty_row.appendChild(td || no_data_content);
|
|
226
|
+
return [empty_row.outerHTML];
|
|
227
|
+
},
|
|
228
|
+
// generate cluster for current scroll position
|
|
229
|
+
generate: function (rows) {
|
|
230
|
+
var opts = this.options,
|
|
231
|
+
rows_len = rows.length;
|
|
232
|
+
if (rows_len < opts.rows_in_block) {
|
|
233
|
+
return {
|
|
234
|
+
top_offset: 0,
|
|
235
|
+
bottom_offset: 0,
|
|
236
|
+
rows_above: 0,
|
|
237
|
+
rows: rows_len ? rows : this.generateEmptyRow()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * this.getClusterNum(rows), 0),
|
|
241
|
+
items_end = items_start + opts.rows_in_cluster,
|
|
242
|
+
top_offset = Math.max(items_start * opts.item_height, 0),
|
|
243
|
+
bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
|
|
244
|
+
this_cluster_rows = [],
|
|
245
|
+
rows_above = items_start;
|
|
246
|
+
if(top_offset < 1) {
|
|
247
|
+
rows_above++;
|
|
248
|
+
}
|
|
249
|
+
for (var i = items_start; i < items_end; i++) {
|
|
250
|
+
rows[i] && this_cluster_rows.push(rows[i]);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
top_offset: top_offset,
|
|
254
|
+
bottom_offset: bottom_offset,
|
|
255
|
+
rows_above: rows_above,
|
|
256
|
+
rows: this_cluster_rows
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
renderExtraTag: function(class_name, height) {
|
|
260
|
+
var tag = document.createElement(this.options.tag),
|
|
261
|
+
clusterize_prefix = 'clusterize-';
|
|
262
|
+
tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
|
|
263
|
+
height && (tag.style.height = height + 'px');
|
|
264
|
+
return tag.outerHTML;
|
|
265
|
+
},
|
|
266
|
+
// if necessary verify data changed and insert to DOM
|
|
267
|
+
insertToDOM: function(rows, cache) {
|
|
268
|
+
// explore row's height
|
|
269
|
+
if( ! this.options.cluster_height) {
|
|
270
|
+
this.exploreEnvironment(rows, cache);
|
|
271
|
+
}
|
|
272
|
+
var data = this.generate(rows),
|
|
273
|
+
this_cluster_rows = data.rows.join(''),
|
|
274
|
+
this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
|
|
275
|
+
top_offset_changed = this.checkChanges('top', data.top_offset, cache),
|
|
276
|
+
only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
|
|
277
|
+
callbacks = this.options.callbacks,
|
|
278
|
+
layout = [];
|
|
279
|
+
|
|
280
|
+
if(this_cluster_content_changed || top_offset_changed) {
|
|
281
|
+
if(data.top_offset) {
|
|
282
|
+
this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
|
|
283
|
+
layout.push(this.renderExtraTag('top-space', data.top_offset));
|
|
284
|
+
}
|
|
285
|
+
layout.push(this_cluster_rows);
|
|
286
|
+
data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
|
|
287
|
+
callbacks.clusterWillChange && callbacks.clusterWillChange();
|
|
288
|
+
this.html(layout.join(''));
|
|
289
|
+
this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
|
|
290
|
+
this.content_elem.style['counter-increment'] = 'clusterize-counter ' + (data.rows_above-1);
|
|
291
|
+
callbacks.clusterChanged && callbacks.clusterChanged();
|
|
292
|
+
} else if(only_bottom_offset_changed) {
|
|
293
|
+
this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
// unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround
|
|
297
|
+
html: function(data) {
|
|
298
|
+
var content_elem = this.content_elem;
|
|
299
|
+
if(ie && ie <= 9 && this.options.tag == 'tr') {
|
|
300
|
+
var div = document.createElement('div'), last;
|
|
301
|
+
div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
|
|
302
|
+
while((last = content_elem.lastChild)) {
|
|
303
|
+
content_elem.removeChild(last);
|
|
304
|
+
}
|
|
305
|
+
var rows_nodes = this.getChildNodes(div.firstChild.firstChild);
|
|
306
|
+
while (rows_nodes.length) {
|
|
307
|
+
content_elem.appendChild(rows_nodes.shift());
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
content_elem.innerHTML = data;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
getChildNodes: function(tag) {
|
|
314
|
+
var child_nodes = tag.children, nodes = [];
|
|
315
|
+
for (var i = 0, ii = child_nodes.length; i < ii; i++) {
|
|
316
|
+
nodes.push(child_nodes[i]);
|
|
317
|
+
}
|
|
318
|
+
return nodes;
|
|
319
|
+
},
|
|
320
|
+
checkChanges: function(type, value, cache) {
|
|
321
|
+
var changed = value != cache[type];
|
|
322
|
+
cache[type] = value;
|
|
323
|
+
return changed;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// support functions
|
|
328
|
+
function on(evt, element, fnc) {
|
|
329
|
+
return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
|
|
330
|
+
}
|
|
331
|
+
function off(evt, element, fnc) {
|
|
332
|
+
return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
|
|
333
|
+
}
|
|
334
|
+
function isArray(arr) {
|
|
335
|
+
return Object.prototype.toString.call(arr) === '[object Array]';
|
|
336
|
+
}
|
|
337
|
+
function getStyle(prop, elem) {
|
|
338
|
+
return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return Clusterize;
|
|
342
|
+
}));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Clusterize scroll container. Height is controlled by parent container. */
|
|
2
|
+
.clusterize-scroll{
|
|
3
|
+
overflow: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Avoid vertical margins for extra tags
|
|
8
|
+
* Necessary for correct calculations when rows have nonzero vertical margins
|
|
9
|
+
*/
|
|
10
|
+
.clusterize-extra-row{
|
|
11
|
+
margin-top: 0 !important;
|
|
12
|
+
margin-bottom: 0 !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* By default extra tag .clusterize-keep-parity added to keep parity of rows.
|
|
16
|
+
* Useful when used :nth-child(even/odd)
|
|
17
|
+
*/
|
|
18
|
+
.clusterize-extra-row.clusterize-keep-parity{
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* During initialization clusterize adds tabindex to force the browser to keep focus
|
|
23
|
+
* on the scrolling list, see issue #11
|
|
24
|
+
* Outline removes default browser's borders for focused elements.
|
|
25
|
+
*/
|
|
26
|
+
.clusterize-content{
|
|
27
|
+
outline: 0;
|
|
28
|
+
counter-reset: clusterize-counter;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Centering message that appears when no data provided
|
|
32
|
+
*/
|
|
33
|
+
.clusterize-no-data td{
|
|
34
|
+
text-align: center;
|
|
35
|
+
}
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
module Onlylogs
|
|
4
4
|
class LogsChannel < ActionCable::Channel::Base
|
|
5
5
|
def subscribed
|
|
6
|
-
|
|
7
|
-
# Wait for the client to send the cursor position
|
|
8
|
-
# start_log_watcher will be called from the initialize_watcher method
|
|
6
|
+
@last_initialize_params = nil
|
|
9
7
|
end
|
|
10
8
|
|
|
11
9
|
def initialize_watcher(data)
|
|
10
|
+
# Prevent duplicate calls with identical parameters
|
|
11
|
+
if @last_initialize_params == data
|
|
12
|
+
Rails.logger.info "Onlylogs: Ignoring duplicate initialize_watcher call"
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@last_initialize_params = data.dup
|
|
12
17
|
cleanup_existing_operations
|
|
13
18
|
|
|
14
19
|
# Decrypt and verify the file path
|
|
@@ -39,19 +44,20 @@ module Onlylogs
|
|
|
39
44
|
return
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
cursor_position = data["cursor_position"] || 0
|
|
43
47
|
filter = data["filter"].presence
|
|
44
48
|
mode = data["mode"] || "live"
|
|
45
49
|
regexp_mode = data["regexp_mode"] == true || data["regexp_mode"] == "true"
|
|
46
|
-
start_position = data["start_position"]&.to_i || 0
|
|
47
|
-
end_position = data["end_position"]&.to_i
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
file_size = ::File.size(file_path)
|
|
52
|
+
start_position = (data["start_position"]&.to_i || 0).clamp(0, file_size)
|
|
53
|
+
end_position = data["end_position"]&.to_i&.clamp(0, file_size) if data["end_position"]
|
|
54
|
+
|
|
55
|
+
if mode == "static"
|
|
56
|
+
# Read the entire file with filter and send all matching lines
|
|
57
|
+
read_static(file_path, filter, regexp_mode, start_position, end_position)
|
|
52
58
|
else
|
|
53
|
-
#
|
|
54
|
-
start_log_watcher(file_path,
|
|
59
|
+
# Follow the tail of the file indefinitely
|
|
60
|
+
start_log_watcher(file_path, filter, regexp_mode)
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
|
|
@@ -75,7 +81,11 @@ module Onlylogs
|
|
|
75
81
|
stop_log_watcher
|
|
76
82
|
end
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
# Bytes from the end of the file to show when starting a live tail without
|
|
85
|
+
# an explicit cursor (matches the default whole-file live-mode page load).
|
|
86
|
+
LIVE_TAIL_BYTES = 10_000
|
|
87
|
+
|
|
88
|
+
def start_log_watcher(file_path, filter = nil, regexp_mode = false)
|
|
79
89
|
return if @log_watcher_running
|
|
80
90
|
|
|
81
91
|
@log_watcher_running = true
|
|
@@ -84,7 +94,9 @@ module Onlylogs
|
|
|
84
94
|
|
|
85
95
|
transmit({action: "message", content: "Reading file. Please wait..."})
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
# Start from LIVE_TAIL_BYTES from the end, or from the beginning if file is smaller
|
|
98
|
+
starting_position = [::File.size(file_path) - LIVE_TAIL_BYTES, 0].max
|
|
99
|
+
@log_file = Onlylogs::File.new(file_path, last_position: starting_position)
|
|
88
100
|
|
|
89
101
|
transmit({action: "message", content: ""})
|
|
90
102
|
|
|
@@ -97,10 +109,9 @@ module Onlylogs
|
|
|
97
109
|
lines_to_send = []
|
|
98
110
|
|
|
99
111
|
new_lines.each do |log_line|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# end
|
|
112
|
+
if @filter.present? && !Onlylogs::Grep.match_line?(log_line, @filter, regexp_mode: @regexp_mode)
|
|
113
|
+
next
|
|
114
|
+
end
|
|
104
115
|
|
|
105
116
|
lines_to_send << render_log_line(log_line)
|
|
106
117
|
end
|
|
@@ -143,34 +154,82 @@ module Onlylogs
|
|
|
143
154
|
@log_file = nil
|
|
144
155
|
end
|
|
145
156
|
|
|
146
|
-
def
|
|
157
|
+
def read_static(file_path, filter = nil, regexp_mode = false, start_position = 0, end_position = nil)
|
|
147
158
|
@log_watcher_running = true
|
|
148
159
|
@log_file = Onlylogs::File.new(file_path, last_position: 0)
|
|
149
160
|
|
|
150
|
-
transmit({action: "message", content: "Searching..."})
|
|
161
|
+
transmit({action: "message", content: filter.present? ? "Searching..." : "Loading..."})
|
|
151
162
|
|
|
152
163
|
@batch_sender = BatchSender.new(self)
|
|
153
164
|
@batch_sender.start
|
|
154
165
|
|
|
155
|
-
line_count = 0
|
|
156
|
-
|
|
157
166
|
begin
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# Add to batch buffer (sender thread will handle sending)
|
|
163
|
-
@batch_sender.add_line(render_log_line(log_line))
|
|
167
|
+
last_line = nil
|
|
168
|
+
last_byte_offset = nil
|
|
169
|
+
line_count = 0
|
|
164
170
|
|
|
165
|
-
|
|
171
|
+
Rails.logger.silence(Logger::ERROR) do
|
|
172
|
+
skip_first = start_position > 0
|
|
173
|
+
|
|
174
|
+
if filter.present?
|
|
175
|
+
# Use grep for filtered search
|
|
176
|
+
@log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |result|
|
|
177
|
+
break if @batch_sender.nil? || @log_watcher_running == false
|
|
178
|
+
|
|
179
|
+
# Skip first line if start_position > 0 (line is cut off at byte boundary)
|
|
180
|
+
if skip_first
|
|
181
|
+
skip_first = false
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Result is a hash with {byte_offset, content}
|
|
186
|
+
byte_offset = result[:byte_offset]
|
|
187
|
+
log_line = result[:content]
|
|
188
|
+
|
|
189
|
+
# Buffer previous line and skip it to avoid cut-off lines at boundaries
|
|
190
|
+
if last_line
|
|
191
|
+
@batch_sender.add_line(render_log_line(last_line, byte_offset: last_byte_offset, show_expand_button: true))
|
|
192
|
+
line_count += 1
|
|
193
|
+
end
|
|
194
|
+
last_line = log_line
|
|
195
|
+
last_byte_offset = byte_offset
|
|
196
|
+
end
|
|
197
|
+
else
|
|
198
|
+
# No filter - read all lines directly (skip grep)
|
|
199
|
+
# Still need byte_offset for highlighting when expanding around a line
|
|
200
|
+
current_byte_offset = start_position
|
|
201
|
+
read_byte_range(file_path, start_position, end_position) do |log_line|
|
|
202
|
+
break if @batch_sender.nil? || @log_watcher_running == false
|
|
203
|
+
|
|
204
|
+
# Skip first line if start_position > 0 (line is cut off at byte boundary)
|
|
205
|
+
if skip_first
|
|
206
|
+
skip_first = false
|
|
207
|
+
next
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Buffer previous line and skip it to avoid cut-off lines at boundaries
|
|
211
|
+
if last_line
|
|
212
|
+
@batch_sender.add_line(render_log_line(last_line, byte_offset: last_byte_offset))
|
|
213
|
+
line_count += 1
|
|
214
|
+
end
|
|
215
|
+
last_line = log_line
|
|
216
|
+
last_byte_offset = current_byte_offset
|
|
217
|
+
# Account for line content plus newline character (2 bytes for \r\n or 1 for \n)
|
|
218
|
+
current_byte_offset += log_line.bytesize + 1
|
|
219
|
+
end
|
|
166
220
|
end
|
|
167
221
|
end
|
|
168
222
|
|
|
169
|
-
#
|
|
223
|
+
# Send last line only if no end_position (avoid cut-off line at byte boundary)
|
|
224
|
+
if last_line && !end_position
|
|
225
|
+
@batch_sender.add_line(render_log_line(last_line, byte_offset: last_byte_offset, show_expand_button: filter.present?))
|
|
226
|
+
line_count += 1
|
|
227
|
+
end
|
|
228
|
+
|
|
170
229
|
@batch_sender.stop
|
|
171
230
|
|
|
172
231
|
# Send completion message
|
|
173
|
-
if line_count >= Onlylogs.max_line_matches
|
|
232
|
+
if Onlylogs.max_line_matches && line_count >= Onlylogs.max_line_matches
|
|
174
233
|
transmit({action: "finish", content: "Search finished. Search results limit reached."})
|
|
175
234
|
else
|
|
176
235
|
transmit({action: "finish", content: "Search finished."})
|
|
@@ -184,8 +243,27 @@ module Onlylogs
|
|
|
184
243
|
end
|
|
185
244
|
end
|
|
186
245
|
|
|
187
|
-
def
|
|
188
|
-
|
|
246
|
+
def read_byte_range(file_path, start_position, end_position)
|
|
247
|
+
file_size = ::File.size(file_path)
|
|
248
|
+
range_size = (end_position || file_size) - start_position
|
|
249
|
+
|
|
250
|
+
return if start_position < 0 || range_size <= 0 || start_position >= file_size
|
|
251
|
+
|
|
252
|
+
::File.read(file_path, range_size, start_position).each_line do |line|
|
|
253
|
+
yield line.chomp
|
|
254
|
+
end
|
|
255
|
+
rescue => e
|
|
256
|
+
Rails.logger.error "Error reading byte range: #{e.message}"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def render_log_line(log_line, byte_offset: nil, show_expand_button: false)
|
|
260
|
+
parsed = FilePathParser.parse(AnsiColorParser.parse(ERB::Util.html_escape(log_line)))
|
|
261
|
+
|
|
262
|
+
{
|
|
263
|
+
content: parsed,
|
|
264
|
+
byte_offset: byte_offset,
|
|
265
|
+
show_expand_button: show_expand_button
|
|
266
|
+
}
|
|
189
267
|
end
|
|
190
268
|
end
|
|
191
269
|
end
|
|
@@ -3,15 +3,8 @@
|
|
|
3
3
|
module Onlylogs
|
|
4
4
|
class LogsController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
@max_lines = (params[:max_lines] || 100).to_i
|
|
7
|
-
|
|
8
6
|
@available_log_files = Onlylogs.available_log_files
|
|
9
7
|
@log_file_path = selected_log_file_path
|
|
10
|
-
|
|
11
|
-
@filter = params[:filter]
|
|
12
|
-
@autoscroll = params[:autoscroll] != "false"
|
|
13
|
-
@regexp_mode = params[:regexp_mode] == "true"
|
|
14
|
-
@mode = @filter.blank? ? (params[:mode] || "live") : "search" # "live" or "search"
|
|
15
8
|
end
|
|
16
9
|
|
|
17
10
|
def download
|