cctail
Version:
Salesforce Commerce Cloud logs remote tail
322 lines (321 loc) • 13.9 kB
JavaScript
;
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();