cctail
Version:
Salesforce Commerce Cloud logs remote tail
229 lines (228 loc) • 11.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const colorette_1 = require("colorette");
const fs_1 = __importDefault(require("fs"));
const moment_1 = __importDefault(require("moment"));
const path_1 = __importDefault(require("path"));
const logger_1 = __importDefault(require("./logger"));
const { log } = console;
const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36";
const timeoutMs = 3000;
const initialBytesRead = 20000;
// Yup, we're single threaded. Thanks SFCC API!
const requestsMaxCount = 1;
const requestIntervalMs = 10;
let requestsPending = 0;
const axios = axios_1.default.create();
// Thank you @matthewsuan! https://gist.github.com/matthewsuan/2bdc9e7f459d5b073d58d1ebc0613169
// Axios Request Interceptor
axios.interceptors.request.use(function (config) {
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
if (requestsPending < requestsMaxCount) {
requestsPending++;
clearInterval(interval);
resolve(config);
}
}, requestIntervalMs);
});
});
// Axios Response Interceptor
axios.interceptors.response.use(function (response) {
requestsPending = Math.max(0, requestsPending - 1);
return Promise.resolve(response);
}, function (error) {
requestsPending = Math.max(0, requestsPending - 1);
return Promise.reject(error);
});
const logfetcher = {
errorcount: 0,
errorlimit: 5,
makeRequest: async function (profile, methodStr, url_suffix, headers, debug) {
if (!this.isUsingBM(profile) && !this.isUsingAPI(profile)) {
this.logMissingAuthCredentials();
process.exit(1);
}
let url = `https://${profile.hostname}/on/demandware.servlet/webdav/Sites/Logs`;
let method = methodStr;
if (url_suffix && url_suffix.length > 0) {
url += '/' + url_suffix;
}
let opts = {
method: method,
timeout: timeoutMs,
url: url,
headers: {}
};
if (this.isUsingBM(profile)) {
opts.headers.Authorization = 'Basic ' + Buffer.from(profile.username + ':' + profile.password).toString('base64');
}
else {
await this.authorize(profile, debug);
opts.headers.Authorization = profile.token;
}
if (headers && headers.size > 0) {
for (let [key, value] of headers) {
opts.headers[key] = value;
}
}
// logger.log(logger.debug, `Request: ${JSON.stringify(opts)}`, debug);
return axios.request(opts);
},
authorize: async function (profile, debug) {
if (!this.isUsingAPI(profile)) {
this.logMissingAuthCredentials();
process.exit(1);
}
if (!profile.token || !profile.token_expiry || moment_1.default.utc().isSameOrAfter(profile.token_expiry)) {
logger_1.default.log(logger_1.default.debug, `Client API token expired or not set, resetting Client API token.`);
}
else {
return;
}
let opts = {
url: 'https://account.demandware.com/dw/oauth2/access_token?grant_type=client_credentials',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
auth: {
username: profile.client_id,
password: profile.client_secret
}
};
// logger.log(logger.debug, `Request: ${JSON.stringify(opts)}`, debug);
try {
logger_1.default.log(logger_1.default.debug, `Authenticating to client API using client id ${profile.client_id}`, debug);
const response = await axios.request(opts);
profile.token = response.data.token_type.trim() + ' ' + response.data.access_token.trim();
profile.token_expiry = moment_1.default.utc().add(response.data.expires_in, 's').subtract(profile.polling_interval, 's');
logger_1.default.log(logger_1.default.debug, `Authenticated, token expires at ${profile.token_expiry.toString()}`, debug);
}
catch (err) {
logger_1.default.log(logger_1.default.error, `Error authenticating client id ${profile.client_id} - please check your credentials.\n${err}.`);
process.exit(1);
}
},
fetchLogList: async function (profile, debug, logpath = '') {
try {
if (!logpath || logpath.length === 0) {
logger_1.default.log(logger_1.default.debug, `Fetching log list from ${profile.hostname}`, debug);
}
else {
logger_1.default.log(logger_1.default.debug, `Fetching log list from ${profile.hostname}, subdirectory ${logpath}`, debug);
}
let headers = new Map([["User-Agent", ua]]);
let res = await this.makeRequest(profile, 'GET', logpath, headers, debug);
return res.data;
}
catch (err) {
logger_1.default.log(logger_1.default.error, 'Fetching log list failed with error: ' + err.message);
switch (err.status) {
case 401:
logger_1.default.log(logger_1.default.error, 'Authentication successful but access to logs folder has been denied.');
logger_1.default.log(logger_1.default.error, 'Please add required webdav permissions in BM -> Administration -> Organization -> WebDAV Client Permissions.');
logger_1.default.log(logger_1.default.error, 'Sample permissions:');
logger_1.default.log(logger_1.default.error, fs_1.default.readFileSync(path_1.default.join(__dirname, '../webdav-permissions-sample.json'), 'utf8'));
log('\n');
logger_1.default.log(logger_1.default.error, 'Exiting cctail.');
process.exit(1);
case 500:
logger_1.default.log(logger_1.default.error, 'Authentication successful but attempt to retrieve WebDAV logs failed.');
logger_1.default.log(logger_1.default.error, 'Please ensure your WebDAV permissions are syntactically correct and have no duplicate entries.');
logger_1.default.log(logger_1.default.error, 'Check in BM -> Administration -> Organization -> WebDAV Client Permissions.');
logger_1.default.log(logger_1.default.error, 'Sample permissions:');
logger_1.default.log(logger_1.default.error, fs_1.default.readFileSync(path_1.default.join(__dirname, '../webdav-permissions-sample.json'), 'utf8'));
log('\n');
logger_1.default.log(logger_1.default.error, 'Exiting cctail.');
process.exit(1);
default:
return '';
}
}
},
fetchFileSize: async function (profile, logobj) {
let size = 0;
try {
logger_1.default.log(logger_1.default.debug, (0, colorette_1.cyan)(`Fetching size for ${logobj.log}`), logobj.debug);
let res = await this.makeRequest(profile, 'HEAD', logobj.log, null, logobj.debug);
if (res.headers['content-length']) {
size = parseInt(res.headers['content-length'], 10);
logger_1.default.log(logger_1.default.debug, `Fetched size for ${logobj.log}: size ${size}`, logobj.debug);
}
else {
logger_1.default.log(logger_1.default.debug, `No content-length returned for ${logobj.log}`, logobj.debug);
}
}
catch (err) {
logger_1.default.log(logger_1.default.error, `Fetching file size of ${logobj.log} failed with error: ${err.message}`);
}
return size;
},
fetchLogContent: async function (profile, logobj) {
try {
// If logobj.size is negative, leave as-is but range starts at 0. (Log rollover case)
let range = 0;
if (logobj.log.endsWith("log")) {
if (!logobj.size) {
let size = await this.fetchFileSize(profile, logobj);
range = logobj.size = Math.max(size - initialBytesRead, 0);
}
else if (logobj.size > 0) {
range = logobj.size;
}
}
else {
logobj.size = -1;
}
let headers = new Map([["Range", `bytes=${range}-`]]);
let res = await this.makeRequest(profile, 'GET', logobj.log, headers, logobj.debug);
logger_1.default.log(logger_1.default.debug, `Fetching contents from ${logobj.log} retured status code ${res.status}`, logobj.debug);
if (res.status === 206) {
if (logobj.size < 0) {
logobj.size = res.data.length;
return [logobj, res.data];
}
if (logobj.size === 0 && res.data.length > initialBytesRead) {
logobj.size = res.data.length;
return [logobj, res.data.substring(res.data.length - initialBytesRead)];
}
logobj.size += res.data.length;
return [logobj, res.data];
}
}
catch (err) {
if (err.response) {
logger_1.default.log(logger_1.default.debug, `Fetching contents from ${logobj.log} returned status code ${err.response.status}`, logobj.debug);
}
if (!err.response || err.response.status !== 416) {
this.errorcount = this.errorcount + 1;
if (this.errorcount > 1) {
logger_1.default.log(logger_1.default.error, `Error fetching contents from ${logobj.log}: ${err.message} (error count ${this.errorcount})`);
}
else {
// don't be too verbose, just retry if this was the first error
logger_1.default.log(logger_1.default.debug, `Error fetching contents from ${logobj.log}: ${err.message} (error count ${this.errorcount})`);
}
}
}
return [logobj, ''];
},
logMissingAuthCredentials: function () {
logger_1.default.log(logger_1.default.error, ('Missing authentication credentials. Please add client_id/client_secret or username/password to log.conf.json or dw.json.'));
logger_1.default.log(logger_1.default.error, (`Sample config:\n`));
logger_1.default.log(logger_1.default.error, (fs_1.default.readFileSync(path_1.default.join(__dirname, '../log.config-sample.json'), 'utf8')));
log('\n');
},
isUsingAPI: function (profile) {
return (profile.client_id && profile.client_secret);
},
isUsingBM: function (profile) {
return (profile.username && profile.password);
}
};
exports.default = logfetcher;