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
JavaScript
/**
* 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