Dashboard.js 12 KB

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