import { Observable, ObservableInterface } from "@hibas123/utils"; import { ConsoleAdapter } from "./consolewriter"; import inspect from "./inspect"; import { Adapter, LoggingTypes, Message, FormatConfig, DefaultFormatConfig, Format, FormatTypes, Colors, FormattedText, FormattedLine, } from "./types"; const browser = typeof window !== "undefined"; export function removeColors(text: string) { text = text.replace( /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "" ); // let index = text.indexOf("\x1b"); // while (index >= 0) { // text = text.substring(0, index) + text.substring(index + 5, text.length); // index = text.indexOf("\x1b"); // } return text; } export interface LoggingBaseOptions { /** * Name will be prefixed on Console output and added to logfiles, if not specified here */ name: string; /** * Prints output to console */ console: boolean; } const adapterCache = new WeakMap(); class AdapterSet { change = new Observable<{ type: "add" | "remove"; adapter: Adapter }>(); adapters: Set = new Set(); addAdapter(adapter: Adapter) { if (!this.adapters.has(adapter)) { this.adapters.add(adapter); this.change.send({ type: "add", adapter: adapter, }); } } } const consoleAdapter = new ConsoleAdapter(); declare var process: { cwd: () => string }; const PROJECT_ROOT = typeof process !== "undefined" ? process.cwd() : undefined; export class LoggingBase { private _formatMap: FormatConfig = new DefaultFormatConfig(); public set formatMap(value: FormatConfig) { this._formatMap = value; } private adapterSet: AdapterSet; private adapter_init: Promise[] = []; private timerMap = new Map(); private messageObservable = new Observable(); protected _name: string; private _logLevel = LoggingTypes.Debug; get logLevel() { return this._logLevel; } set logLevel(value: LoggingTypes) { this._logLevel = value; } get name() { return this._name; } constructor( options?: Partial | string, adapterSet?: AdapterSet ) { let opt: Partial; if (!options) opt = {}; else if (typeof options === "string") { opt = { name: options }; } else { opt = options; } let config: LoggingBaseOptions = { name: undefined, console: true, ...opt, }; if (config.name) this._name = config.name; for (let key in this) { if (typeof this[key] === "function") this[key] = (this[key]).bind(this); } if (adapterSet) { this.adapterSet = adapterSet; this.adapterSet.adapters.forEach((a) => this.initAdapter(a)); } else { this.adapterSet = new AdapterSet(); } this.adapterSet.change.subscribe((change) => { if (change.type === "add") { this.initAdapter(change.adapter); } }); if (config.console) { this.addAdapter(consoleAdapter); } //Binding function to this this.debug = this.debug.bind(this); this.log = this.log.bind(this); this.warn = this.warn.bind(this); this.warning = this.warning.bind(this); this.error = this.error.bind(this); this.errorMessage = this.errorMessage.bind(this); this.flush = this.flush.bind(this); } /** * Can be used to override function from super class * @param child New child logging instance */ protected postGetChild(child: LoggingBase) {} /** * Creates a new logging instance, with the adapters liked together. * @param name Name/Prefix of the new child. The actual name will resolve as "/" */ getChild(name: string) { let lg = new LoggingBase( { console: false, name: this.name ? this.name + "/" + name : name, }, this.adapterSet ); return lg; } private initAdapter(adapter: Adapter) { let cached = adapterCache.get(adapter) || 0; adapterCache.set(adapter, cached + 1); let prms = Promise.resolve( adapter.init(this.messageObservable.getPublicApi()) ); this.adapter_init.push(prms); } addAdapter(adapter: Adapter) { this.adapterSet.addAdapter(adapter); } flush(sync: true): void; flush(sync: false): Promise; flush(sync: boolean): void | Promise { if (sync) { this.adapterSet.adapters.forEach((elm) => elm.flush(true)); } else { let adapters: (void | Promise)[] = []; this.adapterSet.adapters.forEach((elm) => adapters.push(elm.flush(false)) ); return Promise.all(adapters).then(() => {}); } } private $closed = false; public close() { if (this.$closed) return; this.$closed = true; this.adapterSet.adapters.forEach((adapter) => { let cached = adapterCache.get(adapter); if (cached) { cached--; if (cached <= 0) { adapterCache.delete(adapter); adapter.close(); } else adapterCache.set(adapter, cached); } adapter.close ? adapter.close() : undefined; }); this.adapterSet = undefined; this.messageObservable.close(); } public waitForSetup() { return Promise.all(this.adapter_init); } debug(...message: any[]) { if (this._logLevel <= LoggingTypes.Debug) this.message(LoggingTypes.Debug, message); } log(...message: any[]) { if (this._logLevel <= LoggingTypes.Log) this.message(LoggingTypes.Log, message); } warning(...message: any[]) { if (this._logLevel <= LoggingTypes.Warning) this.message(LoggingTypes.Warning, message); } warn(...message: any[]) { if (this._logLevel <= LoggingTypes.Warning) this.message(LoggingTypes.Warning, message); } error(error: Error | string) { if (this._logLevel > LoggingTypes.Error) return; if (!error) error = "Empty ERROR was passed, so no informations available"; if (typeof error === "string") { let e = new Error("This is a fake error, to get a stack trace"); this.message(LoggingTypes.Error, [error, "\n", e.stack]); } else { this.message( LoggingTypes.Error, [error.message, "\n", error.stack], getCallerFromExisting(error) ); } } errorMessage(...message: any[]) { if (this._logLevel <= LoggingTypes.Error) this.message(LoggingTypes.Error, message); } protected getCurrentTime(): any { if (browser && window.performance && window.performance.now) { return window.performance.now(); } else { return Date.now(); } } /** * The time difference in milliseconds (fractions allowed!) * @param start Start time from getCurrentTime */ protected getTimeDiff(start: any) { if (browser && window.performance && window.performance.now) { return window.performance.now() - start; } else { return Date.now() - start; } } time(id?: string, name = id) { if (!id) { id = Math.floor(Math.random() * 899999 + 100000).toString(); } this.timerMap.set(id, { name, start: this.getCurrentTime(), }); return { id, end: () => this.timeEnd(id), }; } timeEnd(id: string) { let timer = this.timerMap.get(id); if (timer) { let diff = this.getTimeDiff(timer.start); this.message(LoggingTypes.Debug, [ withColor(Colors.GREEN, `[${timer.name}]`), `->`, withColor(Colors.BLUE, diff.toFixed(4)), "ms", ]); } } private message( type: LoggingTypes, message: any[], caller?: { file: string; line: number; column?: number } ) { if (this.$closed) return; let date = new Date().toISOString().replace(/T/, " ").replace(/\..+/, ""); let file_raw = caller; if (!file_raw) { try { file_raw = getCallerFile(); } catch (err) { file_raw = { file: "", line: 0, column: 0, }; } } if (PROJECT_ROOT && file_raw.file.startsWith(PROJECT_ROOT)) { let newF = file_raw.file.substring(PROJECT_ROOT.length); if (newF.startsWith("/") || newF.startsWith("\\")) newF = newF.substring(1); file_raw.file = newF; } let file = `${file_raw.file}:${file_raw.line}:${file_raw.column || 0}`; let type_str = LoggingTypes[type].toUpperCase().padEnd(5, " "); let type_format: Format[] = []; switch (type) { case LoggingTypes.Log: type_format = this._formatMap.log; break; case LoggingTypes.Error: type_format = this._formatMap.error; break; case LoggingTypes.Debug: type_format = this._formatMap.debug; break; case LoggingTypes.Warning: type_format = this._formatMap.warning; break; } const prefix: FormattedText[] = []; const a = (text: string, formats: Format[] = []) => { prefix.push({ text, formats, }); }; a("["); a(date, this._formatMap.date); a("]["); a(type_str, type_format); if (file_raw.file) { a("]["); a(file, this._formatMap.file); } a("]: "); let raw: string[] = []; const formatted: FormattedLine[] = []; let line: FormattedLine; const newLine = () => { if (line && line.length > 0) { formatted.push(line); raw.push(line.map((e) => e.text).join("")); } line = [...prefix]; }; newLine(); message.forEach((e, i) => { let formats: Format[] = []; if (typeof e !== "string") { if (e && typeof e === "object") { if (e[colorSymbol]) { formats.push({ type: FormatTypes.COLOR, color: e[colorSymbol], }); e = e.value; } } if (typeof e !== "string") e = inspect(e, { colors: true, showHidden: true, depth: 3, }) as string; } removeColors(e) .split("\n") .map((text, index, { length }) => { line.push({ text, formats }); if (index < length - 1) { newLine(); } }); if (!e.endsWith("\n") && i < message.length - 1) { line.push({ text: " ", formats: [] }); } }); newLine(); let msg: Message = { date: new Date(), file, name: this._name, text: { raw, formatted, }, type, }; this.messageObservable.send(msg); } } const colorSymbol = Symbol("color"); export interface ColorFormat { [colorSymbol]: Colors; value: any; } export function withColor(color: Colors, value: any): ColorFormat { return { [colorSymbol]: color, value, }; } function getStack() { // Save original Error.prepareStackTrace let origPrepareStackTrace = (Error).prepareStackTrace; try { // Override with function that just returns `stack` (Error).prepareStackTrace = function (_, stack) { return stack; }; // Create a new `Error`, which automatically gets `stack` let err = new Error(); // Evaluate `err.stack`, which calls our new `Error.prepareStackTrace` let stack: any[] = err.stack; return stack; } finally { // Restore original `Error.prepareStackTrace` (Error).prepareStackTrace = origPrepareStackTrace; } } function getCallerFile() { try { let stack = getStack(); let current_file = stack.shift().getFileName(); while (stack.length) { let caller_file = stack.shift(); if (current_file !== caller_file.getFileName()) { console.log(Object.keys(caller_file)); return { file: caller_file.getFileName(), line: caller_file.getLineNumber(), column: caller_file.getColumnNumber(), }; } } } catch (err) {} return { file: undefined, line: 0 }; } function getCallerFromExisting( err: Error ): { file: string; line: number; column?: number } { if (!err || !err.stack) return { file: "NOFILE", line: 0 }; let lines = err.stack.split("\n"); lines.shift(); // removing first line while (lines.length > 0) { let line = lines.shift(); let matches = line.match( /[<]?([a-zA-Z]:)?([\/\\]?[a-zA-Z_-])+[.>][a-zA-Z_-]*([:][0-9]+)+/g ); if (matches && matches.length > 0) { let match = matches[0].trim(); let locationString = match.match(/([:][0-9]+)+$/gm)[0]; let line: number; let column: number; if (locationString) { match = match.slice(0, match.length - locationString.length); locationString = locationString.substring(1); [line, column] = locationString.split(":").map(Number); } let file = match; return { file, line, column, }; } } }