1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | var os = require('os');
|
8 | var p = require('path');
|
9 | var blessed = require('blessed');
|
10 | var debug = require('debug')('pm2:monit');
|
11 | var printf = require('sprintf-js').sprintf;
|
12 |
|
13 |
|
14 | const totalMem = os.totalmem();
|
15 |
|
16 | var Dashboard = {};
|
17 |
|
18 | var DEFAULT_PADDING = {
|
19 | top : 0,
|
20 | left : 1,
|
21 | right : 1
|
22 | };
|
23 |
|
24 | var WIDTH_LEFT_PANEL = 30;
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | Dashboard.init = function() {
|
32 |
|
33 | this.screen = blessed.screen({
|
34 | smartCSR: true,
|
35 | fullUnicode: true
|
36 | });
|
37 | this.screen.title = 'PM2 Dashboard';
|
38 |
|
39 | this.logLines = {}
|
40 |
|
41 | this.list = blessed.list({
|
42 | top: '0',
|
43 | left: '0',
|
44 | width: WIDTH_LEFT_PANEL + '%',
|
45 | height: '70%',
|
46 | padding: 0,
|
47 | scrollbar: {
|
48 | ch: ' ',
|
49 | inverse: false
|
50 | },
|
51 | border: {
|
52 | type: 'line'
|
53 | },
|
54 | keys: true,
|
55 | autoCommandKeys: true,
|
56 | tags: true,
|
57 | style: {
|
58 | selected: {
|
59 | bg: 'blue',
|
60 | fg: 'white'
|
61 | },
|
62 | scrollbar: {
|
63 | bg: 'blue',
|
64 | fg: 'black'
|
65 | },
|
66 | fg: 'white',
|
67 | border: {
|
68 | fg: 'blue'
|
69 | },
|
70 | header: {
|
71 | fg: 'blue'
|
72 | }
|
73 | }
|
74 | });
|
75 |
|
76 | this.list.on('select item', (item, i) => {
|
77 | this.logBox.clearItems()
|
78 | })
|
79 |
|
80 | this.logBox = blessed.list({
|
81 | label: ' Logs ',
|
82 | top: '0',
|
83 | left: WIDTH_LEFT_PANEL + '%',
|
84 | width: 100 - WIDTH_LEFT_PANEL + '%',
|
85 | height: '70%',
|
86 | padding: DEFAULT_PADDING,
|
87 | scrollable: true,
|
88 | scrollbar: {
|
89 | ch: ' ',
|
90 | inverse: false
|
91 | },
|
92 | keys: true,
|
93 | autoCommandKeys: true,
|
94 | tags: true,
|
95 | border: {
|
96 | type: 'line'
|
97 | },
|
98 | style: {
|
99 | fg: 'white',
|
100 | border: {
|
101 | fg: 'white'
|
102 | },
|
103 | scrollbar: {
|
104 | bg: 'blue',
|
105 | fg: 'black'
|
106 | }
|
107 | }
|
108 | });
|
109 |
|
110 | this.metadataBox = blessed.box({
|
111 | label: ' Metadata ',
|
112 | top: '70%',
|
113 | left: WIDTH_LEFT_PANEL + '%',
|
114 | width: 100 - WIDTH_LEFT_PANEL + '%',
|
115 | height: '26%',
|
116 | padding: DEFAULT_PADDING,
|
117 | scrollable: true,
|
118 | scrollbar: {
|
119 | ch: ' ',
|
120 | inverse: false
|
121 | },
|
122 | keys: true,
|
123 | autoCommandKeys: true,
|
124 | tags: true,
|
125 | border: {
|
126 | type: 'line'
|
127 | },
|
128 | style: {
|
129 | fg: 'white',
|
130 | border: {
|
131 | fg: 'white'
|
132 | },
|
133 | scrollbar: {
|
134 | bg: 'blue',
|
135 | fg: 'black'
|
136 | }
|
137 | }
|
138 | });
|
139 |
|
140 | this.metricsBox = blessed.list({
|
141 | label: ' Custom Metrics ',
|
142 | top: '70%',
|
143 | left: '0%',
|
144 | width: WIDTH_LEFT_PANEL + '%',
|
145 | height: '26%',
|
146 | padding: DEFAULT_PADDING,
|
147 | scrollbar: {
|
148 | ch: ' ',
|
149 | inverse: false
|
150 | },
|
151 | keys: true,
|
152 | autoCommandKeys: true,
|
153 | tags: true,
|
154 | border: {
|
155 | type: 'line'
|
156 | },
|
157 | style: {
|
158 | fg: 'white',
|
159 | border: {
|
160 | fg: 'white'
|
161 | },
|
162 | scrollbar: {
|
163 | bg: 'blue',
|
164 | fg: 'black'
|
165 | }
|
166 | }
|
167 | });
|
168 |
|
169 | this.box4 = blessed.text({
|
170 | content: ' left/right: switch boards | up/down/mouse: scroll | Ctrl-C: exit{|} {cyan-fg}{bold}To go further check out https://pm2.io/{/} ',
|
171 | left: '0%',
|
172 | top: '95%',
|
173 | width: '100%',
|
174 | height: '6%',
|
175 | valign: 'middle',
|
176 | tags: true,
|
177 | style: {
|
178 | fg: 'white'
|
179 | }
|
180 | });
|
181 |
|
182 | this.list.focus();
|
183 |
|
184 | this.screen.append(this.list);
|
185 | this.screen.append(this.logBox);
|
186 | this.screen.append(this.metadataBox);
|
187 | this.screen.append(this.metricsBox);
|
188 | this.screen.append(this.box4);
|
189 |
|
190 | this.list.setLabel(' Process List ');
|
191 |
|
192 | this.screen.render();
|
193 |
|
194 | var that = this;
|
195 |
|
196 | var i = 0;
|
197 | var boards = ['list', 'logBox', 'metricsBox', 'metadataBox'];
|
198 | this.screen.key(['left', 'right'], function(ch, key) {
|
199 | (key.name === 'left') ? i-- : i++;
|
200 | if (i == 4)
|
201 | i = 0;
|
202 | if (i == -1)
|
203 | i = 3;
|
204 | that[boards[i]].focus();
|
205 | that[boards[i]].style.border.fg = 'blue';
|
206 | if (key.name === 'left') {
|
207 | if (i == 3)
|
208 | that[boards[0]].style.border.fg = 'white';
|
209 | else
|
210 | that[boards[i + 1]].style.border.fg = 'white';
|
211 | }
|
212 | else {
|
213 | if (i == 0)
|
214 | that[boards[3]].style.border.fg = 'white';
|
215 | else
|
216 | that[boards[i - 1]].style.border.fg = 'white';
|
217 | }
|
218 | });
|
219 |
|
220 | this.screen.key(['escape', 'q', 'C-c'], function(ch, key) {
|
221 | this.screen.destroy();
|
222 | process.exit(0);
|
223 | });
|
224 |
|
225 |
|
226 | setInterval(function () {
|
227 | that.screen.render();
|
228 | }, 300);
|
229 |
|
230 | return this;
|
231 | }
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 | Dashboard.refresh = function(processes) {
|
240 | debug('Monit refresh');
|
241 |
|
242 | if(!processes) {
|
243 | this.list.setItem(0, 'No process available');
|
244 | return;
|
245 | }
|
246 |
|
247 | if (processes.length != this.list.items.length) {
|
248 | this.list.clearItems();
|
249 | }
|
250 |
|
251 |
|
252 | var mem = 0;
|
253 | processes.forEach(function(proc) {
|
254 | mem += proc.monit.memory;
|
255 | })
|
256 |
|
257 |
|
258 | processes.sort(function(a, b) {
|
259 | if (a.pm2_env.name < b.pm2_env.name)
|
260 | return -1;
|
261 | if (a.pm2_env.name > b.pm2_env.name)
|
262 | return 1;
|
263 | return 0;
|
264 | });
|
265 |
|
266 |
|
267 | for (var i = 0; i < processes.length; i++) {
|
268 |
|
269 | var memPercent = (processes[i].monit.memory / mem) * 100;
|
270 |
|
271 |
|
272 | var status = processes[i].pm2_env.status == 'online' ? '{green-fg}' : '{red-fg}';
|
273 | status = status + '{bold}' + processes[i].pm2_env.status + '{/}';
|
274 |
|
275 | var name = processes[i].pm2_env.name || p.basename(processes[i].pm2_env.pm_exec_path);
|
276 |
|
277 |
|
278 | var item = printf('[%2s] %s {|} Mem: {bold}{%s-fg}%3d{/} MB CPU: {bold}{%s-fg}%2d{/} %s %s',
|
279 | processes[i].pm2_env.pm_id,
|
280 | name,
|
281 | gradient(memPercent, [255, 0, 0], [0, 255, 0]),
|
282 | (processes[i].monit.memory / 1048576).toFixed(2),
|
283 | gradient(processes[i].monit.cpu, [255, 0, 0], [0, 255, 0]),
|
284 | processes[i].monit.cpu,
|
285 | "%",
|
286 | status);
|
287 |
|
288 |
|
289 | if (this.list.getItem(i)) {
|
290 | this.list.setItem(i, item);
|
291 | }
|
292 | else {
|
293 | this.list.pushItem(item);
|
294 | }
|
295 |
|
296 | var proc = processes[this.list.selected];
|
297 |
|
298 | let process_id = proc.pm_id
|
299 | let logs = this.logLines[process_id];
|
300 | if(typeof(logs) !== "undefined"){
|
301 | this.logBox.setItems(logs)
|
302 | if (!this.logBox.focused) {
|
303 | this.logBox.setScrollPerc(100);
|
304 | }
|
305 | }else{
|
306 | this.logBox.clearItems();
|
307 | }
|
308 | this.logBox.setLabel(` ${proc.pm2_env.name} Logs `)
|
309 |
|
310 | this.metadataBox.setLine(0, 'App Name ' + '{bold}' + proc.pm2_env.name + '{/}');
|
311 | this.metadataBox.setLine(1, 'Namespace ' + '{bold}' + proc.pm2_env.namespace + '{/}');
|
312 | this.metadataBox.setLine(2, 'Version ' + '{bold}' + proc.pm2_env.version + '{/}');
|
313 | this.metadataBox.setLine(3, 'Restarts ' + proc.pm2_env.restart_time);
|
314 | this.metadataBox.setLine(4, 'Uptime ' + ((proc.pm2_env.pm_uptime && proc.pm2_env.status == 'online') ? timeSince(proc.pm2_env.pm_uptime) : 0));
|
315 | this.metadataBox.setLine(5, 'Script path ' + proc.pm2_env.pm_exec_path);
|
316 | this.metadataBox.setLine(6, 'Script args ' + (proc.pm2_env.args ? (typeof proc.pm2_env.args == 'string' ? JSON.parse(proc.pm2_env.args.replace(/'/g, '"')):proc.pm2_env.args).join(' ') : 'N/A'));
|
317 | this.metadataBox.setLine(7, 'Interpreter ' + proc.pm2_env.exec_interpreter);
|
318 | this.metadataBox.setLine(8, 'Interpreter args ' + (proc.pm2_env.node_args.length != 0 ? proc.pm2_env.node_args : 'N/A'));
|
319 | this.metadataBox.setLine(9, 'Exec mode ' + (proc.pm2_env.exec_mode == 'fork_mode' ? '{bold}fork{/}' : '{blue-fg}{bold}cluster{/}'));
|
320 | this.metadataBox.setLine(10, 'Node.js version ' + proc.pm2_env.node_version);
|
321 | this.metadataBox.setLine(11, 'watch & reload ' + (proc.pm2_env.watch ? '{green-fg}{bold}✔{/}' : '{red-fg}{bold}✘{/}'));
|
322 | this.metadataBox.setLine(12, 'Unstable restarts ' + proc.pm2_env.unstable_restarts);
|
323 |
|
324 | this.metadataBox.setLine(13, 'Comment ' + ((proc.pm2_env.versioning) ? proc.pm2_env.versioning.comment : 'N/A'));
|
325 | this.metadataBox.setLine(14, 'Revision ' + ((proc.pm2_env.versioning) ? proc.pm2_env.versioning.revision : 'N/A'));
|
326 | this.metadataBox.setLine(15, 'Branch ' + ((proc.pm2_env.versioning) ? proc.pm2_env.versioning.branch : 'N/A'));
|
327 | this.metadataBox.setLine(16, 'Remote url ' + ((proc.pm2_env.versioning) ? proc.pm2_env.versioning.url : 'N/A'));
|
328 | this.metadataBox.deleteLine(17)
|
329 | this.metadataBox.setLine(17, 'Last update ' + ((proc.pm2_env.versioning) ? proc.pm2_env.versioning.update_time : 'N/A'));
|
330 |
|
331 | if (Object.keys(proc.pm2_env.axm_monitor).length != this.metricsBox.items.length) {
|
332 | this.metricsBox.clearItems();
|
333 | }
|
334 | var j = 0;
|
335 | for (var key in proc.pm2_env.axm_monitor) {
|
336 | var metric_name = proc.pm2_env.axm_monitor[key].hasOwnProperty('value') ? proc.pm2_env.axm_monitor[key].value : proc.pm2_env.axm_monitor[key]
|
337 | var metric_unit = proc.pm2_env.axm_monitor[key].hasOwnProperty('unit') ? proc.pm2_env.axm_monitor[key].unit : null
|
338 | var probe = `{bold}${key}{/} {|} ${metric_name}${metric_unit == null ? '' : ' ' + metric_unit}`
|
339 |
|
340 | if (this.metricsBox.getItem(j)) {
|
341 | this.metricsBox.setItem(j, probe);
|
342 | }
|
343 | else {
|
344 | this.metricsBox.pushItem(probe);
|
345 | }
|
346 | j++;
|
347 | }
|
348 |
|
349 | this.screen.render();
|
350 | }
|
351 |
|
352 | return this;
|
353 | }
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 | Dashboard.log = function(type, data) {
|
362 | var that = this;
|
363 |
|
364 | if(typeof(this.logLines[data.process.pm_id]) == "undefined"){
|
365 | this.logLines[data.process.pm_id]=[];
|
366 | }
|
367 |
|
368 | switch (type) {
|
369 | case 'PM2':
|
370 | var color = '{blue-fg}';
|
371 | break;
|
372 | case 'out':
|
373 | var color = '{green-fg}';
|
374 | break;
|
375 | case 'err':
|
376 | var color = '{red-fg}';
|
377 | break;
|
378 | default:
|
379 | var color = '{white-fg}';
|
380 | }
|
381 |
|
382 | var logs = data.data.split('\n')
|
383 |
|
384 | logs.forEach((log) => {
|
385 | if (log.length > 0) {
|
386 | this.logLines[data.process.pm_id].push(color + data.process.name + '{/} > ' + log)
|
387 |
|
388 |
|
389 |
|
390 | let count = 0;
|
391 | let max_count = 0;
|
392 | let leading_process_id = -1;
|
393 |
|
394 | for(var process_id in this.logLines){
|
395 | count += this.logLines[process_id].length;
|
396 | if( this.logLines[process_id].length > max_count){
|
397 | leading_process_id = process_id;
|
398 | max_count = this.logLines[process_id].length;
|
399 | }
|
400 | }
|
401 |
|
402 | if (count > 200) {
|
403 | this.logLines[leading_process_id].shift()
|
404 | }
|
405 | }
|
406 | })
|
407 |
|
408 | return this;
|
409 | }
|
410 |
|
411 | module.exports = Dashboard;
|
412 |
|
413 | function timeSince(date) {
|
414 |
|
415 | var seconds = Math.floor((new Date() - date) / 1000);
|
416 |
|
417 | var interval = Math.floor(seconds / 31536000);
|
418 |
|
419 | if (interval > 1) {
|
420 | return interval + 'Y';
|
421 | }
|
422 | interval = Math.floor(seconds / 2592000);
|
423 | if (interval > 1) {
|
424 | return interval + 'M';
|
425 | }
|
426 | interval = Math.floor(seconds / 86400);
|
427 | if (interval > 1) {
|
428 | return interval + 'D';
|
429 | }
|
430 | interval = Math.floor(seconds / 3600);
|
431 | if (interval > 1) {
|
432 | return interval + 'h';
|
433 | }
|
434 | interval = Math.floor(seconds / 60);
|
435 | if (interval > 1) {
|
436 | return interval + 'm';
|
437 | }
|
438 | return Math.floor(seconds) + 's';
|
439 | }
|
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 | function gradient(p, rgb_beginning, rgb_end) {
|
448 |
|
449 | var w = (p / 100) * 2 - 1;
|
450 |
|
451 | var w1 = (w + 1) / 2.0;
|
452 | var w2 = 1 - w1;
|
453 |
|
454 | var rgb = [parseInt(rgb_beginning[0] * w1 + rgb_end[0] * w2),
|
455 | parseInt(rgb_beginning[1] * w1 + rgb_end[1] * w2),
|
456 | parseInt(rgb_beginning[2] * w1 + rgb_end[2] * w2)];
|
457 |
|
458 | return "#" + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1);
|
459 | }
|