UNPKG

@apify/log

Version:

Tools and constants shared across Apify projects.

620 lines (611 loc) 20.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/log_consts.ts var LogLevel = /* @__PURE__ */ ((LogLevel2) => { LogLevel2[LogLevel2["OFF"] = 0] = "OFF"; LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR"; LogLevel2[LogLevel2["SOFT_FAIL"] = 2] = "SOFT_FAIL"; LogLevel2[LogLevel2["WARNING"] = 3] = "WARNING"; LogLevel2[LogLevel2["INFO"] = 4] = "INFO"; LogLevel2[LogLevel2["DEBUG"] = 5] = "DEBUG"; LogLevel2[LogLevel2["PERF"] = 6] = "PERF"; return LogLevel2; })(LogLevel || {}); var LEVELS = LogLevel; var LEVEL_TO_STRING = Object.keys(LogLevel).filter((x) => Number.isNaN(+x)); var LogFormat = /* @__PURE__ */ ((LogFormat2) => { LogFormat2["JSON"] = "JSON"; LogFormat2["TEXT"] = "TEXT"; return LogFormat2; })(LogFormat || {}); var IS_APIFY_LOGGER_EXCEPTION = Symbol("apify.processed_error"); var PREFIX_DELIMITER = ":"; var TRUNCATION_FLAG_KEY = "[TRUNCATED]"; var TRUNCATION_SUFFIX = "...[truncated]"; var PREFERRED_ID_FIELDS = [ "_id", "id", "userId", "impersonatedUserId", "impersonatingUserId", "adminUserId", "actorId", "actorTaskId", "taskId", "buildId", "buildNumber", "runId" ]; var PREFERRED_ERROR_FIELDS = [ "name", "message", "stack", "cause" ]; var PREFERRED_HTTP_FIELDS = [ "url", "method", "code", "status", "statusCode", "statusText" ]; var PREFERRED_API_ERROR_FIELDS = [ "errorCode", "errorMessage", "errorResponse" ]; var PREFERRED_DATA_FIELDS = [ "response", "request", "data", "payload", "details", "exception", "config", "headers" ]; var PREFERRED_FIELDS = [ ...PREFERRED_ID_FIELDS, ...PREFERRED_ERROR_FIELDS, ...PREFERRED_HTTP_FIELDS, ...PREFERRED_API_ERROR_FIELDS, ...PREFERRED_DATA_FIELDS ]; // src/log_helpers.ts import { APIFY_ENV_VARS } from "@apify/consts"; function truncate(str, maxLength, suffix = TRUNCATION_SUFFIX) { maxLength = Math.floor(maxLength); if (suffix.length > maxLength) { throw new Error("suffix string cannot be longer than maxLength"); } if (typeof str === "string" && str.length > maxLength) { str = str.substr(0, maxLength - suffix.length) + suffix; } return str; } __name(truncate, "truncate"); function getLevelFromEnv() { const envVar = process.env[APIFY_ENV_VARS.LOG_LEVEL]; if (!envVar) return 4 /* INFO */; if (Number.isFinite(+envVar)) return +envVar; if (LogLevel[envVar]) return LogLevel[envVar]; return +envVar; } __name(getLevelFromEnv, "getLevelFromEnv"); function getFormatFromEnv() { const envVar = process.env[APIFY_ENV_VARS.LOG_FORMAT] || "TEXT" /* TEXT */; switch (envVar.toLowerCase()) { case "JSON" /* JSON */.toLowerCase(): return "JSON" /* JSON */; case "TEXT" /* TEXT */.toLowerCase(): return "TEXT" /* TEXT */; default: console.warn(`Unknown value for environment variable ${APIFY_ENV_VARS.LOG_FORMAT}: ${envVar}`); return "TEXT" /* TEXT */; } } __name(getFormatFromEnv, "getFormatFromEnv"); function sanitizeData(data, options) { const { maxDepth = Infinity, gradualLimitFactor = 1, maxStringLength = Infinity, maxArrayLength = Infinity, maxFields = Infinity, preferredFieldsMap = {}, truncationSuffix = TRUNCATION_SUFFIX, truncationFlagKey = TRUNCATION_FLAG_KEY } = options; if (typeof data === "string") { return data.length > maxStringLength ? truncate(data, maxStringLength, truncationSuffix) : data; } if (["number", "boolean", "symbol", "bigint"].includes(typeof data) || data == null || data instanceof Date) { return data; } if (data instanceof Error) { const { name, message, stack, cause, ...rest } = data; data = { name, message, stack, cause, ...rest, [IS_APIFY_LOGGER_EXCEPTION]: true }; } const nextCall = /* @__PURE__ */ __name((dat) => sanitizeData( dat, { ...options, maxDepth: maxDepth - 1, maxStringLength: Math.max( Math.floor(maxStringLength * gradualLimitFactor), truncationSuffix.length // always at least the length of the truncation suffix ), maxArrayLength: Math.floor(maxArrayLength * gradualLimitFactor), maxFields: Math.floor(maxFields * gradualLimitFactor) } ), "nextCall"); if (Array.isArray(data)) { if (maxDepth <= 0) return "[array]"; const sanitized = data.slice(0, maxArrayLength).map(nextCall); if (data.length > maxArrayLength) { sanitized.push(truncationSuffix); } return sanitized; } if (typeof data === "object" && data !== null) { if (maxDepth <= 0) return "[object]"; const allKeys = Reflect.ownKeys(data); allKeys.sort((a, b) => { const aIndex = preferredFieldsMap[String(a)] ?? -1; const bIndex = preferredFieldsMap[String(b)] ?? -1; if (aIndex === -1 && bIndex === -1) return 0; if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); const sanitized = {}; allKeys.slice(0, maxFields).forEach((key) => { sanitized[key] = nextCall(data[key]); }); if (allKeys.length > maxFields) { sanitized[truncationFlagKey] = true; } return sanitized; } if (typeof data === "function") { return "[function]"; } console.log(`WARNING: Object cannot be logged: ${data}`); return void 0; } __name(sanitizeData, "sanitizeData"); // src/logger.ts import { EventEmitter } from "events"; var _Logger = class _Logger extends EventEmitter { constructor(options) { super(); this.options = options; } setOptions(options) { this.options = { ...this.options, ...options }; } getOptions() { return this.options; } _outputWithConsole(level, line) { switch (level) { case 1 /* ERROR */: console.error(line); break; case 3 /* WARNING */: console.warn(line); break; case 5 /* DEBUG */: console.debug(line); break; default: console.log(line); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars _log(level, message, data, exception, opts = {}) { throw new Error("log() method must be implemented!"); } log(level, message, ...args) { const line = this._log(level, message, ...args); this.emit("line", line); } }; __name(_Logger, "Logger"); var Logger = _Logger; // src/logger_json.ts var DEFAULT_OPTIONS = { skipLevelInfo: false, skipTime: false }; var _LoggerJson = class _LoggerJson extends Logger { constructor(options = {}) { super({ ...DEFAULT_OPTIONS, ...options }); } _log(level, message, data, exception, opts = {}) { const { prefix, suffix } = opts; if (exception) data = { ...data, exception }; if (prefix) message = `${prefix}${PREFIX_DELIMITER} ${message}`; if (suffix) message = `${message} ${suffix}`; const rec = { time: !this.options.skipTime ? /* @__PURE__ */ new Date() : void 0, level: this.options.skipLevelInfo && level === 4 /* INFO */ ? void 0 : LogLevel[level], msg: message, ...data }; const line = JSON.stringify(rec); this._outputWithConsole(level, line); return line; } }; __name(_LoggerJson, "LoggerJson"); var LoggerJson = _LoggerJson; // src/logger_text.ts import c2 from "ansi-colors"; // src/node_internals.ts import c from "ansi-colors"; function identicalSequenceRange(a, b) { for (let i = 0; i < a.length - 3; i++) { const pos = b.indexOf(a[i]); if (pos !== -1) { const rest = b.length - pos; if (rest > 3) { let len = 1; const maxLen = Math.min(a.length - i, rest); while (maxLen > len && a[i + len] === b[pos + len]) { len++; } if (len > 3) { return { len, offset: i }; } } } } return { len: 0, offset: 0 }; } __name(identicalSequenceRange, "identicalSequenceRange"); function getStackString(error) { return error.stack ? String(error.stack) : Error.prototype.toString.call(error); } __name(getStackString, "getStackString"); function getStackFrames(err, stack) { const frames = stack.split("\n"); let cause; try { ({ cause } = err); } catch { } if (cause != null && typeof cause === "object" && IS_APIFY_LOGGER_EXCEPTION in cause) { const causeStack = getStackString(cause); const causeStackStart = causeStack.indexOf("\n at"); if (causeStackStart !== -1) { const causeFrames = causeStack.slice(causeStackStart + 1).split("\n"); const { len, offset } = identicalSequenceRange(frames, causeFrames); if (len > 0) { const skipped = len - 2; const msg = ` ... ${skipped} lines matching cause stack trace ...`; frames.splice(offset + 1, skipped, c.grey(msg)); } } } return frames; } __name(getStackFrames, "getStackFrames"); // src/logger_text.ts var SHORTEN_LEVELS = { SOFT_FAIL: "SFAIL", WARNING: "WARN" }; var LEVEL_TO_COLOR = { [1 /* ERROR */]: "red", [2 /* SOFT_FAIL */]: "red", [3 /* WARNING */]: "yellow", [4 /* INFO */]: "green", [5 /* DEBUG */]: "blue", [6 /* PERF */]: "magenta" }; var SHORTENED_LOG_LEVELS = LEVEL_TO_STRING.map((level) => SHORTEN_LEVELS[level] || level); var MAX_LEVEL_LENGTH_SPACES = Math.max(...SHORTENED_LOG_LEVELS.map((l) => l.length)); var getLevelIndent = /* @__PURE__ */ __name((level) => { let spaces = ""; for (let i = 0; i < MAX_LEVEL_LENGTH_SPACES - level.length; i++) spaces += " "; return spaces; }, "getLevelIndent"); var DEFAULT_OPTIONS2 = { skipTime: true }; var _LoggerText = class _LoggerText extends Logger { constructor(options = {}) { super({ ...DEFAULT_OPTIONS2, ...options }); } _log(level, message, data, exception, opts = {}) { let { prefix, suffix } = opts; let maybeDate = ""; if (!this.options.skipTime) { maybeDate = `${(/* @__PURE__ */ new Date()).toISOString().replace("Z", "").replace("T", " ")} `; } const errStack = exception ? this._parseException(exception) : ""; const color = LEVEL_TO_COLOR[level]; const levelStr = SHORTENED_LOG_LEVELS[level]; const levelIndent = getLevelIndent(levelStr); const dataStr = !data ? "" : ` ${JSON.stringify(data)}`; prefix = prefix ? ` ${prefix}${PREFIX_DELIMITER}` : ""; suffix = suffix ? ` ${suffix}` : ""; const line = `${c2.gray(maybeDate)}${c2[color](levelStr)}${levelIndent}${c2.yellow(prefix)} ${message || ""}${c2.gray(dataStr)}${c2.yellow(suffix)}${errStack}`; this._outputWithConsole(level, line); return line; } _parseException(exception, indentLevel = 1) { if (["string", "boolean", "number", "undefined", "bigint"].includes(typeof exception)) { return ` ${exception}`; } if (exception === null) { return "\nnull"; } if (typeof exception === "symbol") { return ` ${exception.toString()}`; } if (typeof exception === "object" && IS_APIFY_LOGGER_EXCEPTION in exception) { return this._parseLoggerException(exception, indentLevel); } return ` ${JSON.stringify(exception, null, 2)}`; } _parseLoggerException(exception, indentLevel = 1) { const errDetails = []; if (exception.type) { errDetails.push(`type=${exception.type}`); } if (exception.details) { Object.entries(exception.details).map(([key, val]) => errDetails.push(`${key}=${val}`)); } const errorString = exception.stack || exception.reason || exception.message; const isStack = errorString === exception.stack; const errorLines = getStackFrames(exception, errorString); if (isStack) { errorLines[0] = exception.message || errorLines[0]; } if (errDetails.length) { errorLines[0] += c2.gray(`(details: ${errDetails.join(", ")})`); } for (let i = 1; i < errorLines.length; i++) { errorLines[i] = c2.gray(errorLines[i]); } if (exception.cause) { const causeString = this._parseException(exception.cause, indentLevel + 1); const causeLines = causeString.trim().split("\n"); errorLines.push(c2.red(` CAUSE: ${c2.reset(causeLines[0])}`), ...causeLines.slice(1)); } return ` ${errorLines.map((line) => `${" ".repeat(indentLevel * 2)}${line}`).join("\n")}`; } }; __name(_LoggerText, "LoggerText"); var LoggerText = _LoggerText; // src/log.ts var getLoggerForFormat = /* @__PURE__ */ __name((format) => { switch (format) { case "JSON" /* JSON */: return new LoggerJson(); case "TEXT" /* TEXT */: default: return new LoggerText(); } }, "getLoggerForFormat"); var getDefaultOptions = /* @__PURE__ */ __name(() => ({ level: getLevelFromEnv(), maxDepth: 4, gradualLimitFactor: 1 / 2, // at each depth level, the limits will be reduced by half maxStringLength: 1e3, maxArrayLength: 500, maxFields: 20, preferredFields: [...PREFERRED_FIELDS], prefix: null, suffix: null, truncationSuffix: TRUNCATION_SUFFIX, truncationFlagKey: TRUNCATION_FLAG_KEY, logger: getLoggerForFormat(getFormatFromEnv()), data: {} }), "getDefaultOptions"); var _Log = class _Log { constructor(options = {}) { /** * Map of available log levels that's useful for easy setting of appropriate log levels. * Each log level is represented internally by a number. Eg. `log.LEVELS.DEBUG === 5`. */ __publicField(this, "LEVELS", LogLevel); // for BC __publicField(this, "options"); /** Maps preferred fields to their index for faster lookup */ __publicField(this, "preferredFieldsMap"); __publicField(this, "warningsOnceLogged", /* @__PURE__ */ new Set()); this.options = { ...getDefaultOptions(), ...options }; if (!LogLevel[this.options.level]) throw new Error('Options "level" must be one of log.LEVELS enum!'); if (typeof this.options.maxDepth !== "number") throw new Error('Options "maxDepth" must be a number!'); if (typeof this.options.gradualLimitFactor !== "number") throw new Error('Options "gradualLimitFactor" must be a number!'); if (typeof this.options.maxStringLength !== "number") throw new Error('Options "maxStringLength" must be a number!'); if (typeof this.options.maxArrayLength !== "number") throw new Error('Options "maxArrayLength" must be a number!'); if (typeof this.options.maxFields !== "number") throw new Error('Options "maxFields" must be a number!'); if (!Array.isArray(this.options.preferredFields)) throw new Error('Options "preferredFields" must be an array!'); if (this.options.prefix && typeof this.options.prefix !== "string") throw new Error('Options "prefix" must be a string!'); if (this.options.suffix && typeof this.options.suffix !== "string") throw new Error('Options "suffix" must be a string!'); if (typeof this.options.truncationSuffix !== "string") throw new Error('Options "truncationSuffix" must be a string!'); if (typeof this.options.truncationFlagKey !== "string") throw new Error('Options "truncationFlagKey" must be a string!'); if (typeof this.options.logger !== "object") throw new Error('Options "logger" must be an object!'); if (typeof this.options.data !== "object") throw new Error('Options "data" must be an object!'); this.preferredFieldsMap = Object.fromEntries( this.options.preferredFields.map((field, index) => [field, index]) ); } _sanitizeData(obj) { return sanitizeData( obj, { maxDepth: this.options.maxDepth, gradualLimitFactor: this.options.gradualLimitFactor, maxStringLength: this.options.maxStringLength, maxArrayLength: this.options.maxArrayLength, maxFields: this.options.maxFields, preferredFieldsMap: this.preferredFieldsMap, truncationSuffix: this.options.truncationSuffix, truncationFlagKey: this.options.truncationFlagKey } ); } /** * Returns the currently selected logging level. This is useful for checking whether a message * will actually be printed to the console before one actually performs a resource intensive operation * to construct the message, such as querying a DB for some metadata that need to be added. If the log * level is not high enough at the moment, it doesn't make sense to execute the query. */ getLevel() { return this.options.level; } /** * Sets the log level to the given value, preventing messages from less important log levels * from being printed to the console. Use in conjunction with the `log.LEVELS` constants such as * * ``` * log.setLevel(log.LEVELS.DEBUG); * ``` * * Default log level is INFO. */ setLevel(level) { if (!LogLevel[level]) throw new Error('Options "level" must be one of log.LEVELS enum!'); this.options.level = level; } internal(level, message, data, exception) { if (level > this.options.level) return; data = { ...this.options.data, ...data }; data = Reflect.ownKeys(data).length > 0 ? this._sanitizeData(data) : void 0; exception = this._sanitizeData(exception); this.options.logger.log(level, message, data, exception, { prefix: this.options.prefix, suffix: this.options.suffix }); } /** * Configures logger. */ setOptions(options) { this.options = { ...this.options, ...options }; } /** * Returns the logger configuration. */ getOptions() { return { ...this.options }; } /** * Creates a new instance of logger that inherits settings from a parent logger. */ child(options) { let { prefix } = this.options; if (options.prefix) { prefix = prefix ? `${prefix}${PREFIX_DELIMITER}${options.prefix}` : options.prefix; } const data = options.data ? { ...this.options.data, ...options.data } : this.options.data; const newOptions = { ...this.options, ...options, prefix, data }; return new _Log(newOptions); } /** * Logs an `ERROR` message. Use this method to log error messages that are not directly connected * to an exception. For logging exceptions, use the `log.exception` method. */ error(message, data) { this.internal(1 /* ERROR */, message, data); } /** * Logs an `ERROR` level message with a nicely formatted exception. Note that the exception is the first parameter * here and an additional message is only optional. */ exception(exception, message, data) { this.internal(1 /* ERROR */, message, data, exception); } softFail(message, data) { this.internal(2 /* SOFT_FAIL */, message, data); } /** * Logs a `WARNING` level message. Data are stringified and appended to the message. */ warning(message, data) { this.internal(3 /* WARNING */, message, data); } /** * Logs an `INFO` message. `INFO` is the default log level so info messages will be always logged, * unless the log level is changed. Data are stringified and appended to the message. */ info(message, data) { this.internal(4 /* INFO */, message, data); } /** * Logs a `DEBUG` message. By default, it will not be written to the console. To see `DEBUG` * messages in the console, set the log level to `DEBUG` either using the `log.setLevel(log.LEVELS.DEBUG)` * method or using the environment variable `APIFY_LOG_LEVEL=DEBUG`. Data are stringified and appended * to the message. */ debug(message, data) { this.internal(5 /* DEBUG */, message, data); } perf(message, data) { this.internal(6 /* PERF */, message, data); } /** * Logs a `WARNING` level message only once. */ warningOnce(message) { if (this.warningsOnceLogged.has(message)) return; this.warningsOnceLogged.add(message); this.warning(message); } /** * Logs given message only once as WARNING. It's used to warn user that some feature he is using has been deprecated. */ deprecated(message) { this.warningOnce(message); } }; __name(_Log, "Log"); var Log = _Log; // src/index.ts var log = new Log(); var index_default = log; export { IS_APIFY_LOGGER_EXCEPTION, LEVELS, LEVEL_TO_STRING, Log, LogFormat, LogLevel, Logger, LoggerJson, LoggerText, PREFERRED_API_ERROR_FIELDS, PREFERRED_DATA_FIELDS, PREFERRED_ERROR_FIELDS, PREFERRED_FIELDS, PREFERRED_HTTP_FIELDS, PREFERRED_ID_FIELDS, PREFIX_DELIMITER, TRUNCATION_FLAG_KEY, TRUNCATION_SUFFIX, index_default as default, getFormatFromEnv, getLevelFromEnv, sanitizeData, truncate }; //# sourceMappingURL=index.mjs.map