import { Observable } from "@hibas123/utils"; import { ConsoleAdapter } from "./consolewriter"; import inspect from "./inspect"; import { Adapter, LoggingTypes, Message, FormatConfig, DefaultFormatConfig, Format, FormatTypes, Colors, FormattedText, FormattedLine } from "./types"; 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; } export class LoggingBase { private _formatMap: FormatConfig = new DefaultFormatConfig(); public set formatMap(value: FormatConfig) { this._formatMap = value; } private adapter = new Set(); private adapter_init: Promise[] = []; 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) { 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 (config.console) { this.addAdapter(new 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); } addAdapter(adapter: Adapter) { if (!this.adapter.has(adapter)) { this.adapter.add(adapter); let prms = Promise.resolve(adapter.init(this.messageObservable.getPublicApi(), this._name)); this.adapter_init.push(prms); } } flush(sync: true): void; flush(sync: false): Promise; flush(sync: boolean): void | Promise { if (sync) { this.adapter.forEach(elm => elm.flush(true)); } else { let adapters: (void | Promise)[] = []; this.adapter.forEach(elm => adapters.push(elm.flush(false))); return Promise.all(adapters).then(() => { }); } } public close() { this.adapter.forEach(adapter => adapter.close ? adapter.close() : undefined); } 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); } private message(type: LoggingTypes, message: any[], caller?: { file: string, line: number }) { let date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); let file_raw = caller || getCallerFile(); let file = `${file_raw.file}:${String(file_raw.line).padEnd(3, " ")}`; 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 (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; // 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; // Restore original `Error.prepareStackTrace` (Error).prepareStackTrace = origPrepareStackTrace; // Remove superfluous function call on stack stack.shift(); // getStack --> Error return stack } function baseName(path) { return path.split(/[\\/]/).pop(); } 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()) return { file: baseName(caller_file.getFileName()), line: caller_file.getLineNumber() }; } } catch (err) { } return { file: undefined, line: 0 }; } function getCallerFromExisting(err: Error): { file: string, line: 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_-]+[:][0-9]+/g) if (matches && matches.length > 0) { let [f, line] = matches[0].split(":") return { file: f, line: Number(line) }; } } }