UNPKG

cctail

Version:

Salesforce Commerce Cloud logs remote tail

229 lines (228 loc) 11.2 kB
"use strict"; 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;