locustjs-logging
Version:
This library provides various types of loggers that could be employed in any type of javascript application.
1,121 lines (1,021 loc) • 27.3 kB
JavaScript
import {
isEmpty,
isObject,
isString,
isSomeString,
isFunction,
isjQueryElement,
isNull,
isNumeric,
isAnObject,
isSomeArray,
isNumber,
} from "locustjs-base";
import {
LocalStorageJson,
SessionStorageJson,
StorageBase,
} from "locustjs-storage";
import {
Exception,
throwIfNotInstanceOf,
throwIfInstantiateAbstract,
throwNotImplementedException,
throwNotSupportedException,
} from "locustjs-exception";
import Enum from "locustjs-enum";
import { htmlEncode } from "htmlencode";
const LogType = Enum.define(
{
info: 0,
log: 1,
debug: 2,
trace: 3,
warn: 4,
danger: 5,
success: 6,
fail: 7,
abort: 8,
suggest: 9,
cancel: 10,
},
"LogType"
);
const ConsoleColors = {
BackColor: {
Black: 40,
Red: 41,
Green: 42,
Yellow: 43,
Blue: 44,
Magenta: 45,
Cyan: 46,
White: 47,
Gray: 100,
BrightRed: 101,
BrightGreen: 102,
BrightYellow: 103,
BrightBlue: 104,
BrightMagenta: 105,
BrightCyan: 106,
BrightWhite: 107,
},
ForeColor: {
Black: 30,
Red: 31,
Green: 32,
Yellow: 33,
Blue: 34,
Magenta: 35,
Cyan: 36,
White: 37,
Gray: 90,
BrightRed: 91,
BrightGreen: 92,
BrightYellow: 93,
BrightBlue: 94,
BrightMagenta: 95,
BrightCyan: 96,
BrightWhite: 97,
},
Modifier: {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
},
};
class InvalidLoggerTypeException extends Exception {}
class NoLoggerFactoryException extends Exception {}
class InvalidLoggerException extends Exception {}
class Log {
constructor(type, host, scope, exception, ...data) {
this.date = new Date();
this.type = LogType.getString(type, LogType.log);
this.scope = isString(scope) ? scope : undefined;
this.data = data.length > 1 ? data : data.length > 0 ? data[0] : undefined;
this.host = isString(host) ? host : undefined;
this.exception = isEmpty(exception) ? undefined : new Exception(exception);
this.batch = data.length > 1;
}
}
function formatDate(d, type) {
let result;
if (type == "utc") {
result =
d.getUTCFullYear() +
"/" +
("0" + d.getUTCDate()).slice(-2) +
"/" +
("0" + (d.getUTCMonth() + 1)).slice(-2) +
" " +
("0" + d.getUTCHours()).slice(-2) +
":" +
("0" + d.getUTCMinutes()).slice(-2) +
":" +
("0" + d.getUTCSeconds()).slice(-2) +
"." +
d.getUTCMilliseconds();
} else {
result =
d.getFullYear() +
"/" +
("0" + d.getDate()).slice(-2) +
"/" +
("0" + (d.getMonth() + 1)).slice(-2) +
" " +
("0" + d.getHours()).slice(-2) +
":" +
("0" + d.getMinutes()).slice(-2) +
":" +
("0" + d.getSeconds()).slice(-2) +
"." +
d.getMilliseconds();
}
return result;
}
function merge(obj, keyValueSeparator, itemSeparator) {
return obj
? Object.keys(obj).reduce(
(prev, curr) =>
(prev ? prev + itemSeparator : "") +
curr +
keyValueSeparator +
obj[curr],
""
)
: "";
}
function colorize(text, color, reset = true) {
let result = "";
if (color) {
if (color.bc) {
result += `\x1b[${color.bc}m`;
}
if (color.fc) {
result += `\x1b[${color.fc}m`;
}
if (reset) {
result += text + ConsoleColors.Modifier.Reset;
}
} else {
result = text || "";
}
return result;
}
/*
options: {
host: string, // host
}
*/
class LoggerBase {
constructor(options) {
throwIfInstantiateAbstract(LoggerBase, this);
this.options = Object.assign({}, options);
this._scopes = [];
this.scope = '';
}
getScope() {
return this.scope;
}
enterScope(value) {
this._scopes.push(this.scope);
this.scope = isFunction(value) ? value.name : isString(value) ? value: '';
}
exitScope() {
this.scope = this._scopes.pop();
}
get host() {
return this.options.host;
}
set host(value) {
this.options.host = value;
}
/*
overloads:
_createLog(type, ...data)
_createLog(type, Exception, ...data) // locustjs Exception
_createLog(type, Error, ...data) // javascript error
*/
_createLog(type, ...data) {
let result;
const arg0 = data.length > 0 ? data[0] : undefined;
if (
isAnObject(arg0) &&
(arg0 instanceof Exception || arg0 instanceof Error)
) {
result = new Log(
type,
this.options.host,
this.scope,
arg0,
...data.slice(1)
);
} else {
result = new Log(type, this.options.host, this.scope, undefined, ...data);
}
return result;
}
_logInternal(log) {
throwNotImplementedException("_logInternal", this.host);
}
clear() {
throwNotImplementedException("clear", this.host);
}
getAll() {
throwNotImplementedException("getAll", this.host);
}
/*
// *********** UNDER DEVELOPMENT ***************
// filters: fromIndex, toIndex, fromDate, toDate, types, host, categories
// orderBy:
// orderDir
// top
// page:
// pageSize:
getRange(rangeOptions) {
throwNotSpportedException("getRange", this.host);
}
// *********** UNDER DEVELOPMENT ***************
*/
_log(type, ...args) {
if (args && args.length) {
const log = this._createLog(type, ...args);
return this._logInternal(log);
}
}
log(...args) {
return this._log(LogType.log, ...args);
}
debug(...args) {
return this._log(LogType.debug, ...args);
}
warn(...args) {
return this._log(LogType.warn, ...args);
}
danger(...args) {
return this._log(LogType.danger, ...args);
}
info(...args) {
return this._log(LogType.info, ...args);
}
success(...args) {
return this._log(LogType.success, ...args);
}
fail(...args) {
return this._log(LogType.fail, ...args);
}
abort(...args) {
return this._log(LogType.abort, ...args);
}
suggest(...args) {
return this._log(LogType.suggest, ...args);
}
trace(...args) {
return this._log(LogType.trace, ...args);
}
cancel(...args) {
return this._log(LogType.cancel, ...args);
}
}
/*
options: {
next: LoggerBase, // next logger
filter: array | string, // list of log types to be logged; default = '*' i.e. all lgo types
unattended: bool, // whether or not this logger acts in an unattended behavior,
// i.e. it actually logs, but pretends it does not, thus, logging is passed
// to the next logger in the chain; default = undefiend = false
}
*/
class ChainLogger extends LoggerBase {
constructor(options) {
super(options);
throwIfInstantiateAbstract(ChainLogger, this);
if (isNull(this.options.next)) {
this.options.next = new NullLogger();
}
throwIfNotInstanceOf(
"options.next",
LoggerBase,
this.options.next,
this.host
);
this.options.filter = "*";
this.options.scopeFilter = "";
this.options.unattended = false;
}
get next() {
return this.options.next;
}
canLog(log) {
let filter = isSomeArray(this.options.filter)
? this.options.filter
: isSomeString(this.options.filter)
? this.options.filter.split(",")
: ["*"];
return (
filter.findIndex((x) => {
const type = isNumber(x)
? LogType.getString(x)
: (x || "").toString().trim().toLowerCase();
return type == "*" || type == "all" || type == log.type;
}) >= 0 && (isEmpty(this.options.scopeFilter) || this.options.scopeFilter == log.scope)
);
}
__logInternal(log) {
throwNotImplementedException("__logInternal", this.host);
}
_logInternal(log) {
let result = this.__logInternal(log);
if (result === undefined) {
result =
this.options.unattended == false ||
this.options.unattended == undefined;
}
return result;
}
_log(type, ...args) {
try {
if (args && args.length) {
const log = this._createLog(type, ...args);
let logged = false;
if (this.canLog(log)) {
logged = this._logInternal(log);
}
if (!(logged == null || logged == true)) {
if (this.next instanceof LoggerBase) {
this.next.log[type](...args);
}
}
}
} catch (e) {
if (this.next && this.next instanceof LoggerBase) {
const method = LogType.getString(type);
this.next[method](e);
this.next[method](...args);
} else {
throw e;
}
}
}
_clearInternal() { }
clear() {
this._clearInternal();
current = this.options.next;
while (current) {
current.clear();
current = current.next;
}
}
}
class ArrayLogger extends ChainLogger {
constructor(options) {
super(options);
this._logs = [];
}
__logInternal(log) {
this._logs.push(log);
}
getAll() {
return this._logs;
}
_clearInternal() {
this._logs = [];
}
}
/*
options: {
styles: object, // color customization object
dateType: 'utc' | 'current' (default)
env: 'node' | 'web' (default)
}
*/
class ConsoleLogger extends ChainLogger {
constructor(options) {
super(options);
if (!isObject(this.options.styles)) {
this.options.styles = {};
}
if (this.options.env == "node") {
this._initNodeColoring();
} else {
this._initWebColoring();
}
}
_initNodeColoring() {
const scope = {
fc: ConsoleColors.ForeColor.Blue,
bc: ConsoleColors.BackColor.Black,
};
const host = {
fc: ConsoleColors.ForeColor.Black,
bc: ConsoleColors.BackColor.BrightCyan,
};
const date = {
fc: ConsoleColors.ForeColor.BrightYellow,
bc: ConsoleColors.BackColor.Black,
};
this._initStyle(
"log",
this._labelNode(ConsoleColors.ForeColor.White),
date,
scope,
host
);
this._initStyle(
"info",
this._labelNode(ConsoleColors.ForeColor.BrightBlue),
date,
scope,
host
);
this._initStyle(
"debug",
this._labelNode(ConsoleColors.ForeColor.Magenta),
date,
scope,
host
);
this._initStyle(
"trace",
this._labelNode(
ConsoleColors.ForeColor.Black,
ConsoleColors.BackColor.Cyan
),
date,
scope,
host
);
this._initStyle(
"warn",
this._labelNode(
ConsoleColors.ForeColor.White,
ConsoleColors.BackColor.Magenta
),
date,
scope,
host
);
this._initStyle(
"danger",
this._labelNode(ConsoleColors.ForeColor.BrightRed),
date,
scope,
host
);
this._initStyle(
"success",
this._labelNode(ConsoleColors.ForeColor.Green),
date,
scope,
host
);
this._initStyle(
"fail",
this._labelNode(ConsoleColors.ForeColor.Red),
date,
scope,
host
);
this._initStyle(
"abort",
this._labelNode(
ConsoleColors.ForeColor.Black,
ConsoleColors.BackColor.White
),
date,
scope,
host
);
this._initStyle(
"suggest",
this._labelNode(ConsoleColors.ForeColor.BrightMagenta),
date,
scope,
host
);
this._initStyle(
"cancel",
this._labelNode(ConsoleColors.ForeColor.Yellow),
date,
scope,
host
);
}
_initWebColoring() {
const scope = {
color: "darkblue",
"font-weight": "bold",
};
const host = {
color: "black",
"background-color": "lightcyan",
};
const date = {
color: "blue",
};
this._initStyle("info", this._labelWeb("blue"), date, scope, host);
this._initStyle("debug", this._labelWeb("darkmagenta"), date, scope, host);
this._initStyle(
"trace",
this._labelWeb("cyan", "black"),
date,
scope,
host
);
this._initStyle(
"warn",
this._labelWeb("magenta"),
date,
scope,
host
);
this._initStyle("danger", this._labelWeb("red"), date, scope, host);
this._initStyle("success", this._labelWeb("green"), date, scope, host);
this._initStyle("fail", this._labelWeb("darkred"), date, scope, host);
this._initStyle("abort", this._labelWeb("black"), date, scope, host);
this._initStyle("suggest", this._labelWeb("purple"), date, scope, host);
this._initStyle("cancel", this._labelWeb("darkorange"), date, scope, host);
}
_labelWeb(bg, fc = "white") {
return { color: fc, "background-color": bg };
}
_labelNode(fc, bc = ConsoleColors.BackColor.Black) {
return { fc, bc };
}
_initStyle(type, label, date, scope, host) {
if (!isObject(this.options.styles[type])) {
this.options.styles[type] = {};
}
if (!isObject(this.options.styles[type].label)) {
this.options.styles[type].label = {};
}
if (!isObject(this.options.styles[type].date)) {
this.options.styles[type].date = {};
}
if (!isObject(this.options.styles[type].scope)) {
this.options.styles[type].scope = {};
}
if (!isObject(this.options.styles[type].host)) {
this.options.styles[type].host = {};
}
this._assignStyle(this.options.styles[type].label, label);
this._assignStyle(this.options.styles[type].scope, scope);
this._assignStyle(this.options.styles[type].host, host);
this._assignStyle(this.options.styles[type].date, date);
}
_assignStyle(obj, style) {
Object.keys(style).forEach((key) => {
if (!isString(obj[key])) {
obj[key] = style[key];
}
});
}
__logInternal(log) {
if (this.options.env == "node") {
this._logNode(log);
} else {
this._logWeb(log);
}
}
_logNode(log) {
const style = this.options.styles[log.type] || {};
let prefix = `${colorize(
formatDate(log.date, this.options.dateType),
style.date
)} ${colorize(` ${log.type.toUpperCase()} `, style.label)}`;
if (isSomeString(log.host)) {
prefix += ` ${colorize(` ${log.host} `, style.host)} `;
}
if (isSomeString(log.scope)) {
prefix += ` ${colorize(log.scope, style.scope)} `;
}
const args = [prefix];
if (log.batch) {
args.splice(args.length, 0, ...log.data);
} else {
args.push(log.data);
}
if (log.exception) {
args.push(log.exception);
}
const type = LogType.getNumber(log.type);
let method = "";
switch (type) {
case LogType.debug:
method = "debug";
break;
case LogType.danger:
method = "error";
break;
case LogType.trace:
method = "trace";
break;
case LogType.warn:
method = "warn";
break;
case LogType.info:
method = "info";
break;
default:
method = "log";
break;
}
console[method](...args);
}
_logWeb(log) {
let prefix = `%c${formatDate(log.date, this.options.dateType)} %c ${log.type.toUpperCase()} `;
const style = this.options.styles[log.type] || {};
const colors = [merge(style.date, ":", ";"), merge(style.label, ":", ";")];
if (isSomeString(log.host)) {
prefix += `%c ${log.host} `;
colors.push(merge(style.host, ":", ";"));
}
if (isSomeString(log.scope)) {
prefix += `%c ${log.scope} `;
colors.push(merge(style.scope, ":", ";"));
}
const args = [prefix, ...colors];
if (log.batch) {
args.splice(args.length, 0, ...log.data);
} else {
args.push(log.data);
}
if (log.exception) {
args.push(log.exception);
}
const type = LogType.getNumber(log.type);
let method = "";
switch (type) {
case LogType.debug:
method = "debug";
break;
case LogType.danger:
method = "error";
break;
case LogType.trace:
method = "trace";
break;
case LogType.warn:
method = "warn";
break;
case LogType.info:
method = "info";
break;
default:
method = "log";
break;
}
console[method](...args);
}
getAll() {
throwNotSupportedException("getAll", this.host);
}
_clearInternal() {
console.clear();
}
}
/*
options: {
storeKey: string // storage item key where logs will be stored at. default = 'logs'
store:
bufferSize: number // number of logs to be buffered before flushing into store. default = 5
}
*/
class StorageLogger extends ArrayLogger {
constructor(options) {
super(options);
throwIfInstantiateAbstract(StorageLogger, this);
if (!isSomeString(this.options.storeKey)) {
this.options.storeKey = "logs";
}
if (isEmpty(this.options.store)) {
throwIfNotInstanceOf(
"options.store",
StorageBase,
this.options.store,
this.host
);
}
if (!isNumeric(this.options.bufferSize)) {
this.options.bufferSize = 5;
}
this._new_log_count = 0;
}
__logInternal(log) {
super.__logInternal(log);
if (this.options.bufferSize > 0) {
this._new_log_count++;
if (this._new_log_count >= this.options.bufferSize) {
this.options.store.setItem(this.options.storeKey, this.getAll());
this._new_log_count = 0;
}
} else {
this.options.store.setItem(this.options.storeKey, this.getAll());
}
}
_clearInternal() {
super._clearInternal();
this._new_log_count = 0;
this.options.store.setItem(this.options.storeKey, []);
}
}
class LocalStorageLogger extends StorageLogger {
constructor(options) {
super(Object.assign(options, { store: new LocalStorageJson() }));
}
}
class SessionStorageLogger extends StorageLogger {
constructor(options) {
super(Object.assign(options, { store: new SessionStorageJson() }));
}
}
/*
options: {
target: string (css selector) | object // a DOM node or css selector of a DOM node where logs should be appended to.
// default = #dom-logger
onInit: func // custom log table creator
onNewLog: func // custom log table record
className: string // css className of target node. default = dom-logger
format: func // custom format
}
*/
class DOMLogger extends ChainLogger {
constructor(options) {
super(options);
this.target = this.options.target || "#dom-logger";
this._init();
}
get target() {
return this._target;
}
set target(value) {
if (isjQueryElement(value)) {
this._target = value.length ? value[0] : null;
} else if (isString(value)) {
this._target = document.querySelector(value);
} else {
this._target =
isObject(value) && isFunction(value.querySelector) ? value : null;
}
}
_init(recreate = false) {
this._count = 0;
if (this.target && (this.target.querySelectorAll('tbody tr').length == 0 || recreate)) {
if (isFunction(this.options.onInit)) {
const result = this.options.onInit(this);
if (isSomeString(result)) {
this.target.innerHTML = result;
} else if (isObject(result) && isFunction(result.querySelector)) {
this.target.appendChild(result);
}
} else {
const className = this.options.className || "dom-logger";
this.target.innerHTML = `
<table class="${className}">
<thead>
<tr>
<th>Row</th>
<th>Type</th>
<th>Date</th>
<th>Scope/Host</th>
<th>Data/Exception</th>
</tr>
</thead>
<tbody>
</tbody>
</table>`;
}
if (isFunction(this.options.onRowClick)) {
document
.querySelectorAll(`${className} table tbody tr`)
.forEach((tr) => {
tr.onclick = this.options.onRowClick;
});
}
}
}
__logInternal(log) {
if (this.target) {
let tr;
if (isFunction(this.options.onNewLog)) {
tr = this.options.onNewLog(this, log, this._count);
} else {
const body = this.target.querySelector("tbody");
if (body) {
tr = document.createElement("TR");
tr.setAttribute('class', log.type);
const tdRow = document.createElement("TD");
const tdType = document.createElement("TD");
const tdDate = document.createElement("TD");
const tdScopeHost = document.createElement("TD");
const tdDataException = document.createElement("TD");
tdRow.innerHTML = `<div>${this._count + 1}</div>`;
tdType.innerHTML = `<div>${log.type}</div>`;
tdDate.innerHTML = `<div>${formatDate(log.date)}</div>`;
tdScopeHost.innerHTML = `${
log.scope ? `<span class="scope">${log.scope}</span>` : ""
}${log.host ? `<span class="host">${log.host}</span>` : ""}`;
let data;
let exception;
try {
if (isFunction(this.options.format)) {
data = this.options.format(this, log, 'data');
exception = this.options.format(this, log, 'exception');
} else {
data = log.data ? JSON.stringify(log.data, null, 2): '';
exception = log.exception ? JSON.stringify(log.exception, null, 2): '';
}
} catch (err) {
this.danger(err);
}
const dataHtml = htmlEncode(data);
const exceptionHtml = htmlEncode(exception);
tdDataException.innerHTML = `${
data ? `<div class="data" title="${dataHtml.replace(/"/g, '"')}">${dataHtml}</div>` : ""
}${
exception ? `<div class="exception" title="${exceptionHtml.replace(/"/g, '"')}">${exceptionHtml}</div>` : ""
}`;
tr.appendChild(tdRow);
tr.appendChild(tdType);
tr.appendChild(tdDate);
tr.appendChild(tdScopeHost);
tr.appendChild(tdDataException);
}
if (isObject(tr) && isFunction(tr.querySelector)) {
body.appendChild(tr);
}
}
this._count++;
}
}
_clearInternal() {
this._init(true);
}
}
class NullLogger extends LoggerBase {
_logInternal() {}
clear() {}
getAll() {
return [];
}
}
/*
options: {
dom: object // custom options for DOMLogger
store: object // custom options for store logger types
console: object // custom options for console logger
factory: func // custom factory for creating loggers
next: LoggerBase // default next logger
}
*/
class DynamicLogger extends LoggerBase {
constructor(options) {
super(
Object.assign(
{
dom: {},
store: {},
array: {},
factory: null,
console: {},
},
options
)
);
this.type = "null";
}
getScope() {
return this._instance.getScope();
}
enterScope(value) {
this._instance.enterScope(value);
}
exitScope() {
this._instance.exitScope();
}
_createLogger(factory, type, fallback) {
let result;
try {
if (isFunction(factory)) {
result = factory(this, type);
}
if (result == null) {
if (isFunction(fallback)) {
result = fallback();
} else {
throw new NoLoggerFactoryException();
}
}
if (result == null) {
throw new InvalidLoggerTypeException();
} else {
if (!(result instanceof LoggerBase)) {
throw new InvalidLoggerException();
}
}
} catch (e) {
this.enterScope("DynamicLogger._createLogger");
this.danger(e);
this.exitScope();
}
return result;
}
get type() {
return this._type;
}
set type(value) {
let logger;
let fallback;
value = (value || "").toString().trim().toLowerCase();
switch (value) {
case "console":
fallback = () =>
new ConsoleLogger({
host: this.options.host,
next: this.options.next,
...this.options.console,
});
break;
case "dom":
fallback = () =>
new DOMLogger({
host: this.options.host,
next: this.options.next,
...this.options.dom,
});
break;
case "":
case "null":
fallback = () => new NullLogger();
break;
case "array":
fallback = () => new ArrayLogger({
host: this.options.host,
next: this.options.next,
...this.options.array,
});
break;
case "localstorage":
fallback = () =>
new LocalStorageLogger({
host: this.options.host,
next: this.options.next,
...this.options.store,
});
break;
case "sessionstorage":
fallback = () =>
new SessionStorageLogger({
host: this.options.host,
next: this.options.next,
...this.options.store,
});
break;
default:
fallback = () => null;
break;
}
logger = this._createLogger(this.options.factory, value, fallback);
if (logger) {
this._instance = logger;
this._type = value;
}
}
_log(type, ...args) {
const _type = LogType.getString(type);
if (isFunction(this._instance[_type])) {
this._instance[_type](...args);
}
}
clear() {
this._instance.clear();
}
getAll() {
return this._instance.getAll();
}
}
export {
Log,
LogType,
ConsoleColors,
formatDate,
merge,
colorize,
LoggerBase,
ChainLogger,
ArrayLogger,
ConsoleLogger,
DOMLogger,
NullLogger,
StorageLogger,
LocalStorageLogger,
SessionStorageLogger,
DynamicLogger,
NoLoggerFactoryException,
InvalidLoggerTypeException,
InvalidLoggerException,
};