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 +7 -0
- data/README.md +74 -0
- data/app/assets/javascripts/example.js +601 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/channels/log_channel.rb +11 -0
- data/app/controllers/log_controller.rb +9 -0
- data/app/views/layouts/log.html.erb +40 -0
- data/app/views/log/index.html.erb +31 -0
- data/lib/rails-tail-log-monitor/engine.rb +16 -0
- data/lib/rails-tail-log-monitor.rb +125 -0
- metadata +73 -0
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
|
+

|
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,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('<', '<').gsub('>', '>')
|
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: []
|