UNPKG

12.1 kBJavaScriptView Raw
1/**
2 * Copyright 2013-2021 the PM2 project authors. All rights reserved.
3 * Use of this source code is governed by a license that
4 * can be found in the LICENSE file.
5 */
6
7var os = require('os');
8var p = require('path');
9var blessed = require('blessed');
10var debug = require('debug')('pm2:monit');
11var printf = require('fast-printf').printf;
12
13// Total memory
14const totalMem = os.totalmem();
15
16var Dashboard = {};
17
18var DEFAULT_PADDING = {
19 top : 0,
20 left : 1,
21 right : 1
22};
23
24var WIDTH_LEFT_PANEL = 30;
25
26/**
27 * Synchronous Dashboard init method
28 * @method init
29 * @return this
30 */
31Dashboard.init = function() {
32 // Init Screen
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 // async refresh of the ui
226 setInterval(function () {
227 that.screen.render();
228 }, 300);
229
230 return this;
231}
232
233/**
234 * Refresh dashboard
235 * @method refresh
236 * @param {} processes
237 * @return this
238 */
239Dashboard.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 // Total of processes memory
252 var mem = 0;
253 processes.forEach(function(proc) {
254 mem += proc.monit.memory;
255 })
256
257 // Sort process list
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 // Loop to get process infos
267 for (var i = 0; i < processes.length; i++) {
268 // Percent of memory use by one process in all pm2 processes
269 var memPercent = (processes[i].monit.memory / mem) * 100;
270
271 // Status of process
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 // Line of list
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 // Check if item exist
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 // render the logBox
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 * Put Log
357 * @method log
358 * @param {} data
359 * @return this
360 */
361Dashboard.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 // Logs colors
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 //removing logs if longer than limit
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
411module.exports = Dashboard;
412
413function 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/* Args :
442 * p : Percent 0 - 100
443 * rgb_ : Array of rgb [255, 255, 255]
444 * Return :
445 * Hexa #FFFFFF
446 */
447function 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}