UNPKG

cctail

Version:

Salesforce Commerce Cloud logs remote tail

322 lines (321 loc) 13.9 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const colorette_1 = require("colorette"); const fs_1 = __importDefault(require("fs")); const moment_1 = __importDefault(require("moment")); const path_1 = __importDefault(require("path")); const prompts_1 = __importDefault(require("prompts")); const underscore_string_1 = __importDefault(require("underscore.string")); const yargs_1 = __importDefault(require("yargs")); const logemitter_1 = __importDefault(require("./lib/logemitter")); const logfetcher_1 = __importDefault(require("./lib/logfetcher")); const logfluent_1 = __importDefault(require("./lib/logfluent")); const logger_1 = __importDefault(require("./lib/logger")); const logparser_1 = __importDefault(require("./lib/logparser")); let fluent; let logConfig; let profiles; let profile; let debug = false; let interactive = true; let pollingSeconds = 3; let refreshLogListSeconds = 600; let nextLogRefresh; let latestCodeprofilerLogSent; let envVarPrefix = "ENV_"; let run = async function () { let packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../package.json'), 'utf8')); logger_1.default.log(logger_1.default.info, `cctail - v${packageJson.version}`); readLogConf(); if (!profiles || Object.keys(profiles).length === 0) { logger_1.default.log(logger_1.default.warn, `No profiles in log.conf.json, checking for dw.json in path ${process.cwd()}\n`); readDwJson(); } yargs_1.default.parserConfiguration({ "parse-numbers": false }); const args = yargs_1.default.argv; if (args['d']) { debug = true; } let fileobjs = []; if (interactive) { fileobjs = await interact(args._[0]); } else { fileobjs = await dontInteract(args._[0]); } if (fileobjs.length === 0) { logger_1.default.log(logger_1.default.error, 'ERROR: No logs selected or returned, exiting.'); process.exit(-1); } setImmediate(pollLogs, fileobjs); }; let dontInteract = async function (profilename) { if (!profile) { if (Object.keys(profiles).length === 1) { profile = profiles[Object.keys(profiles)[0]]; } else if (!profilename) { logger_1.default.log(logger_1.default.error, 'ERROR: No profile selected, exiting.'); process.exit(-1); } else if (!profiles[`${profilename}`]) { logger_1.default.log(logger_1.default.error, `ERROR: Specified profile ${profilename} not found.`); process.exit(-1); } else { profile = profiles[profilename]; logger_1.default.log(logger_1.default.info, `Using profile ${profilename}.`); } setPollingInterval(profile); if (profile.refresh_loglist_interval) { refreshLogListSeconds = profile.refresh_loglist_interval; logger_1.default.log(logger_1.default.info, `Setting log list refresh interval (seconds): ${refreshLogListSeconds}`); } else { profile.refresh_loglist_interval = refreshLogListSeconds; logger_1.default.log(logger_1.default.info, `Using default log list refresh interval (seconds): ${refreshLogListSeconds}`); } } nextLogRefresh = (0, moment_1.default)().add(refreshLogListSeconds, 's'); let fileobjs = await getThatLogList(profile); let logx = []; if (profile.log_types && profile.log_types.length > 0) { for (let thisfile of fileobjs) { let logname = thisfile.log.substr(0, thisfile.log.indexOf('-')); if (profile.log_types.indexOf(logname) != -1) { logx.push(thisfile); } } } else { logx = fileobjs; } if (!profile.log_types || profile.log_types.indexOf('codeprofiler') > 0) { let cpfileobjs = await getThatLogList(profile, '.csv'); if (cpfileobjs && cpfileobjs.length > 0) { let newestcpfile = cpfileobjs.reduce((newest, compare) => newest.date.isAfter(compare.date) ? newest : compare); if (!latestCodeprofilerLogSent || newestcpfile.date.isAfter(latestCodeprofilerLogSent.date)) { logx.push(newestcpfile); latestCodeprofilerLogSent = newestcpfile; } } } return logx; }; let interact = async function (profilename) { if (!profile) { if (Object.keys(profiles).length === 1) { profile = profiles[Object.keys(profiles)[0]]; } else { if (profilename === undefined) { const profileselection = await (0, prompts_1.default)({ type: 'select', name: 'value', message: 'Select a profile:', choices: Object.keys(profiles).map(i => ({ title: ` [${i}] ${profiles[i].hostname}`, value: `${i}` })) }); profilename = profileselection.value; } if (!profilename) { logger_1.default.log(logger_1.default.error, 'ERROR: No profile selected, exiting.'); process.exit(-1); } if (!profiles[`${profilename}`]) { logger_1.default.log(logger_1.default.error, `ERROR: Specified profile ${profilename} not found.`); process.exit(-1); } profile = profiles[profilename]; } setPollingInterval(profile); } let fileobjs = await getThatLogList(profile); fileobjs.sort((a, b) => b.date.unix() - a.date.unix()); let logx = []; let logchoiche = []; for (let i in fileobjs) { let sizeformatted = underscore_string_1.default.lpad(fileobjs[i].size_string, 12); if (sizeformatted.trim() !== '0.0 kb') { sizeformatted = (0, colorette_1.yellow)(sizeformatted); } let dateformatted = underscore_string_1.default.lpad(fileobjs[i].date.format('YYYY-MM-DD HH:mm:ss'), 20); if (fileobjs[i].date.isSame(moment_1.default.utc(), 'hour')) { dateformatted = (0, colorette_1.yellow)(dateformatted); } let logname = underscore_string_1.default.rpad(fileobjs[i].log, 70); logname = logger_1.default.colorize(logname, logname); logchoiche.push({ title: `${(0, colorette_1.green)(underscore_string_1.default.lpad(i, 2))} ${logname} ${sizeformatted} ${dateformatted}`, value: i }); } let logselection = await (0, prompts_1.default)({ type: 'autocompleteMultiselect', name: 'value', message: `Select logs on [${(0, colorette_1.green)(profile.hostname)}]`, choices: logchoiche, // eslint-disable-next-line no-return-assign onState: ((statedata) => { statedata.value ? statedata.value.forEach((i) => i.title = `\n${i.title}`) : 'no selection'; }) }); if (logselection.value) { // ctrl+c logselection.value.forEach((i) => { logx.push(fileobjs[i]); }); } return logx; }; let setPollingInterval = function (profile) { if (profile.polling_interval) { pollingSeconds = profile.polling_interval; logger_1.default.log(logger_1.default.info, `Setting polling interval (seconds): ${pollingSeconds}`); } else { logger_1.default.log(logger_1.default.info, `Using default polling interval (seconds): ${pollingSeconds}`); profile.polling_interval = pollingSeconds; } }; let getThatLogList = async function (profile, filesuffix = ".log") { let fileobjs = []; let data = ''; if (filesuffix === ".csv") { data = await logfetcher_1.default.fetchLogList(profile, debug, 'codeprofiler'); } else { data = await logfetcher_1.default.fetchLogList(profile, debug); } let regexp = new RegExp(`<a href="/on/demandware.servlet/webdav/Sites/Logs/(.*?)">[\\s\\S\\&\\?]*?<td align="right">(?:<tt>)?(.*?)(?:<\\/tt>)?</td>[\\s\\S\\&\\?]*?<td align="right"><tt>(.*?)</tt></td>`, 'gim'); let match = regexp.exec(data); while (match != null) { let filedate = moment_1.default.utc(match[3]); if (match[1].substr(-4) === filesuffix && filedate.isSame(moment_1.default.utc(), 'day')) { fileobjs.push({ log: match[1], size_string: match[2], date: moment_1.default.utc(match[3]), debug: debug }); logger_1.default.log(logger_1.default.debug, `Available Log: ${match[1]}`, debug); } match = regexp.exec(data); } return fileobjs; }; let pollLogs = async function (fileobjs, doRollover = false) { if (logfetcher_1.default.isUsingAPI(profile) && logfetcher_1.default.errorcount > logfetcher_1.default.errorlimit) { logger_1.default.log(logger_1.default.error, `Error count (${logfetcher_1.default.errorcount}) exceeded limit of ${logfetcher_1.default.errorlimit}, resetting Client API token.`); logfetcher_1.default.errorcount = 0; profile.token = null; await logfetcher_1.default.authorize(profile, debug); } if (!doRollover) { if (moment_1.default.utc().isAfter(fileobjs[0].date, 'day')) { logger_1.default.log(logger_1.default.info, 'Logs have rolled over, collecting last entries from old logs.'); doRollover = true; } else { logger_1.default.log(logger_1.default.debug, 'Logs have not rolled over since last poll cycle.', debug); if (nextLogRefresh && (0, moment_1.default)().isSameOrAfter(nextLogRefresh)) { logger_1.default.log(logger_1.default.debug, 'Refreshing log list.', debug); let newfiles = await dontInteract(); for (let newfile of newfiles) { if (!fileobjs.some(logfile => logfile.log === newfile.log)) { logger_1.default.log(logger_1.default.debug, `Added new log file: ${newfile.log}.`, debug); fileobjs.push(newfile); } } } } if (fluent) { fluent.output(profile.hostname, await logparser_1.default.process(fileobjs.map((logobj) => logfetcher_1.default.fetchLogContent(profile, logobj))), false, fileobjs[0].debug); } else { let parsed = logemitter_1.default.sort(await logparser_1.default.process(fileobjs.map((logobj) => logfetcher_1.default.fetchLogContent(profile, logobj)))); logemitter_1.default.output(parsed, false, fileobjs[0].debug); } // Codeprofiler files should only be consumed once let cp = fileobjs.findIndex(logobj => logobj.log.endsWith("csv")); if (cp > -1) { logger_1.default.log(logger_1.default.debug, `Removed codeprofiler log ${fileobjs[cp].log} from list.`, debug); fileobjs.splice(cp, 1); } } else { if (interactive) { fileobjs = await interact(); } else { fileobjs = await dontInteract(); } if (fileobjs.length != 0) { doRollover = false; for (let i of fileobjs) { i.size = -1; } } else { logger_1.default.log(logger_1.default.warn, 'No logs to report yet, waiting until next cycle.'); } } setTimeout(pollLogs, pollingSeconds * 1000, fileobjs, doRollover); }; function replaceEnvPlaceholders(data) { Object.keys(data).forEach(function (key) { var value = data[key]; if (typeof (value) === 'object') { replaceEnvPlaceholders(value); } else if (typeof (value) === 'string' && value.startsWith(envVarPrefix)) { var checkForVar = value.replace(envVarPrefix, ""); if (process.env.hasOwnProperty(checkForVar)) { data[key] = process.env[checkForVar]; } } }); return data; } function readDwJson() { let dwJsonPath = path_1.default.join(process.cwd(), 'dw.json'); logger_1.default.log(logger_1.default.info, `Loading profile from ${dwJsonPath}\n`); try { const dwJson = replaceEnvPlaceholders(JSON.parse(fs_1.default.readFileSync(dwJsonPath, 'utf8'))); const name = dwJson.profile || dwJson.hostname.split('-')[0].split('-')[0]; profiles[name] = dwJson; } catch (err) { logger_1.default.log(logger_1.default.error, `No dw.json found in path ${process.cwd()}\n`); process.exit(-1); } } function readLogConf() { var _a; try { logConfig = replaceEnvPlaceholders(JSON.parse(fs_1.default.readFileSync(`${process.cwd()}/log.conf.json`, 'utf8'))); profiles = (_a = logConfig.profiles) !== null && _a !== void 0 ? _a : logConfig; // support for old configs (without "profiles" group) if (logConfig.interactive !== undefined && logConfig.interactive === false) { interactive = false; logger_1.default.log(logger_1.default.info, "Interactive mode is disabled."); } if (logConfig.fluent && logConfig.fluent.enabled) { let fluentConfig = logConfig.fluent; fluent = new logfluent_1.default(fluentConfig); logger_1.default.log(logger_1.default.info, "FluentD output is enabled."); } else { logger_1.default.log(logger_1.default.info, "Console output is enabled."); } } catch (err) { logger_1.default.log(logger_1.default.error, `\nMissing or invalid log.conf.json.\nError message: ${err}\n`); process.exit(-1); } } run();