UNPKG

pm2

Version:

Production process manager for Node.JS applications with a built-in load balancer.

567 lines (473 loc) 17.7 kB
const cst = require('../../../constants') const Common = require('../../Common') const Configuration = require('../../Configuration') const UxHelpers = require('./helpers.js') const chalk = require('ansis') const Table = require('cli-tableau') const Passwd = require('../../tools/passwd.js') const List = {} // Recomputed in listModulesAndAppsManaged() against the real table width; // this initial value is the fallback for the other display functions. let CONDENSED_MODE = (process.stdout.columns || 300) < 134 /** * Check if dump file contains same apps that the one managed by PM2 */ function checkIfProcessAreDumped(list) { try { var dump_raw = require('fs').readFileSync(cst.DUMP_FILE_PATH) var dump = JSON.parse(dump_raw) var apps_dumped = dump.map(proc => proc.name) var apps_running = list .filter(proc => proc.pm2_env.pmx_module != true) .map(proc => proc.name) var diff = apps_dumped.filter(a => !apps_running.includes(a)) if (diff.length > 0) { Common.warn(`Current process list is not synchronized with saved list. App ${chalk.bold(diff.join(' '))} differs. Type 'pm2 save' to synchronize.`) } else if (apps_dumped.length != apps_running.length) { Common.warn(`Current process list is not synchronized with saved list. Type 'pm2 save' to synchronize.`) } } catch(e) { } } var proc_id = 0 // Truncate before colorizing: cli-tableau's truncate() miscounts ANSI bytes, cutting styled strings too short and leaking style into the next column. function fitColumn(str, colWidth) { // cli-tableau content width = colWidth - padding-left(1) - padding-right(1) var width = colWidth - 2 str = String(str == null ? '' : str) if (str.length > width) str = str.substr(0, width - 1) + '…' return str } /** * List Applications and Modules managed by PM2 */ function listModulesAndAppsManaged(list, commander) { // Cap the (otherwise unbounded) name column so a single long process // name can't push the table past the terminal width. var NAME_COL_MAX = 40 var name_col_size = 11 if (list && list.length > 0) name_col_size = Math.min((list.reduce((p, c) => (p.name.length > c.name.length) ? p : c)).name.length + 5, NAME_COL_MAX) var id_width = Math.max( 2 + (Math.max(...list.map((l) => String(l.pm2_env.pm_id || 0).length)) || 0), 4 ); var app_head = { id: id_width, name: name_col_size, namespace: 13, version: 9, mode: 9, pid: 10, uptime: 8, '↺': 6, status: 11, cpu: 10, mem: 10, user: 10, watching: 10 } var mod_head = { id: id_width, module: 30, version: 15, pid: 10, status: 10, '↺': 6, cpu: 10, mem: 10, user: 10 } // cli-tableau renders at fixed widths and never shrinks to fit the // terminal. Pick the widest layout that fits: full -> condensed -> mini. // process.stdout.columns is undefined when piped, so the 300 fallback // keeps the full table for non-TTY output. var term_cols = process.stdout.columns || 300 var tableWidth = function (head) { var keys = Object.keys(head) return keys.reduce((s, k) => s + head[k], 0) + keys.length + 1 } var condensed_app = { id: id_width, name: Math.min(name_col_size, 20), mode: 10, '↺': 6, status: 11, cpu: 10, memory: 10 } var condensed_mod = { id: id_width, name: Math.min(name_col_size, 20), status: 10, cpu: 10, mem: 10 } var mini_app = { id: id_width, name: Math.min(name_col_size, 16), status: 11, cpu: 8, mem: 10 } var mini_mod = { id: id_width, name: Math.min(name_col_size, 16), status: 11, cpu: 8, mem: 10 } var MINI_MODE = false if (term_cols >= tableWidth(app_head)) { CONDENSED_MODE = false } else if (term_cols >= tableWidth(condensed_app)) { CONDENSED_MODE = true app_head = condensed_app mod_head = condensed_mod } else { CONDENSED_MODE = true MINI_MODE = true app_head = mini_app mod_head = mini_mod } var app_table = new Table({ head : Object.keys(app_head), colWidths: Object.keys(app_head).map(k => app_head[k]), colAligns : ['left'], style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true} }) var module_table = new Table({ head : Object.keys(mod_head), colWidths: Object.keys(mod_head).map(k => mod_head[k]), colAligns : ['left'], style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true} }) var sortField = 'name', sortOrder = 'asc', sort, fields = { name: 'pm2_env.name', namespace: 'pm2_env.namespace', pid: 'pid', id: 'pm_id', cpu: 'monit.cpu', memory: 'monit.memory', uptime: 'pm2_env.pm_uptime', status: 'pm2_env.status' } if (commander && commander.sort) { sort = commander.sort.split(':'); if(fields[sort[0].toLowerCase()]) { sortField = sort[0].toLowerCase(); sortOrder = sort.length === 2 ? sort[1] : 'asc'; } } list.sort(function(a, b) { var fieldA = UxHelpers.getNestedProperty(fields[sortField], a) var fieldB = UxHelpers.getNestedProperty(fields[sortField], b) if (sortOrder === 'desc') { if (fieldA > fieldB) return -1 if (fieldA < fieldB) return 1 } else { if (fieldA < fieldB) return -1 if (fieldA > fieldB) return 1 } return 0 }) list.forEach(function(l) { var obj = {} if (l.pm2_env.pm_id > proc_id) { proc_id = l.pm2_env.pm_id } var mode = l.pm2_env.exec_mode var status = l.pm2_env.status var key = l.pm2_env.pm_id key = chalk.bold.cyan(key) if (l.pm2_env.axm_options) { var is_tracing_enabled = false if (l.pm2_env.axm_options.tracing && typeof(l.pm2_env.axm_options.tracing) == 'boolean' && l.pm2_env.axm_options.tracing == true) is_tracing_enabled = true if (l.pm2_env.axm_options.tracing && l.pm2_env.axm_options.tracing.enabled && typeof(l.pm2_env.axm_options.tracing.enabled) == 'boolean' && l.pm2_env.axm_options.tracing.enabled == true) is_tracing_enabled = true if (is_tracing_enabled == true) l.pm2_env.name = chalk.green('[T]') + ' ' + l.pm2_env.name if (l.pm2_env._km_monitored) l.pm2_env.name = chalk.bold.green('[M]') + ' ' + l.pm2_env.name } if (l.pm2_env.pmx_module == true) { if (l.pm2_env.name == 'pm2-sysmonit') return // pm2 ls for Modules obj[key] = [] obj[key].push(l.name) // Module version + PID if (!CONDENSED_MODE) { var pid = l.pm2_env.axm_options.pid ? l.pm2_env.axm_options.pid : l.pid obj[key].push(l.pm2_env.version || 'N/A', pid) } // Status obj[key].push(UxHelpers.colorStatus(status)) // Restart if (!CONDENSED_MODE) obj[key].push(l.pm2_env.restart_time ? l.pm2_env.restart_time : 0) // CPU + Memory obj[key].push(l.monit ? (l.monit.cpu + '%') : 'N/A', l.monit ? UxHelpers.bytesToSize(l.monit.memory, 1) : 'N/A' ) // User if (!CONDENSED_MODE) { if (l.pm2_env.uid && typeof(l.pm2_env.uid) == 'number') { // Resolve user id to username let users = Passwd.getUsers() Object.keys(users).forEach(function(username) { var user = users[username] if (user.userId == l.pm2_env.uid) { l.pm2_env.uid = user.username } }) } obj[key].push(chalk.bold(fitColumn(l.pm2_env.uid || l.pm2_env.username, mod_head.user))) } UxHelpers.safe_push(module_table, obj) } else { // pm2 ls for Applications obj[key] = [] // PM2 ID obj[key].push(fitColumn(l.pm2_env.name, app_head.name)) // Namespace if (!CONDENSED_MODE) obj[key].push(l.pm2_env.namespace) // Version if (!CONDENSED_MODE) obj[key].push(l.pm2_env.version) // Exec mode if (!MINI_MODE) obj[key].push(mode == 'fork_mode' ? chalk.inverse.bold('fork') : chalk.blue.bold('cluster')) // PID if (!CONDENSED_MODE) obj[key].push(l.pid) // Uptime if (!CONDENSED_MODE) obj[key].push((l.pm2_env.pm_uptime && status == 'online') ? UxHelpers.timeSince(l.pm2_env.pm_uptime) : 0) // Restart if (!MINI_MODE) obj[key].push(l.pm2_env.restart_time ? l.pm2_env.restart_time : 0) // Status obj[key].push(UxHelpers.colorStatus(status)) // CPU obj[key].push(l.monit ? l.monit.cpu + '%' : 'N/A') // Memory obj[key].push(l.monit ? UxHelpers.bytesToSize(l.monit.memory, 1) : 'N/A') // User if (!CONDENSED_MODE) { if (l.pm2_env.uid && typeof(l.pm2_env.uid) == 'number') { // Resolve user id to username let users = Passwd.getUsers() Object.keys(users).forEach(function(username) { var user = users[username] if (user.userId == l.pm2_env.uid) { l.pm2_env.uid = user.username } }) } obj[key].push(chalk.bold(fitColumn(l.pm2_env.uid || l.pm2_env.username, app_head.user))) } // Watch status if (!CONDENSED_MODE) obj[key].push(l.pm2_env.watch ? chalk.green.bold('enabled') : chalk.gray('disabled')) UxHelpers.safe_push(app_table, obj) } }) // Print Applications Managed console.log(app_table.toString()) // Print Modules Managed if (module_table.length > 0) { console.log(chalk.bold(`Module${module_table.length > 1 ? 's' : ''}`)) console.log(module_table.toString()) } proc_id++ } // Container display function containersListing(sys_infos) { var stacked_docker = (process.stdout.columns || 100) < 140 var docker_head = { id: 4, image: 50, status: 10, '↺': 6, cpu: 10, mem: 10, 'net I/O ⇵': 11, 'fs I/O ⇵': 11 } if (stacked_docker) { docker_head = { id: 4, image: 25, status: 10, cpu: 10, mem: 10 } } var docker_table = new Table({ colWidths: Object.keys(docker_head).map(k => docker_head[k]), head : Object.keys(docker_head), colAligns : ['left'], style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true} }) sys_infos.containers.forEach((c) => { var cpu = c.stats.cpu_percent var mem = c.stats.mem_percent == 0 ? '0' : c.stats.mem_percent var id = chalk.bold.cyan(proc_id++) var state = UxHelpers.colorStatus(c.state) if (stacked_docker) docker_table.push([id, c.image, state, `${cpu}%`, `${mem}mb`]) else { docker_table.push([ id, c.image, state, c.restartCount, `${cpu == 0 ? '0' : cpu}%`, `${mem}mb`, `${c.stats.netIO.rx}/${isNaN(c.stats.netIO.tx) == true ? '0.0' : c.stats.netIO.tx}`, `${c.stats.blockIO.r}/${c.stats.blockIO.w}` ]) } }) console.log(chalk.bold(`Container${sys_infos.containers.length > 1 ? 's' : ''}`)) console.log(docker_table.toString()) } /** * High resource processes */ function listHighResourcesProcesses(sys_infos) { const CPU_MIN_SHOW = 60 const MEM_MIN_SHOW = 30 var sys_proc_head = ['id', 'cmd', 'pid', 'cpu', 'mem', 'uid'] var sys_proc_table = new Table({ colWidths: [4, CONDENSED_MODE ? 29 : 77, 10, 10, 10, 8], head : sys_proc_head, colAligns : ['left'], style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true} }) sys_infos.processes.cpu_sorted = sys_infos.processes.cpu_sorted.filter((proc) => { return proc.cpu > CPU_MIN_SHOW && proc.cmd.includes('node') === false && proc.cmd.includes('God Daemon') === false }) sys_infos.processes.cpu_sorted.forEach(proc => { var cpu = `${UxHelpers.colorizedMetric(proc.cpu, 40, 70, '%')}` var mem = `${UxHelpers.colorizedMetric(proc.memory, 40, 70, '%')}` var cmd = proc.cmd sys_proc_table.push([chalk.bold.cyan(proc_id++), cmd, proc.pid, cpu, mem, proc.uid]) }) sys_infos.processes.mem_sorted = sys_infos.processes.mem_sorted.filter((proc) => { return proc.memory > MEM_MIN_SHOW && proc.cmd.includes('node') == false }) sys_infos.processes.mem_sorted.forEach((proc) => { var cpu = `${UxHelpers.colorizedMetric(proc.cpu, 40, 70, '%')}` var mem = `${UxHelpers.colorizedMetric(proc.memory, 40, 70, '%')}` var cmd = proc.cmd // if (proc.cmd.length > 50) // cmd = '…' + proc.cmd.slice(proc.cmd.length - 48, proc.cmd.length) sys_proc_table.push([chalk.bold.cyan(proc_id++), cmd, proc.pid, cpu, mem, proc.uid]) }) if (sys_infos.processes.cpu_sorted.length >= 1 || sys_infos.processes.mem_sorted.length >= 1) { console.log(chalk.bold('Intensive Processes')) console.log(sys_proc_table.toString()) } } /** * Sys info line */ function miniMonitBar(sys_infos) { let sys_metrics = sys_infos.pm2_env.axm_monitor let cpu = sys_metrics['CPU Usage'] if (typeof(cpu) == 'undefined') return var sys_summary_line = `${chalk.bold.cyan('host metrics')} ` sys_summary_line += `| ${chalk.bold('cpu')}: ${UxHelpers.colorizedMetric(cpu.value, 40, 70, '%')}` let temp = sys_metrics['CPU Temperature'] ? sys_metrics['CPU Temperature'].value : null if (temp && temp != '-1') { sys_summary_line += ` ${UxHelpers.colorizedMetric(temp, 50, 70, 'º')}` } let ram_usage = sys_metrics['RAM Usage'] ? sys_metrics['RAM Usage'].value : null if (ram_usage === null) { let mem_total = sys_metrics['RAM Total'] ? sys_metrics['RAM Total'].value : null let mem_available = sys_metrics['RAM Available'] ? sys_metrics['RAM Available'].value : null if (mem_total) ram_usage = (100 - ((mem_available / mem_total) * 100)).toFixed(1) } if (ram_usage !== null && ram_usage !== undefined) { sys_summary_line += ` | ${chalk.bold('ram usage')}: ${UxHelpers.colorizedMetric(ram_usage, 70, 90, '%')} ` } // GPU memory + temperature (only when a GPU reports) if (sys_metrics['graphics:mem:total'] && Number(sys_metrics['graphics:mem:total'].value) > 0) { let gpu_total = sys_metrics['graphics:mem:total'].value let gpu_used = sys_metrics['graphics:mem:used'] ? sys_metrics['graphics:mem:used'].value : 0 sys_summary_line += `| ${chalk.bold('gpu')}: ${gpu_used}/${gpu_total}mb` let gpu_temp = sys_metrics['graphics:temp'] ? sys_metrics['graphics:temp'].value : null if (gpu_temp && gpu_temp != '-1') sys_summary_line += ` ${UxHelpers.colorizedMetric(gpu_temp, 50, 70, 'º')}` sys_summary_line += ` ` } let interfaces = Object.keys(sys_metrics).filter(m => m.includes('net') && m != 'net:default').map(i => i.split(':')[2]).filter((iface, i, self) => self.indexOf(iface) === i) interfaces.forEach(iface => { if (!sys_metrics[`net:rx_5:${iface}`]) return let rx = sys_metrics[`net:rx_5:${iface}`].value let tx = sys_metrics[`net:tx_5:${iface}`] ? sys_metrics[`net:tx_5:${iface}`].value : 0 // Only show interfaces actually carrying traffic if (!(Number(rx) > 0 || Number(tx) > 0)) return sys_summary_line += `| ${chalk.bold(iface)}: ` sys_summary_line += `⇓ ${UxHelpers.colorizedMetric(rx, 10, 20, 'mb/s')} ` sys_summary_line += `⇑ ${UxHelpers.colorizedMetric(tx, 10, 20, 'mb/s')} ` // Show errors/drops only when non-zero let rx_err = sys_metrics[`net:rx_errors_60:${iface}`] ? Number(sys_metrics[`net:rx_errors_60:${iface}`].value) : 0 let tx_err = sys_metrics[`net:tx_errors_60:${iface}`] ? Number(sys_metrics[`net:tx_errors_60:${iface}`].value) : 0 let rx_drop = sys_metrics[`net:rx_dropped_60:${iface}`] ? Number(sys_metrics[`net:rx_dropped_60:${iface}`].value) : 0 let tx_drop = sys_metrics[`net:tx_dropped_60:${iface}`] ? Number(sys_metrics[`net:tx_dropped_60:${iface}`].value) : 0 if (rx_err + tx_err > 0) sys_summary_line += `${chalk.bold('err')} ${UxHelpers.colorizedMetric(rx_err + tx_err, 1, 10, '/min')} ` if (rx_drop + tx_drop > 0) sys_summary_line += `${chalk.bold('drop')} ${UxHelpers.colorizedMetric(rx_drop + tx_drop, 1, 10, '/min')} ` }) if (CONDENSED_MODE == false && sys_metrics['Disk Reads']) { let read = sys_metrics['Disk Reads'].value let write = sys_metrics['Disk Writes'] ? sys_metrics['Disk Writes'].value : 0 sys_summary_line += `| ${chalk.bold('disk')}: ⇓ ${UxHelpers.colorizedMetric(read, 10, 20, 'mb/s')}` sys_summary_line += ` ⇑ ${UxHelpers.colorizedMetric(write, 10, 20, 'mb/s')} ` let disks = Object.keys(sys_metrics).filter(m => m.includes('fs:')).map(i => i.split(':')[2]).filter((iface, i, self) => self.indexOf(iface) === i) var disk_nb = 0 disks.forEach(fs => { if (!sys_metrics[`fs:use:${fs}`]) return let use = sys_metrics[`fs:use:${fs}`].value if (use > 60) sys_summary_line += `${chalk.gray(fs)} ${UxHelpers.colorizedMetric(use, 80, 90, '%')} ` }) } sys_summary_line += '|' console.log(sys_summary_line) } /** * pm2 ls * @method dispAsTable * @param {Object} list * @param {Object} commander * @param {Object} systemdata host metrics snapshot (daemon SysMetrics, axm_monitor-shaped) */ module.exports = function(list, commander, systemdata) { var pm2_conf = Configuration.getSync('pm2') if (!list) return console.log('list empty') listModulesAndAppsManaged(list, commander) if (systemdata && Object.keys(systemdata).length > 0) miniMonitBar({ pm2_env: { axm_monitor: systemdata } }) // Disable warning message of process list not saved //checkIfProcessAreDumped(list) }