rails-tail-log-monitor 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5c49d2b0b7fbd62b9ba73a7e390eb7288a4c07ea4709fcd04ccb232fae496b52
4
+ data.tar.gz: 46e641bafd169e55c7ecaa9eaa5684ef8b1685b94c40e838a86ba95bc61a0076
5
+ SHA512:
6
+ metadata.gz: c303322df80b932b2b7e8ef05fa8e1eeef498c8df4f867394ecc6015d981a54da552ac8a1218f5845f2ddc4957d98db26c46c6463f9d5846b3faf6e0a204b18c
7
+ data.tar.gz: 71841858a0fb2e60bc1fd87dd62069766866263c9e701290ffd6c17527afcc94bf924f35f014dd39f255091b2bbf4055b6806e7eed6016286abfc95aeccd458f
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # Rails Tail Log Monitor
2
+
3
+ The rails-tail-log-monitor gem simplifies the process of monitoring server logs by displaying the tail of the log file directly in the terminal window alongside the standard Rails server output. With rails-tail-log-monitor, developers can effortlessly keep track of the most recent log entries without the need for manual log file inspection.
4
+
5
+ ![demo](demo.gif)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rails-tail-log-monitor', '~> 1.0.0'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install google-translate-free
22
+
23
+ ## Usage
24
+ Make sure your `package.json` has `actioncable`
25
+ ```json
26
+ {
27
+ ...,
28
+ "@rails/actioncable": "^6.0.0"
29
+ }
30
+ ```
31
+ Update `config/cable.yml`
32
+ ```yaml
33
+ development:
34
+ adapter: redis
35
+ url: <%= "redis://localhost:6379/#{ENV.fetch('REDIS_PORT')}" %>
36
+ channel_prefix: your_app_development
37
+ ```
38
+ Update `routes.rb`
39
+ ```ruby
40
+ # config/routes.rb
41
+ Rails.application.routes.draw do
42
+ ...
43
+ # example for usage same Sidekiq
44
+ authenticate :administrator do
45
+ mount Sidekiq::Web => '/sidekiq'
46
+ mount LogMonitor::Engine => '/log'
47
+ end
48
+ end
49
+ ```
50
+ Custom setting `config/initializers/rails_tail_log_monitor.rb`
51
+ ```ruby
52
+ LogMonitor.configure do |config|
53
+ config.action_cable_url = "ws://localhost:3000/cable"
54
+ config.keep_alive_time = 60 # default = 30
55
+ end
56
+ ```
57
+
58
+ After installation and configuration, start your Rails application, open `https://localhost:3000/log` URL, and make a few requests.
59
+
60
+
61
+ ## Contributors
62
+
63
+ - [datpmt](https://github.com/datpmt)
64
+
65
+ I welcome contributions to this project.
66
+
67
+ 1. Fork it.
68
+ 2. Create your feature branch (`git checkout -b your-feature`).
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
70
+ 4. Push to the branch (`git push origin your-feature`).
71
+ 5. Create a new pull request.
72
+
73
+ ## License
74
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,601 @@
1
+ (function() {
2
+ var context = this;
3
+
4
+ (function() {
5
+ (function() {
6
+ var slice = [].slice;
7
+
8
+ this.ActionCable = {
9
+ INTERNAL: {
10
+ "message_types": {
11
+ "welcome": "welcome",
12
+ "ping": "ping",
13
+ "confirmation": "confirm_subscription",
14
+ "rejection": "reject_subscription"
15
+ },
16
+ "default_mount_path": "/cable",
17
+ "protocols": ["actioncable-v1-json", "actioncable-unsupported"]
18
+ },
19
+ WebSocket: window.WebSocket,
20
+ logger: window.console,
21
+ createConsumer: function(url) {
22
+ var ref;
23
+ if (url == null) {
24
+ url = (ref = this.getConfig("url")) != null ? ref : this.INTERNAL.default_mount_path;
25
+ }
26
+ return new ActionCable.Consumer(this.createWebSocketURL(url));
27
+ },
28
+ getConfig: function(name) {
29
+ var element;
30
+ element = document.head.querySelector("meta[name='action-cable-" + name + "']");
31
+ return element != null ? element.getAttribute("content") : void 0;
32
+ },
33
+ createWebSocketURL: function(url) {
34
+ var a;
35
+ if (url && !/^wss?:/i.test(url)) {
36
+ a = document.createElement("a");
37
+ a.href = url;
38
+ a.href = a.href;
39
+ a.protocol = a.protocol.replace("http", "ws");
40
+ return a.href;
41
+ } else {
42
+ return url;
43
+ }
44
+ },
45
+ startDebugging: function() {
46
+ return this.debugging = true;
47
+ },
48
+ stopDebugging: function() {
49
+ return this.debugging = null;
50
+ },
51
+ log: function() {
52
+ var messages, ref;
53
+ messages = 1 <= arguments.length ? slice.call(arguments, 0) : [];
54
+ if (this.debugging) {
55
+ messages.push(Date.now());
56
+ return (ref = this.logger).log.apply(ref, ["[ActionCable]"].concat(slice.call(messages)));
57
+ }
58
+ }
59
+ };
60
+
61
+ }).call(this);
62
+ }).call(context);
63
+
64
+ var ActionCable = context.ActionCable;
65
+
66
+ (function() {
67
+ (function() {
68
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
69
+
70
+ ActionCable.ConnectionMonitor = (function() {
71
+ var clamp, now, secondsSince;
72
+
73
+ ConnectionMonitor.pollInterval = {
74
+ min: 3,
75
+ max: 30
76
+ };
77
+
78
+ ConnectionMonitor.staleThreshold = 6;
79
+
80
+ function ConnectionMonitor(connection) {
81
+ this.connection = connection;
82
+ this.visibilityDidChange = bind(this.visibilityDidChange, this);
83
+ this.reconnectAttempts = 0;
84
+ }
85
+
86
+ ConnectionMonitor.prototype.start = function() {
87
+ if (!this.isRunning()) {
88
+ this.startedAt = now();
89
+ delete this.stoppedAt;
90
+ this.startPolling();
91
+ document.addEventListener("visibilitychange", this.visibilityDidChange);
92
+ return ActionCable.log("ConnectionMonitor started. pollInterval = " + (this.getPollInterval()) + " ms");
93
+ }
94
+ };
95
+
96
+ ConnectionMonitor.prototype.stop = function() {
97
+ if (this.isRunning()) {
98
+ this.stoppedAt = now();
99
+ this.stopPolling();
100
+ document.removeEventListener("visibilitychange", this.visibilityDidChange);
101
+ return ActionCable.log("ConnectionMonitor stopped");
102
+ }
103
+ };
104
+
105
+ ConnectionMonitor.prototype.isRunning = function() {
106
+ return (this.startedAt != null) && (this.stoppedAt == null);
107
+ };
108
+
109
+ ConnectionMonitor.prototype.recordPing = function() {
110
+ return this.pingedAt = now();
111
+ };
112
+
113
+ ConnectionMonitor.prototype.recordConnect = function() {
114
+ this.reconnectAttempts = 0;
115
+ this.recordPing();
116
+ delete this.disconnectedAt;
117
+ return ActionCable.log("ConnectionMonitor recorded connect");
118
+ };
119
+
120
+ ConnectionMonitor.prototype.recordDisconnect = function() {
121
+ this.disconnectedAt = now();
122
+ return ActionCable.log("ConnectionMonitor recorded disconnect");
123
+ };
124
+
125
+ ConnectionMonitor.prototype.startPolling = function() {
126
+ this.stopPolling();
127
+ return this.poll();
128
+ };
129
+
130
+ ConnectionMonitor.prototype.stopPolling = function() {
131
+ return clearTimeout(this.pollTimeout);
132
+ };
133
+
134
+ ConnectionMonitor.prototype.poll = function() {
135
+ return this.pollTimeout = setTimeout((function(_this) {
136
+ return function() {
137
+ _this.reconnectIfStale();
138
+ return _this.poll();
139
+ };
140
+ })(this), this.getPollInterval());
141
+ };
142
+
143
+ ConnectionMonitor.prototype.getPollInterval = function() {
144
+ var interval, max, min, ref;
145
+ ref = this.constructor.pollInterval, min = ref.min, max = ref.max;
146
+ interval = 5 * Math.log(this.reconnectAttempts + 1);
147
+ return Math.round(clamp(interval, min, max) * 1000);
148
+ };
149
+
150
+ ConnectionMonitor.prototype.reconnectIfStale = function() {
151
+ if (this.connectionIsStale()) {
152
+ ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + (this.getPollInterval()) + " ms, time disconnected = " + (secondsSince(this.disconnectedAt)) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
153
+ this.reconnectAttempts++;
154
+ if (this.disconnectedRecently()) {
155
+ return ActionCable.log("ConnectionMonitor skipping reopening recent disconnect");
156
+ } else {
157
+ ActionCable.log("ConnectionMonitor reopening");
158
+ return this.connection.reopen();
159
+ }
160
+ }
161
+ };
162
+
163
+ ConnectionMonitor.prototype.connectionIsStale = function() {
164
+ var ref;
165
+ return secondsSince((ref = this.pingedAt) != null ? ref : this.startedAt) > this.constructor.staleThreshold;
166
+ };
167
+
168
+ ConnectionMonitor.prototype.disconnectedRecently = function() {
169
+ return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
170
+ };
171
+
172
+ ConnectionMonitor.prototype.visibilityDidChange = function() {
173
+ if (document.visibilityState === "visible") {
174
+ return setTimeout((function(_this) {
175
+ return function() {
176
+ if (_this.connectionIsStale() || !_this.connection.isOpen()) {
177
+ ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState);
178
+ return _this.connection.reopen();
179
+ }
180
+ };
181
+ })(this), 200);
182
+ }
183
+ };
184
+
185
+ now = function() {
186
+ return new Date().getTime();
187
+ };
188
+
189
+ secondsSince = function(time) {
190
+ return (now() - time) / 1000;
191
+ };
192
+
193
+ clamp = function(number, min, max) {
194
+ return Math.max(min, Math.min(max, number));
195
+ };
196
+
197
+ return ConnectionMonitor;
198
+
199
+ })();
200
+
201
+ }).call(this);
202
+ (function() {
203
+ var i, message_types, protocols, ref, supportedProtocols, unsupportedProtocol,
204
+ slice = [].slice,
205
+ bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
206
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
207
+
208
+ ref = ActionCable.INTERNAL, message_types = ref.message_types, protocols = ref.protocols;
209
+
210
+ supportedProtocols = 2 <= protocols.length ? slice.call(protocols, 0, i = protocols.length - 1) : (i = 0, []), unsupportedProtocol = protocols[i++];
211
+
212
+ ActionCable.Connection = (function() {
213
+ Connection.reopenDelay = 500;
214
+
215
+ function Connection(consumer) {
216
+ this.consumer = consumer;
217
+ this.open = bind(this.open, this);
218
+ this.subscriptions = this.consumer.subscriptions;
219
+ this.monitor = new ActionCable.ConnectionMonitor(this);
220
+ this.disconnected = true;
221
+ }
222
+
223
+ Connection.prototype.send = function(data) {
224
+ if (this.isOpen()) {
225
+ this.webSocket.send(JSON.stringify(data));
226
+ return true;
227
+ } else {
228
+ return false;
229
+ }
230
+ };
231
+
232
+ Connection.prototype.open = function() {
233
+ if (this.isActive()) {
234
+ ActionCable.log("Attempted to open WebSocket, but existing socket is " + (this.getState()));
235
+ return false;
236
+ } else {
237
+ ActionCable.log("Opening WebSocket, current state is " + (this.getState()) + ", subprotocols: " + protocols);
238
+ if (this.webSocket != null) {
239
+ this.uninstallEventHandlers();
240
+ }
241
+ this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols);
242
+ this.installEventHandlers();
243
+ this.monitor.start();
244
+ return true;
245
+ }
246
+ };
247
+
248
+ Connection.prototype.close = function(arg) {
249
+ var allowReconnect, ref1;
250
+ allowReconnect = (arg != null ? arg : {
251
+ allowReconnect: true
252
+ }).allowReconnect;
253
+ if (!allowReconnect) {
254
+ this.monitor.stop();
255
+ }
256
+ if (this.isActive()) {
257
+ return (ref1 = this.webSocket) != null ? ref1.close() : void 0;
258
+ }
259
+ };
260
+
261
+ Connection.prototype.reopen = function() {
262
+ var error;
263
+ ActionCable.log("Reopening WebSocket, current state is " + (this.getState()));
264
+ if (this.isActive()) {
265
+ try {
266
+ return this.close();
267
+ } catch (error1) {
268
+ error = error1;
269
+ return ActionCable.log("Failed to reopen WebSocket", error);
270
+ } finally {
271
+ ActionCable.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
272
+ setTimeout(this.open, this.constructor.reopenDelay);
273
+ }
274
+ } else {
275
+ return this.open();
276
+ }
277
+ };
278
+
279
+ Connection.prototype.getProtocol = function() {
280
+ var ref1;
281
+ return (ref1 = this.webSocket) != null ? ref1.protocol : void 0;
282
+ };
283
+
284
+ Connection.prototype.isOpen = function() {
285
+ return this.isState("open");
286
+ };
287
+
288
+ Connection.prototype.isActive = function() {
289
+ return this.isState("open", "connecting");
290
+ };
291
+
292
+ Connection.prototype.isProtocolSupported = function() {
293
+ var ref1;
294
+ return ref1 = this.getProtocol(), indexOf.call(supportedProtocols, ref1) >= 0;
295
+ };
296
+
297
+ Connection.prototype.isState = function() {
298
+ var ref1, states;
299
+ states = 1 <= arguments.length ? slice.call(arguments, 0) : [];
300
+ return ref1 = this.getState(), indexOf.call(states, ref1) >= 0;
301
+ };
302
+
303
+ Connection.prototype.getState = function() {
304
+ var ref1, state, value;
305
+ for (state in WebSocket) {
306
+ value = WebSocket[state];
307
+ if (value === ((ref1 = this.webSocket) != null ? ref1.readyState : void 0)) {
308
+ return state.toLowerCase();
309
+ }
310
+ }
311
+ return null;
312
+ };
313
+
314
+ Connection.prototype.installEventHandlers = function() {
315
+ var eventName, handler;
316
+ for (eventName in this.events) {
317
+ handler = this.events[eventName].bind(this);
318
+ this.webSocket["on" + eventName] = handler;
319
+ }
320
+ };
321
+
322
+ Connection.prototype.uninstallEventHandlers = function() {
323
+ var eventName;
324
+ for (eventName in this.events) {
325
+ this.webSocket["on" + eventName] = function() {};
326
+ }
327
+ };
328
+
329
+ Connection.prototype.events = {
330
+ message: function(event) {
331
+ var identifier, message, ref1, type;
332
+ if (!this.isProtocolSupported()) {
333
+ return;
334
+ }
335
+ ref1 = JSON.parse(event.data), identifier = ref1.identifier, message = ref1.message, type = ref1.type;
336
+ switch (type) {
337
+ case message_types.welcome:
338
+ this.monitor.recordConnect();
339
+ return this.subscriptions.reload();
340
+ case message_types.ping:
341
+ return this.monitor.recordPing();
342
+ case message_types.confirmation:
343
+ return this.subscriptions.notify(identifier, "connected");
344
+ case message_types.rejection:
345
+ return this.subscriptions.reject(identifier);
346
+ default:
347
+ return this.subscriptions.notify(identifier, "received", message);
348
+ }
349
+ },
350
+ open: function() {
351
+ ActionCable.log("WebSocket onopen event, using '" + (this.getProtocol()) + "' subprotocol");
352
+ this.disconnected = false;
353
+ if (!this.isProtocolSupported()) {
354
+ ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.");
355
+ return this.close({
356
+ allowReconnect: false
357
+ });
358
+ }
359
+ },
360
+ close: function(event) {
361
+ ActionCable.log("WebSocket onclose event");
362
+ if (this.disconnected) {
363
+ return;
364
+ }
365
+ this.disconnected = true;
366
+ this.monitor.recordDisconnect();
367
+ return this.subscriptions.notifyAll("disconnected", {
368
+ willAttemptReconnect: this.monitor.isRunning()
369
+ });
370
+ },
371
+ error: function() {
372
+ return ActionCable.log("WebSocket onerror event");
373
+ }
374
+ };
375
+
376
+ return Connection;
377
+
378
+ })();
379
+
380
+ }).call(this);
381
+ (function() {
382
+ var slice = [].slice;
383
+
384
+ ActionCable.Subscriptions = (function() {
385
+ function Subscriptions(consumer) {
386
+ this.consumer = consumer;
387
+ this.subscriptions = [];
388
+ }
389
+
390
+ Subscriptions.prototype.create = function(channelName, mixin) {
391
+ var channel, params, subscription;
392
+ channel = channelName;
393
+ params = typeof channel === "object" ? channel : {
394
+ channel: channel
395
+ };
396
+ subscription = new ActionCable.Subscription(this.consumer, params, mixin);
397
+ return this.add(subscription);
398
+ };
399
+
400
+ Subscriptions.prototype.add = function(subscription) {
401
+ this.subscriptions.push(subscription);
402
+ this.consumer.ensureActiveConnection();
403
+ this.notify(subscription, "initialized");
404
+ this.sendCommand(subscription, "subscribe");
405
+ return subscription;
406
+ };
407
+
408
+ Subscriptions.prototype.remove = function(subscription) {
409
+ this.forget(subscription);
410
+ if (!this.findAll(subscription.identifier).length) {
411
+ this.sendCommand(subscription, "unsubscribe");
412
+ }
413
+ return subscription;
414
+ };
415
+
416
+ Subscriptions.prototype.reject = function(identifier) {
417
+ var i, len, ref, results, subscription;
418
+ ref = this.findAll(identifier);
419
+ results = [];
420
+ for (i = 0, len = ref.length; i < len; i++) {
421
+ subscription = ref[i];
422
+ this.forget(subscription);
423
+ this.notify(subscription, "rejected");
424
+ results.push(subscription);
425
+ }
426
+ return results;
427
+ };
428
+
429
+ Subscriptions.prototype.forget = function(subscription) {
430
+ var s;
431
+ this.subscriptions = (function() {
432
+ var i, len, ref, results;
433
+ ref = this.subscriptions;
434
+ results = [];
435
+ for (i = 0, len = ref.length; i < len; i++) {
436
+ s = ref[i];
437
+ if (s !== subscription) {
438
+ results.push(s);
439
+ }
440
+ }
441
+ return results;
442
+ }).call(this);
443
+ return subscription;
444
+ };
445
+
446
+ Subscriptions.prototype.findAll = function(identifier) {
447
+ var i, len, ref, results, s;
448
+ ref = this.subscriptions;
449
+ results = [];
450
+ for (i = 0, len = ref.length; i < len; i++) {
451
+ s = ref[i];
452
+ if (s.identifier === identifier) {
453
+ results.push(s);
454
+ }
455
+ }
456
+ return results;
457
+ };
458
+
459
+ Subscriptions.prototype.reload = function() {
460
+ var i, len, ref, results, subscription;
461
+ ref = this.subscriptions;
462
+ results = [];
463
+ for (i = 0, len = ref.length; i < len; i++) {
464
+ subscription = ref[i];
465
+ results.push(this.sendCommand(subscription, "subscribe"));
466
+ }
467
+ return results;
468
+ };
469
+
470
+ Subscriptions.prototype.notifyAll = function() {
471
+ var args, callbackName, i, len, ref, results, subscription;
472
+ callbackName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
473
+ ref = this.subscriptions;
474
+ results = [];
475
+ for (i = 0, len = ref.length; i < len; i++) {
476
+ subscription = ref[i];
477
+ results.push(this.notify.apply(this, [subscription, callbackName].concat(slice.call(args))));
478
+ }
479
+ return results;
480
+ };
481
+
482
+ Subscriptions.prototype.notify = function() {
483
+ var args, callbackName, i, len, results, subscription, subscriptions;
484
+ subscription = arguments[0], callbackName = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : [];
485
+ if (typeof subscription === "string") {
486
+ subscriptions = this.findAll(subscription);
487
+ } else {
488
+ subscriptions = [subscription];
489
+ }
490
+ results = [];
491
+ for (i = 0, len = subscriptions.length; i < len; i++) {
492
+ subscription = subscriptions[i];
493
+ results.push(typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : void 0);
494
+ }
495
+ return results;
496
+ };
497
+
498
+ Subscriptions.prototype.sendCommand = function(subscription, command) {
499
+ var identifier;
500
+ identifier = subscription.identifier;
501
+ return this.consumer.send({
502
+ command: command,
503
+ identifier: identifier
504
+ });
505
+ };
506
+
507
+ return Subscriptions;
508
+
509
+ })();
510
+
511
+ }).call(this);
512
+ (function() {
513
+ ActionCable.Subscription = (function() {
514
+ var extend;
515
+
516
+ function Subscription(consumer, params, mixin) {
517
+ this.consumer = consumer;
518
+ if (params == null) {
519
+ params = {};
520
+ }
521
+ this.identifier = JSON.stringify(params);
522
+ extend(this, mixin);
523
+ }
524
+
525
+ Subscription.prototype.perform = function(action, data) {
526
+ if (data == null) {
527
+ data = {};
528
+ }
529
+ data.action = action;
530
+ return this.send(data);
531
+ };
532
+
533
+ Subscription.prototype.send = function(data) {
534
+ return this.consumer.send({
535
+ command: "message",
536
+ identifier: this.identifier,
537
+ data: JSON.stringify(data)
538
+ });
539
+ };
540
+
541
+ Subscription.prototype.unsubscribe = function() {
542
+ return this.consumer.subscriptions.remove(this);
543
+ };
544
+
545
+ extend = function(object, properties) {
546
+ var key, value;
547
+ if (properties != null) {
548
+ for (key in properties) {
549
+ value = properties[key];
550
+ object[key] = value;
551
+ }
552
+ }
553
+ return object;
554
+ };
555
+
556
+ return Subscription;
557
+
558
+ })();
559
+
560
+ }).call(this);
561
+ (function() {
562
+ ActionCable.Consumer = (function() {
563
+ function Consumer(url) {
564
+ this.url = url;
565
+ this.subscriptions = new ActionCable.Subscriptions(this);
566
+ this.connection = new ActionCable.Connection(this);
567
+ }
568
+
569
+ Consumer.prototype.send = function(data) {
570
+ return this.connection.send(data);
571
+ };
572
+
573
+ Consumer.prototype.connect = function() {
574
+ return this.connection.open();
575
+ };
576
+
577
+ Consumer.prototype.disconnect = function() {
578
+ return this.connection.close({
579
+ allowReconnect: false
580
+ });
581
+ };
582
+
583
+ Consumer.prototype.ensureActiveConnection = function() {
584
+ if (!this.connection.isActive()) {
585
+ return this.connection.open();
586
+ }
587
+ };
588
+
589
+ return Consumer;
590
+
591
+ })();
592
+
593
+ }).call(this);
594
+ }).call(this);
595
+
596
+ if (typeof module === "object" && module.exports) {
597
+ module.exports = ActionCable;
598
+ } else if (typeof define === "function" && define.amd) {
599
+ define(ActionCable);
600
+ }
601
+ }).call(this);
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Channel < ActionCable::Channel::Base
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationCable
4
+ class Connection < ActionCable::Connection::Base
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LogChannel < ApplicationCable::Channel
4
+ def subscribed
5
+ stream_from 'log_channel'
6
+ end
7
+
8
+ def keep_alive
9
+ $monitoring_keep_alive = Time.zone.now.to_i
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails-tail-log-monitor'
4
+ class LogController < ::ApplicationController
5
+ layout 'log'
6
+ def index
7
+ LogMonitor::LogMonitorService.start_monitoring
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ <style>
2
+ html, body {
3
+ margin: 0 !important;
4
+ padding: 0 !important;
5
+ }
6
+
7
+ #log-title {
8
+ right: 40px !important;
9
+ font-weight: 900 !important;
10
+ font-size: 24px !important;
11
+ color: #FF0188 !important;
12
+ position: fixed !important;
13
+ font-family: monospace;
14
+ }
15
+
16
+ #log-content {
17
+ background-color: black;
18
+ color: floralwhite;
19
+ height: 100vh;
20
+ overflow-y: scroll;
21
+ white-space: pre-wrap;
22
+ padding: 20px;
23
+ font-family: monospace;
24
+ word-break: break-all;
25
+ box-sizing: border-box;
26
+ font-size: 14px;
27
+ }
28
+ </style>
29
+ <!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <title>Rails Tail Log</title>
34
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
35
+ <%= javascript_include_tag "example" %>
36
+ </head>
37
+ <body>
38
+ <%= yield %>
39
+ </body>
40
+ </html>
@@ -0,0 +1,31 @@
1
+ <div id="log-content"><h1 id="log-title">Log Tail Monitor</h1></div>
2
+ <script>
3
+ document.addEventListener("DOMContentLoaded", function(event) {
4
+ const consumer = ActionCable.createConsumer();
5
+
6
+ const subscription = consumer.subscriptions.create({ channel: "LogChannel" }, {
7
+ connected() {
8
+ console.log('Connected.');
9
+ },
10
+ disconnected() {
11
+ console.log('Disconnected.');
12
+ },
13
+ received(data) {
14
+ const logContent = document.getElementById('log-content');
15
+ data.log_entries.forEach(log => {
16
+ const logElement = document.createElement('div');
17
+ logElement.innerHTML = log;
18
+ logContent.appendChild(logElement);
19
+ });
20
+ logContent.scrollTop = logContent.scrollHeight;
21
+ }
22
+ });
23
+
24
+ function recursiveFetch() {
25
+ subscription.perform('keep_alive');
26
+ setTimeout(recursiveFetch, 1000);
27
+ }
28
+
29
+ recursiveFetch();
30
+ });
31
+ </script>
@@ -0,0 +1,16 @@
1
+ # lib/rails-tail-log-monitor/engine.rb
2
+ module LogMonitor
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace LogMonitor
5
+
6
+ initializer 'log_monitor.assets.precompile' do |app|
7
+ app.config.assets.precompile += %w[example.js]
8
+ end
9
+
10
+ initializer 'log_monitor.routes' do |app|
11
+ app.routes.append do
12
+ get '/log', to: 'log#index'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,125 @@
1
+ # lib/rails-tail-log-monitor.rb
2
+
3
+ require 'rails-tail-log-monitor/engine'
4
+
5
+ module LogMonitor
6
+ class << self
7
+ attr_writer :configuration
8
+
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+ end
13
+
14
+ def self.configure
15
+ yield(configuration)
16
+ end
17
+
18
+ class Configuration
19
+ DEFAULT_ACTION_CABLE_URL = 'ws://localhost:3000/cable'.freeze
20
+ DEFAULT_KEEP_ALIVE_TIME = 30
21
+
22
+ class << self
23
+ def action_cable_url
24
+ configuration.action_cable_url
25
+ end
26
+
27
+ def keep_alive_time
28
+ configuration.keep_alive_time
29
+ end
30
+ end
31
+
32
+ attr_accessor :action_cable_url, :keep_alive_time
33
+
34
+ def initialize
35
+ @action_cable_url = DEFAULT_ACTION_CABLE_URL
36
+ @keep_alive_time = DEFAULT_KEEP_ALIVE_TIME
37
+ end
38
+
39
+ def keep_alive_time=(time)
40
+ @keep_alive_time = [time.to_i, DEFAULT_KEEP_ALIVE_TIME].max
41
+ end
42
+ end
43
+
44
+ class Railtie < Rails::Railtie
45
+ config.after_initialize do
46
+ ActionCable.server.config.logger = Logger.new(nil)
47
+ ActionCable.server.config.url = LogMonitor.configuration.action_cable_url
48
+ end
49
+ end
50
+
51
+ class LogMonitorService
52
+ ANSI_TO_HTML = {
53
+ '30' => 'black',
54
+ '31' => 'red',
55
+ '32' => 'green',
56
+ '33' => 'yellow',
57
+ '34' => 'blue',
58
+ '35' => 'magenta',
59
+ '36' => 'cyan',
60
+ '37' => 'white',
61
+ '90' => 'darkgrey',
62
+ '91' => 'lightred',
63
+ '92' => 'lightgreen',
64
+ '93' => 'lightyellow',
65
+ '94' => 'lightblue',
66
+ '95' => 'lightmagenta',
67
+ '96' => 'lightcyan',
68
+ '97' => 'lightgrey'
69
+ }.freeze
70
+
71
+ def self.start_monitoring
72
+ $monitoring_keep_alive = Time.zone.now.to_i
73
+ $monitoring_thread ||= Thread.new { monitor_log }
74
+ end
75
+
76
+ def self.stop_monitoring
77
+ $monitoring_thread&.kill
78
+ $monitoring_thread&.join
79
+ $monitoring_thread = nil
80
+ end
81
+
82
+ def self.monitor_log
83
+ log_file = Rails.root.join('log', "#{Rails.env}.log")
84
+ last_position = File.size(log_file)
85
+
86
+ begin
87
+ loop do
88
+ sleep 0.3
89
+ if Time.zone.now.to_i - $monitoring_keep_alive > LogMonitor.configuration.keep_alive_time
90
+ stop_monitoring
91
+ break
92
+ else
93
+ new_log_entries = File.open(log_file, 'r') do |file|
94
+ file.seek(last_position)
95
+ file.read
96
+ end.split("\n", -1)
97
+ if new_log_entries.present?
98
+ new_log_entries = new_log_entries[0..-2] if new_log_entries.last.empty?
99
+ ActionCable.server.broadcast(
100
+ 'log_channel', { log_entries: new_log_entries.map { |log| ansi_to_html(log) } }
101
+ )
102
+ end
103
+
104
+ last_position = File.size(log_file)
105
+ end
106
+ end
107
+ rescue StandardError => e
108
+ Rails.logger.error "Error in LogMonitorService: #{e.message}"
109
+ end
110
+ end
111
+
112
+ def self.ansi_to_html(log)
113
+ return '<br>' if log.blank?
114
+
115
+ html_text = log.dup
116
+ html_text = html_text.gsub('<', '&lt;').gsub('>', '&gt;')
117
+ ANSI_TO_HTML.each_key do |code|
118
+ html_text.gsub!(/\e\[#{code}m/, "<span style='color: #{ANSI_TO_HTML[code]};'>")
119
+ end
120
+ html_text.gsub!(/\e\[0m/, '</span>')
121
+ html_text.gsub!(/\e\[1m/, "<span style='font-weight: bold;'>")
122
+ html_text
123
+ end
124
+ end
125
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-tail-log-monitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - datpmt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
27
+ description: The rails-tail-log-monitor gem simplifies the process of monitoring server
28
+ logs by displaying the tail of the log file directly in the terminal window alongside
29
+ the standard Rails server output. With rails-tail-log-monitor, developers can effortlessly
30
+ keep track of the most recent log entries without the need for manual log file inspection.
31
+ email: datpmt.2k@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - README.md
37
+ - app/assets/javascripts/example.js
38
+ - app/channels/application_cable/channel.rb
39
+ - app/channels/application_cable/connection.rb
40
+ - app/channels/log_channel.rb
41
+ - app/controllers/log_controller.rb
42
+ - app/views/layouts/log.html.erb
43
+ - app/views/log/index.html.erb
44
+ - lib/rails-tail-log-monitor.rb
45
+ - lib/rails-tail-log-monitor/engine.rb
46
+ homepage: https://rubygems.org/gems/rails-tail-log-monitor
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ source_code_uri: https://github.com/datpmt/rails-tail-log-monitor
51
+ changelog_uri: https://github.com/datpmt/rails-tail-log-monitor/blob/main/CHANGELOG.md
52
+ rubygems_mfa_required: 'true'
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2.7'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.2.15
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: 'Purpose: Displays the tail of the server log in the terminal alongside the
72
+ Rails server output.'
73
+ test_files: []