UNPKG

typescript-logging

Version:

TypeScript Logging core written in and to be used by TypeScript (this is the core project, you need to install a flavor too).

675 lines (660 loc) 24.1 kB
/** * Extends Map and adds a few convenient functions. */ class EnhancedMap extends Map { /** * If key has a mapping already returns the currently associated value. If * there is no mapping, calls the computer which must return a value V. * The value is then stored for given key and returned. * @param key Key * @param computer Computer which is called only if key has no mapping yet. * @return Existing value if the key already existed, or the newly computed value. */ computeIfAbsent(key, computer) { if (this.has(key)) { return this.get(key); } const newValue = computer(key); this.set(key, newValue); return newValue; } /** * If the key exists already calls given computer, if the key does not exist * this method does nothing. * * The computer is called with current key and current value associated. The * computer can return a (new) value V or undefined. When undefined is returned * the key is removed from this map, when a V is returned the key is updated * with the new value V. * @param key Key * @param computer Computer which is called only if the key has a mapping already * @return Undefined if the key has no mapping, otherwise the value returned from computer */ computeIfPresent(key, computer) { const currentValue = this.get(key); if (currentValue === undefined) { return undefined; } const newValue = computer(key, currentValue); if (newValue !== undefined) { this.set(key, newValue); } else { this.delete(key); } return newValue; } /** * Computes a value for given key, the computer can return a value V (in which case the map * will set the value for given key), if it returns undefined the mapping for key K will be * removed. * @param key Key to compute * @param computer Computer which is called, note that the currentValue argument contains the existing * value or is undefined when no mapping exists for the key. * @return The newly computed value */ compute(key, computer) { const currentValue = this.get(key); const newValue = computer(key, currentValue); if (newValue) { this.set(key, newValue); } else { this.delete(key); } return newValue; } } /** * Internal log level (note: do NOT use LogLevel, or we get circular loading issues!) */ var InternalLogLevel; (function (InternalLogLevel) { InternalLogLevel[InternalLogLevel["Trace"] = 0] = "Trace"; InternalLogLevel[InternalLogLevel["Debug"] = 1] = "Debug"; InternalLogLevel[InternalLogLevel["Info"] = 2] = "Info"; InternalLogLevel[InternalLogLevel["Warn"] = 3] = "Warn"; InternalLogLevel[InternalLogLevel["Error"] = 4] = "Error"; })(InternalLogLevel || (InternalLogLevel = {})); /** * Internal logger, this is NOT for end users. Instead this is used to enable logging for typescript-logging itself in case of problems. * * @param name Name of logger */ function getInternalLogger(name) { return provider.getLogger(name); } /** * Can be used to change the *internal* logging of the library. * Has no effect on end user logging. * * As such should normally not be used by end users. */ const INTERNAL_LOGGING_SETTINGS = { /** * Changes the log level for the internal logging (for all new and existing loggers) * @param level New log level */ setInternalLogLevel: (level) => provider.changeLogLevel(level), /** * Changes where messages are written to for all new and existing loggers), * by default they are written to the console. * @param fnOutput Function to write messages to */ setOutput: (fnOutput) => provider.changeOutput(fnOutput), /** * Resets the log level and output back to defaults (level to error and writing to console) * for all new and existing loggers. */ reset: () => provider.reset(), }; class InternalLoggerImpl { constructor(name, level, fnOutput) { this._name = name; this._level = level; this._fnOutput = fnOutput; } trace(msg) { this.log(InternalLogLevel.Trace, msg); } debug(msg) { this.log(InternalLogLevel.Debug, msg); } error(msg, error) { this.log(InternalLogLevel.Error, msg, error); } info(msg) { this.log(InternalLogLevel.Info, msg); } warn(msg, error) { this.log(InternalLogLevel.Warn, msg, error); } setLevel(level) { this._level = level; } setOutput(fnOutput) { this._fnOutput = fnOutput; } log(level, msg, error) { if (this._level > level) { return; } // tslint:disable-next-line:no-console this._fnOutput(`${InternalLogLevel[this._level].toString()} <INTERNAL LOGGER> ${this._name} ${msg()}${error ? "\n" + error.stack : ""}`); } } class InternalProviderImpl { constructor() { this._loggers = new EnhancedMap(); this._logLevel = InternalLogLevel.Error; this._fnOutput = InternalProviderImpl.logConsole; } getLogger(name) { return this._loggers.computeIfAbsent(name, key => new InternalLoggerImpl(key, this._logLevel, this._fnOutput)); } changeLogLevel(level) { this._logLevel = level; this._loggers.forEach(logger => logger.setLevel(level)); } changeOutput(_fnOutput) { this._fnOutput = _fnOutput; this._loggers.forEach(logger => logger.setOutput(this._fnOutput)); } reset() { this.changeLogLevel(InternalLogLevel.Error); this._fnOutput = InternalProviderImpl.logConsole; this._loggers.forEach(logger => logger.setOutput(this._fnOutput)); } static logConsole(msg) { // tslint:disable-next-line:no-console if (console && console.log) { // tslint:disable-next-line:no-console console.log(msg); } } } const provider = new InternalProviderImpl(); var InternalLogger = /*#__PURE__*/Object.freeze({ __proto__: null, INTERNAL_LOGGING_SETTINGS: INTERNAL_LOGGING_SETTINGS, get InternalLogLevel () { return InternalLogLevel; }, getInternalLogger: getInternalLogger }); /** * Log level for a logger. */ var LogLevel; (function (LogLevel) { // Do not change values/order. Logging a message relies on this. LogLevel[LogLevel["Trace"] = 0] = "Trace"; LogLevel[LogLevel["Debug"] = 1] = "Debug"; LogLevel[LogLevel["Info"] = 2] = "Info"; LogLevel[LogLevel["Warn"] = 3] = "Warn"; LogLevel[LogLevel["Error"] = 4] = "Error"; LogLevel[LogLevel["Fatal"] = 5] = "Fatal"; LogLevel[LogLevel["Off"] = 6] = "Off"; })(LogLevel || (LogLevel = {})); /* tslint:disable:no-namespace */ (function (LogLevel) { /** * Convert given value to LogLevel, if not matching returns undefined. * @param val Value to convert */ function toLogLevel(val) { switch (val.toLowerCase()) { case "trace": return LogLevel.Trace; case "debug": return LogLevel.Debug; case "info": return LogLevel.Info; case "warn": return LogLevel.Warn; case "error": return LogLevel.Error; case "fatal": return LogLevel.Fatal; case "off": return LogLevel.Off; default: return undefined; } } LogLevel.toLogLevel = toLogLevel; })(LogLevel || (LogLevel = {})); /* tslint:disable:enable-namespace */ /** * Standard logger implementation that provides the basis for all loggers. */ class CoreLoggerImpl { constructor(runtime) { this._runtime = runtime; } get id() { return this._runtime.id; } get logLevel() { return this._runtime.level; } get runtimeSettings() { /* Return it as new literal, we don't want people to play with our internal state */ return Object.assign({}, this._runtime); } set runtimeSettings(runtime) { this._runtime = runtime; } trace(message, ...args) { this.logMessage(LogLevel.Trace, message, args); } debug(message, ...args) { this.logMessage(LogLevel.Debug, message, args); } info(message, ...args) { this.logMessage(LogLevel.Info, message, args); } warn(message, ...args) { this.logMessage(LogLevel.Warn, message, args); } error(message, ...args) { this.logMessage(LogLevel.Error, message, args); } fatal(message, ...args) { this.logMessage(LogLevel.Fatal, message, args); } logMessage(level, logMessageType, args) { if (this._runtime.level > level) { return; } const nowMillis = Date.now(); const message = typeof logMessageType === "string" ? logMessageType : logMessageType(); const errorAndArgs = CoreLoggerImpl.getErrorAndArgs(args); /* * Deal with raw message here. */ switch (this._runtime.channel.type) { case "RawLogChannel": this._runtime.channel.write({ message, exception: errorAndArgs.error, args: errorAndArgs.args, timeInMillis: nowMillis, level, logNames: this._runtime.name, }, this._runtime.argumentFormatter); return; case "LogChannel": this._runtime.channel.write(this.createLogMessage(message, level, errorAndArgs, nowMillis)); break; } } formatArgValue(value) { try { return this._runtime.argumentFormatter(value); } catch (e) { // We don't really care what failed, except that the convert function failed. return `>>ARG CONVERT FAILED: '${value !== undefined ? value.toString() : "undefined"}'<<`; } } createLogMessage(message, level, errorAndArgs, nowMillis) { let errorResult; const error = errorAndArgs.error; const args = errorAndArgs.args; if (error) { errorResult = `${error.name}: ${error.message}`; if (error.stack) { errorResult += `@\n${error.stack}`; } } /* * We need to add the date, and log names (in front of the now formatted message). * Finally we also need to format any additional arguments and append after the message. */ const dateFormatted = this._runtime.dateFormatter(nowMillis); let levelAsStr = LogLevel[level].toUpperCase(); if (levelAsStr.length < 5) { levelAsStr += " "; } const names = typeof this._runtime.name === "string" ? this._runtime.name : this._runtime.name.join(", "); const argsFormatted = typeof args !== "undefined" && args.length > 0 ? (" [" + (args.map(arg => this.formatArgValue(arg))).join(", ") + "]") : ""; const completedMessage = dateFormatted + " " + levelAsStr + " [" + names + "] " + message + argsFormatted; return { message: completedMessage, error: errorResult, }; } static getErrorAndArgs(args) { /* The args are optional, but the first entry may be an Error or a function to an Error, or finally be a function to extra arguments. The last is only true, if the length of args === 1, otherwise we expect args starting at pos 1 and further to be just that - args. */ if (args.length === 0) { return {}; } let error; let actualArgs; const value0 = args[0]; /* If the first argument is an Error, we can stop straight away, the rest are additional arguments then if any */ if (value0 instanceof Error) { error = value0; actualArgs = args.length > 1 ? args.slice(1) : undefined; return { error, args: actualArgs }; } /* If the first argument is a function, it means either it will return the Error, or if the array length === 1 a function, returning the arguments */ if (typeof value0 === "function") { const errorOrArgs = value0(); if (errorOrArgs instanceof Error) { error = errorOrArgs; actualArgs = args.length > 1 ? args.slice(1) : undefined; return { error, args: actualArgs }; } if (args.length === 1) { /* The first argument was a function, we assume it returned the extra argument(s) */ if (Array.isArray(errorOrArgs)) { return { args: errorOrArgs.length > 0 ? errorOrArgs : undefined }; } else { /* No idea what was returned we just assume a single value */ return { args: errorOrArgs }; } } else { /* This is a weird situation but there's no way to avoid it, the first argument was a function but did not return an Error and the args are > 1, so just add the args returned, as well as any remaining. */ if (Array.isArray(errorOrArgs)) { return { args: [...errorOrArgs, ...args.slice(1)] }; } return { args: [errorOrArgs, ...args.slice(1)] }; } } /* All args are ordinary arguments, or at least the first arg was not an Error or a Function, so we add all as args */ return { args }; } } /** * Pad given value with given fillChar from the beginning (default is an empty space) * @param value Value to pad * @param length The length the string must be * @param fillChar The padding char (1 char length allowed only) * @return Padded string or the same string if it is already of given length (or larger). */ function padStart(value, length, fillChar = " ") { return padInternal(value, length, "start", fillChar); } /** * Pad given value with given fillChar from the end (default is an empty space) * @param value Value to pad * @param length The length the string must be * @param fillChar The padding char (1 char length allowed only) * @return Padded string or the same string if it is already of given length (or larger). */ function padEnd(value, length, fillChar = " ") { return padInternal(value, length, "end", fillChar); } /** * Returns the max length of a string value in given array * @param arr Array to check * @return Max length, 0 if array is empty */ function maxLengthStringValueInArray(arr) { return arr .map(v => v.length) .reduce((previous, current) => { if (current > previous) { return current; } return previous; }, 0); } function padInternal(value, length, padType, fillChar = " ") { if (length <= value.length) { return value; } if (fillChar.length > 1) { throw new Error(`Fill char must be one char exactly, it is: ${fillChar.length}`); } const charsNeeded = length - value.length; let padding = ""; for (let i = 0; i < charsNeeded; i++) { padding += fillChar; } if (padType === "start") { return padding + value; } return value + padding; } /** * Default argument formatter function, used by the library, see {@link ArgumentFormatterType}. * Can be used by an end user as well if needed. * @param arg The argument to format * @returns argument stringified to string (JSON.stringify), if arg is undefined returns "undefined" (without quotes). */ function formatArgument(arg) { if (arg === undefined) { return "undefined"; } return JSON.stringify(arg); } /** * Default date formatter function, used by the library, see {@link DateFormatterType}. * Can be used by an end user as well if needed. * @param millisSinceEpoch Milliseconds since epoch * @returns The date in format: yyyy-MM-dd HH:mm:ss,SSS (example: 2021-02-26 09:06:28,123) */ function formatDate(millisSinceEpoch) { const date = new Date(millisSinceEpoch); const year = date.getFullYear(); const month = padStart((date.getMonth() + 1).toString(), 2, "0"); const day = padStart(date.getDate().toString(), 2, "0"); const hours = padStart(date.getHours().toString(), 2, "0"); const minutes = padStart(date.getMinutes().toString(), 2, "0"); const seconds = padStart(date.getSeconds().toString(), 2, "0"); const millis = padStart(date.getMilliseconds().toString(), 3, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds},${millis}`; } /* tslint:disable:no-console */ /** * Default standard LogChannel which logs to console. */ class ConsoleLogChannel { constructor() { this.type = "LogChannel"; } write(msg) { if (console && console.log) { console.log(msg.message + (msg.error ? `\n${msg.error}` : "")); } } } /* tslint:disable:no-namespace */ /** * Provides access to various default channels provided by typescript logging. */ var DefaultChannels; (function (DefaultChannels) { /** * Create a new standard LogChannel that logs to the console. */ function createConsoleChannel() { return new ConsoleLogChannel(); } DefaultChannels.createConsoleChannel = createConsoleChannel; })(DefaultChannels || (DefaultChannels = {})); /** * Implementation for {@link LogProvider} */ class LogProviderImpl { constructor(name, settings) { this._log = getInternalLogger("core.impl.LogProviderImpl"); this._name = name; this._settings = settings; this._loggers = new EnhancedMap(); this._idToKeyMap = new EnhancedMap(); this._globalRuntimeSettings = { level: settings.level, channel: settings.channel }; this._nextLoggerId = 1; this._log.trace(() => `Created LogProviderImpl with settings: ${JSON.stringify(this._settings)}`); } get runtimeSettings() { return Object.assign(Object.assign({}, this._settings), { level: this._globalRuntimeSettings.level, channel: this._globalRuntimeSettings.channel }); } getLogger(name) { return this.getOrCreateLogger(name); } updateLoggerRuntime(log, settings) { this._log.debug(() => `Updating logger ${log.id} runtime settings using: '${JSON.stringify(settings)}'`); const key = this._idToKeyMap.get(log.id); if (key === undefined) { this._log.warn(() => `Cannot update logger with id: ${log.id}, it was not found.`); return false; } this._loggers.computeIfPresent(key, (currentKey, currentValue) => { currentValue.runtimeSettings = LogProviderImpl.mergeRuntimeSettingsIntoLogRuntime(currentValue.runtimeSettings, settings); return currentValue; }); return true; } updateRuntimeSettings(settings) { this._log.debug(() => `Updating global runtime settings and updating existing loggers runtime settings using: '${JSON.stringify(settings)}'`); this._globalRuntimeSettings = { /* * Undefined check is necessary, as level is a number (and LogLevel.Trace = 0), a ternary check otherwise results in the annoying "truthy/falsy" * behavior of javascript where 0 is seen as false. */ level: settings.level !== undefined ? settings.level : this._globalRuntimeSettings.level, channel: settings.channel !== undefined ? settings.channel : this._globalRuntimeSettings.channel, }; this._loggers.forEach(logger => logger.runtimeSettings = LogProviderImpl.mergeRuntimeSettingsIntoLogRuntime(logger.runtimeSettings, settings)); } /** * Removes all state and loggers, it reverts back to as it was after initial construction. */ clear() { this._loggers.clear(); this._idToKeyMap.clear(); this._globalRuntimeSettings = Object.assign({}, this._settings); this._nextLoggerId = 1; } getOrCreateLogger(name) { const key = LogProviderImpl.createKey(name); const logger = this._loggers.computeIfAbsent(key, () => { const runtime = { level: this._globalRuntimeSettings.level, channel: this._globalRuntimeSettings.channel, id: this.nextLoggerId(), name, argumentFormatter: this._settings.argumentFormatter, dateFormatter: this._settings.dateFormatter, }; return new CoreLoggerImpl(runtime); }); this._idToKeyMap.computeIfAbsent(logger.id, () => key); return logger; } nextLoggerId() { const result = this._name + "_" + this._nextLoggerId; this._nextLoggerId++; return result; } static mergeRuntimeSettingsIntoLogRuntime(currentSettings, settings) { return Object.assign(Object.assign({}, currentSettings), { /* * Undefined check is necessary, as level is a number (and LogLevel.Trace = 0), a ternary check otherwise results in the annoying "truthy/falsy" * behavior of javascript where 0 is seen as false. */ level: settings.level !== undefined ? settings.level : currentSettings.level, channel: settings.channel !== undefined ? settings.channel : currentSettings.channel }); } static createKey(name) { if (typeof name === "string") { return name; } return name.join(","); } } /** * Create a new LogProvider, this is for flavor usage only. End users should not * use this and instead use whatever the flavor offers to build some config and * get loggers from there. */ function createLogProvider(name, settings) { return new LogProviderImpl(name, settings); } var index = /*#__PURE__*/Object.freeze({ __proto__: null, EnhancedMap: EnhancedMap, maxLengthStringValueInArray: maxLengthStringValueInArray, padEnd: padEnd, padStart: padStart }); /** * LogChannel that pushes log messages to a buffer. */ class ArrayLogChannel { constructor() { this._buffer = []; this.type = "LogChannel"; } write(msg) { this._buffer.push(msg); } get logMessages() { return this._buffer; } get messages() { return this._buffer.map(msg => msg.message); } } /** * RawLogChannel that pushes raw log messages to a buffer. */ class ArrayRawLogChannel { constructor() { this._buffer = []; this.type = "RawLogChannel"; } write(msg, _) { this._buffer.push(msg); } get messages() { return this._buffer.map(m => m.message); } get errors() { return this._buffer.map(m => m.exception); } get size() { return this._buffer.length; } get rawMessages() { return this._buffer; } clear() { this._buffer = []; } } /** * Test class to help test the log control. */ class TestControlMessage { constructor() { this._messages = []; this.write = this.write.bind(this); } get messages() { return this._messages; } write(msg) { this._messages.push(msg); } clear() { this._messages = []; } } var TestClasses = /*#__PURE__*/Object.freeze({ __proto__: null, ArrayLogChannel: ArrayLogChannel, ArrayRawLogChannel: ArrayRawLogChannel, TestControlMessage: TestControlMessage }); export { InternalLogger as $internal, TestClasses as $test, DefaultChannels, LogLevel, createLogProvider, formatArgument, formatDate, index as util }; //# sourceMappingURL=typescript-logging.esm.js.map