kod-temp
Version:
Workano Communications SDK
345 lines (344 loc) • 11.7 kB
JavaScript
/* global window */
/* eslint-disable prefer-destructuring, no-param-reassign, no-underscore-dangle */
import moment from "moment";
import isMobile from "../utils/isMobile";
import { obfuscateToken } from "../utils/string";
const TRACE = "trace";
const DEBUG = "debug";
const INFO = "info";
const LOG = "log";
const WARN = "warn";
const ERROR = "error";
const CONSOLE_METHODS = [INFO, LOG, WARN, ERROR];
const LOG_LEVELS = [TRACE, DEBUG, INFO, LOG, WARN, ERROR];
const CATEGORY_PREFIX = "logger-category=";
const MAX_REMOTE_RETRY = 10;
const addLevelsTo = (instance, withMethods = false) => {
instance.TRACE = TRACE;
instance.DEBUG = DEBUG;
instance.INFO = INFO;
instance.LOG = LOG;
instance.WARN = WARN;
instance.ERROR = ERROR;
if (withMethods) {
instance.trace = (...args) => instance.apply(null, [TRACE, ...args]);
instance.debug = (...args) => instance.apply(null, [DEBUG, ...args]);
instance.info = (...args) => instance.apply(null, [INFO, ...args]);
instance.log = (...args) => instance.apply(null, [LOG, ...args]);
instance.warn = (...args) => instance.apply(null, [WARN, ...args]);
instance.error = (...args) => instance.apply(null, [ERROR, ...args]);
}
return instance;
};
const safeStringify = (object) => {
const result = '{"message": "Not parsable JSON"}';
try {
return JSON.stringify(object);
}
catch (e) {
// Nothing to do
}
return result;
};
class IssueReporter {
TRACE;
INFO;
LOG;
WARN;
ERROR;
oldConsoleMethods;
enabled;
remoteClientConfiguration;
buffer;
bufferTimeout;
_boundProcessBuffer;
_boundParseLoggerBody;
_callback;
constructor() {
addLevelsTo(this);
this.oldConsoleMethods = null;
this.enabled = false;
this.remoteClientConfiguration = null;
this.buffer = [];
this.bufferTimeout = null;
this._boundProcessBuffer = this._processBuffer.bind(this);
this._boundParseLoggerBody = this._parseLoggerBody.bind(this);
this._callback = null;
}
init() {
this._catchConsole();
}
setCallback(cb) {
this._callback = cb;
}
configureRemoteClient(configuration = {
tag: "workano-sdk",
host: null,
port: null,
level: null,
extra: {},
}) {
this.remoteClientConfiguration = configuration;
}
enable() {
if (!this.oldConsoleMethods) {
this.init();
}
this.enabled = true;
}
disable() {
this.enabled = false;
}
loggerFor(category) {
const logger = (level, ...args) => {
this.log.apply(this, [
level,
this._makeCategory(category),
...args,
]);
};
return addLevelsTo(logger, true);
}
removeSlashes(str) {
return str.replace(/"/g, "'").replace(/\\/g, "");
}
obfuscateHeaderToken(headers) {
const newHeaders = { ...headers };
if ("X-Auth-Token" in newHeaders) {
newHeaders["X-Auth-Token"] = obfuscateToken(newHeaders["X-Auth-Token"]);
}
return newHeaders;
}
log(level, ...args) {
if (!this.enabled) {
return;
}
// Handle category label
let category = null;
let skipSendToRemote = false;
let extra = {};
if (args[0].indexOf(CATEGORY_PREFIX) === 0) {
category = args[0].split("=")[1];
// eslint-disable-next-line no-param-reassign
args.splice(0, 1);
}
// Handle extra data as object for the last argument
const lastArg = args[args.length - 1];
if (lastArg &&
((typeof lastArg === "object" && Object.keys(lastArg).length) ||
lastArg instanceof Error)) {
if (lastArg instanceof Error) {
extra = {
errorMessage: lastArg.message,
errorStack: lastArg.stack,
errorType: lastArg.constructor.name,
skipSendToRemote: lastArg.skipSendToRemote,
};
}
else {
extra = lastArg;
}
// eslint-disable-next-line no-param-reassign
args.splice(1, 1);
}
if (extra.skipSendToRemote) {
skipSendToRemote = true;
delete extra.skipSendToRemote;
}
const date = moment().format("YYYY-MM-DD HH:mm:ss.SSS");
const message = args.join(", ");
let consoleMessage = message;
if (Object.keys(extra).length) {
const parsedExtra = safeStringify(extra);
consoleMessage = `${consoleMessage} (${parsedExtra})`;
}
if (category) {
consoleMessage = `[${category}] ${consoleMessage}`;
}
// Log the message in the console anyway (but don't console.error on mobile)
const consoleLevel = isMobile() && level === "error" ? WARN : level;
// eslint-disable-next-line
const oldMethod = this.oldConsoleMethods?.[consoleLevel] ||
this.oldConsoleMethods?.log;
oldMethod.apply(oldMethod, [date, consoleMessage]);
if (this._callback) {
this._callback(level, consoleMessage);
}
if (!skipSendToRemote) {
this._sendToRemoteLogger(level, {
date,
message,
category,
...extra,
});
}
}
logRequest(url, options, response, start) {
if (!this.enabled) {
return;
}
const { status } = response;
const duration = +new Date() - +start;
let level = TRACE;
if (status >= 400 && status < 500) {
level = WARN;
}
else if (status >= 500) {
level = ERROR;
}
this.log(level, this._makeCategory("http"), url, {
status,
body: this.removeSlashes(JSON.stringify(options.body)),
method: options.method,
headers: this.obfuscateHeaderToken(options.headers),
duration,
});
}
// Logs aren't stored anymore
getLogs() {
console.warn("IssueReporter's logs aren't stored anymore. Please use fluentd to store them");
return [];
}
getParsedLogs() {
console.warn("IssueReporter's logs aren't stored anymore. Please use fluentd to store them");
return [];
}
getReport() {
return this.getParsedLogs().join("\r\n");
}
_catchConsole() {
this.oldConsoleMethods = {};
CONSOLE_METHODS.forEach((methodName) => {
if (this.oldConsoleMethods) {
// @ts-ignore: keys
// eslint-disable-next-line
this.oldConsoleMethods[methodName] = console[methodName];
}
const parent = typeof window !== "undefined" ? window : global;
parent.console[methodName] = (...args) => {
// Store message
try {
this.log(methodName, args.join(" "));
if (this.oldConsoleMethods) {
// Use old console method to log it normally
this.oldConsoleMethods[methodName].apply(null, args);
}
}
catch (e) {
// Avoid circular structure issues
}
};
});
}
_sendToRemoteLogger(level, payload = {}) {
if (!this.remoteClientConfiguration) {
return;
}
const { level: minLevel, bufferSize } = this.remoteClientConfiguration;
if (!minLevel || this._isLevelAbove(minLevel, level)) {
return;
}
payload.level = level;
if (bufferSize > 0) {
return this._addToBuffer(payload);
}
this._sendDebugToGrafana(payload);
}
_parseLoggerBody(payload) {
const { level } = payload;
const { maxMessageSize, extra } = this.remoteClientConfiguration || {};
delete payload.level;
if (maxMessageSize &&
typeof payload.message === "string" &&
payload.message.length > maxMessageSize) {
payload.message = `${payload.message.substr(0, maxMessageSize)}...`;
}
return safeStringify({
level,
...payload,
...extra,
});
}
_addToBuffer(payload) {
// Reset buffer timer
if (this.bufferTimeout) {
clearTimeout(this.bufferTimeout);
this.bufferTimeout = null;
}
this.buffer.push(payload);
const { bufferSize, bufferTimeout } = this.remoteClientConfiguration;
if (this.buffer.length > bufferSize) {
return this._processBuffer();
}
if (bufferTimeout > 0) {
this.bufferTimeout = setTimeout(this._boundProcessBuffer, bufferTimeout);
}
}
_processBuffer() {
this._sendDebugToGrafana(this.buffer);
this.buffer = [];
if (this.bufferTimeout) {
clearTimeout(this.bufferTimeout);
this.bufferTimeout = null;
}
}
_computeRetryDelay(attempt, initial = 1000, maxWait = 50000) {
// min wait is initial(attempt = 0)
// later retries are randomly distributed between initial wait and exponential
const base = 1.5;
const wait = Math.min(initial * base ** attempt, maxWait);
const jitterWait = Math.max(initial, Math.random() * wait);
return jitterWait;
}
_sendDebugToGrafana(payload, retry = 0) {
if (!this.remoteClientConfiguration || retry >= MAX_REMOTE_RETRY) {
return;
}
const { tag, host, port } = this.remoteClientConfiguration;
const isSecure = +port === 443;
const url = `http${isSecure ? "s" : ""}://${host}${isSecure ? "" : `:${port}`}/${tag}`;
const body = Array.isArray(payload)
? `[${payload.map(this._boundParseLoggerBody).join(",")}]`
: this._parseLoggerBody(payload);
fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body,
}).catch((e) => {
e.skipSendToRemote = true;
this.log("error", this._makeCategory("grafana"), "Sending log to grafana, error", e);
// wait at least 1 second, at most 50 seconds
const wait = this._computeRetryDelay(retry, 1000, 50000);
setTimeout(() => {
if (Array.isArray(payload)) {
payload = payload.map((message) => this._writeRetryCount(message, retry + 1));
}
else if (payload && typeof payload === "object") {
payload = this._writeRetryCount(payload, retry + 1);
}
this._sendDebugToGrafana(payload, retry + 1);
}, wait);
});
}
_writeRetryCount(message, count) {
if (message && typeof message === "object") {
message._retry = count;
}
return message;
}
_isLevelAbove(level1, level2) {
const index1 = LOG_LEVELS.indexOf(level1);
const index2 = LOG_LEVELS.indexOf(level2);
if (index1 === -1 || index2 === -1) {
return false;
}
return index1 > index2;
}
_makeCategory(category) {
return `${CATEGORY_PREFIX}${category}`;
}
}
export default new IssueReporter();