/**
* @author Michael Hasenstein <hasenstein@yahoo.com>
* @copyright REFINIO GmbH 2017
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 0.0.1
*/
/**
* This module attaches to the global MessageBus and after start attaches to all events of the types
* "debug", "log", "alert" and "error". It uses regular `console.log()` statements but adds
* colorization for each message type. It was made for nodejs and browser console environments.
*
* Defines a static namespace object. By using this object the destination of the log can easily
* be changed. It also supports severity levels, and for production COLOR lower level calls to
* the log function can be stripped from the source COLOR entirely by grepping for severity
* level keywords. Development may prefer logs to the console while production COLOR would log via
* XHR calls to a server.
*
* Example
*
* ```
* import {start as startLogger} from 'one.core/lib/logger';
* startLogger();
* ```
* @private
* @module
*/
/* eslint-disable no-console */
import {getInstanceName} from './instance';
import {createMessageBus} from './message-bus';
import {PLATFORMS} from './platforms';
import {platform} from './system/platform';
import type {AnyObject} from './util/object';
import type {ElementType} from './util/type-checks';
import {isObject, isString} from './util/type-checks-basic';
const MessageBus = createMessageBus('logger');
// eslint-disable-next-line no-var,@typescript-eslint/no-unused-vars
declare var WorkerGlobalScope: any;
// Just requiring the module starts the server at its default values so that it can run as a
// standalone script as well.
// const isBrowser =
// (typeof window !== 'undefined' && getObjTypeName(window) === 'Window') ||
// (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope);
const isBrowser = platform === PLATFORMS.BROWSER;
/**
* A selection of colors and text effects for console output.
*
* - browser: Terminal colors for the browser console
* See {@link https://stackoverflow.com/q/7505623/544779}
*
* - node.js: ANSI terminal codes to turn on certain font effects
* See {@link https://stackoverflow.com/q/4842424/544779}
* @global
* @type {object}
*/
export const COLOR = isBrowser
? ({
OFF: '',
BOLD_ON: 'font-weight: bold;',
BOLD_OFF: 'font-weight: normal;',
INVERSE_ON: '',
FG_BLACK: 'color: black;',
FG_RED: 'color: red;',
FG_GREEN: 'color: green;',
FG_YELLOW: 'color: yellow;',
FG_BLUE: 'color: blue;',
FG_MAGENTA: 'color: magenta;',
FG_CYAN: 'color: cyan;',
FG_WHITE: 'color: white;',
BG_BLACK: 'background-color: black;',
BG_RED: 'background-color: red;',
BG_GREEN: 'background-color: green;',
BG_YELLOW: 'background-color: yellow;',
BG_BLUE: 'background-color: blue;',
BG_MAGENTA: 'background-color: magenta;',
BG_CYAN: 'background-color: cyan;',
BG_WHITE: 'background-color: white;'
} as const)
: ({
OFF: '\x1b[0m',
BOLD_ON: '\x1b[1m',
BOLD_OFF: '\x1b[22m',
INVERSE_ON: '\x1b[7m',
FG_BLACK: '\x1b[30m',
FG_RED: '\x1b[31m',
FG_GREEN: '\x1b[32m',
FG_YELLOW: '\x1b[33m',
FG_BLUE: '\x1b[34m',
FG_MAGENTA: '\x1b[35m',
FG_CYAN: '\x1b[36m',
FG_WHITE: '\x1b[37m',
BG_BLACK: '',
BG_RED: '\x1b[41m',
BG_GREEN: '\x1b[42m',
BG_YELLOW: '\x1b[43m',
BG_BLUE: '\x1b[44m',
BG_MAGENTA: '\x1b[45m',
BG_CYAN: '\x1b[46m',
BG_WHITE: '\x1b[47m'
} as const);
// Assign ANSI effect codes to log levels
const LEVEL_COLOR = isBrowser
? ({
debug: COLOR.FG_YELLOW + COLOR.BG_BLACK,
log: COLOR.FG_GREEN,
alert: COLOR.BG_WHITE + COLOR.FG_BLUE,
error: COLOR.BOLD_ON + COLOR.BG_RED + COLOR.FG_BLACK
} as const)
: ({
debug: COLOR.FG_YELLOW,
log: COLOR.FG_GREEN,
alert: COLOR.BG_WHITE + COLOR.FG_BLUE,
error: COLOR.BOLD_ON + COLOR.BG_RED + COLOR.FG_BLACK
} as const);
const LOG_LEVELS = ['debug', 'log', 'alert', 'error'] as const;
type LOG_TYPES = ElementType<typeof LOG_LEVELS>;
/**
* @private
* @typedef {object} Logger
* @property {Function} debug - This function is used to write "debug" messages to the console.
* @property {Function} log - This function is used to write "log" messages to the console.
* @property {Function} alert - This function is used to write "alert" messages to the console.
* @property {Function} error - This function is used to write "error" messages to the console.
* @private
* @type {Logger}
*/
const Log = {
// About the type cast: Filled with its elements in the loop just below
} as Record<LOG_TYPES, (id: string, txt: string) => void>;
let printInstanceName = false;
let printTimestamo = false;
// Create logging functions with the id of the module that is going to use them
// already pre-filled, and ANSI color codes as well.
LOG_LEVELS.forEach(level => {
Log[level] = (id, ...messages) => {
const instanceName = getInstanceName() || 'n/a';
const name = printInstanceName ? '[' + instanceName + '] ' : '';
const date = new Date();
const ts = printTimestamo
? `${date.getDate()}.${
date.getMonth() + 1
}.${date.getFullYear()} @ ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()} `
: '';
isBrowser
? console.log(`${name}${ts}%c ${id} ${level} ${messages.join(' ')}`, LEVEL_COLOR[level])
: // Use "+" to add the ANSI codes because console.log turns commas into spaces
console.log(...[name + LEVEL_COLOR[level] + id, level + COLOR.OFF], ...messages);
};
});
/**
* @private
* @param {string} type
* @param {string} src
* @param {Array<*>} messages
* @returns {undefined}
*/
function log(type: LOG_TYPES, src: string, messages: readonly unknown[]): void {
const level = LOG_LEVELS.includes(type) ? type : 'debug';
Log[level](
src,
messages
.map(msg => {
if (isObject(msg)) {
// Use "duck typing" because "msg instanceof Error" would fail across execution
// contexts (frames, windows, node's vm.runInNewContext, etc.)
if (isString(msg.message) && isString(msg.stack)) {
// Make sure to include all guaranteed-to-be-there Error properties on all
// platforms, and go through a set to ensure we include them only once
return [
'name',
'message',
'stack',
...Reflect.ownKeys(msg).filter(isString)
]
.filter((item, index, arr) => arr.indexOf(item) === index)
.map(key => msg[key])
.join(' ');
}
// A circle-proof JSON.stringify. Details are lost - better than a crash.
// DETECTS ALL DUPLICATE REFERENCES - not just circles. Actually dealing with
// loops would take quite a bit more code, not worth it for this logging
// function.
const seenObjects: Set<AnyObject> = new Set();
return JSON.stringify(msg, (_key, value) => {
if (isObject(value)) {
if (seenObjects.has(value)) {
return '[~CIRCLE-OR-DUPLICATE]';
}
seenObjects.add(value);
}
return value;
});
}
return msg;
})
.join('\n')
);
}
// ------------------------------------------------------------------------------------
// LOG ANYTHING!
// Attach to _any_ event, send to the desired log-level function.
// ------------------------------------------------------------------------------------
/**
* This function attaches the log functions for message bus message types "debug", "log",
* "alert" and "error" to the message bus. The function take an option object parameter.
* @static
* @param {object} options
* @param {boolean} [options.includeInstanceName=false] - If set to true the console output is
* going to include the name of the instance
* @param {boolean} [options.includeTimestamp=false] - If set to true the console output is
* going to include the current date and time
* @param {boolean} [options.types=['error','alert','log','debug']] - Which message types to
* subscribe to for logging? If none are provided subscribe to the four default log message types.
* @returns {undefined} Returns nothing
*/
export function start({
includeInstanceName = false,
includeTimestamp = false,
types = LOG_LEVELS
}: {
includeInstanceName?: boolean;
includeTimestamp?: boolean;
types?: Readonly<LOG_TYPES[]>;
} = {}): void {
printInstanceName = includeInstanceName;
printTimestamo = includeTimestamp;
for (const type of types) {
MessageBus.on(type, (src, ...messages): void => log(type, src, messages));
}
MessageBus.send('log', 'Logger started.');
}