import * as util from "util"; import * as fs from "fs"; import { EventEmitter } from "events"; import * as path from "path"; import { Lock, Observable } from "@hibas123/utils"; import { Adapter, LoggingTypes, Message } from "./types"; import { ConsoleWriter } from "./consolewriter"; const OriginalErrorStackFunction = (Error.prototype).prepareStackTrace export const Colors = { Reset: "\x1b[0m", Bright: "\x1b[1m", Dim: "\x1b[2m", Underscore: "\x1b[4m", Blink: "\x1b[5m", Reverse: "\x1b[7m", Hidden: "\x1b[8m", FgBlack: "\x1b[30m", FgRed: "\x1b[31m", FgGreen: "\x1b[32m", FgYellow: "\x1b[33m", FgBlue: "\x1b[34m", FgMagenta: "\x1b[35m", FgCyan: "\x1b[36m", FgWhite: "\x1b[37m", BgBlack: "\x1b[40m", BgRed: "\x1b[41m", BgGreen: "\x1b[42m", BgYellow: "\x1b[43m", BgBlue: "\x1b[44m", BgMagenta: "\x1b[45m", BgCyan: "\x1b[46m", BgWhite: "\x1b[47m" } export interface LoggingBaseOptions { /** * Name will be prefixed on Console output and added to logfiles, if not specified here */ name: string, files: boolean | { /** * Filename/path of the logfile. Skip if generated with name */ logfile: string; /** * Filename/path of the logfile. Skip if generated with name */ errorfile: string; } /** * Prints output to console */ console: boolean; } export class LoggingBase { private logFile: any; private errorFile: any; private adapter: Adapter[] = []; private adapter_init: Promise[] = []; private messageObservable = new Observable(); private name: string; constructor(options?: Partial | string) { let opt: Partial; if (!options) opt = {} else if (typeof options === "string") { opt = { name: options }; } else { opt = options; } let config = { name: undefined, console: true, files: true, ...opt }; if (typeof config.files === "boolean") { if (config.files === true) { let name = ""; if (config.name) name = "." + config.name; config.files = { logfile: `./logs/all${name}.log`, errorfile: `./logs/error${name}.log` } } else { config.files = undefined; } } if (config.name) this.name = config.name; for (let key in this) { if (typeof this[key] === "function") this[key] = (this[key]).bind(this); } if (typeof config.files !== "boolean" && config.files !== undefined && (config.files.logfile || config.files.errorfile)) { const { LoggingFiles } = require("./filewriter"); if (config.files.logfile) { this.addAdapter(new LoggingFiles(config.files.logfile)); } if (config.files.errorfile) { this.addAdapter(new LoggingFiles(config.files.errorfile, true)); } } if (config.console) { this.addAdapter(new ConsoleWriter()); } } addAdapter(adapter: Adapter) { this.adapter.push(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 { return Promise.all(this.adapter.map(elm => elm.flush(false))).then(() => { }); } } public waitForSetup() { return Promise.all(this.adapter_init); } public events: EventEmitter = new EventEmitter(); debug(...message: any[]) { this.message(LoggingTypes.Debug, message); } log(...message: any[]) { this.message(LoggingTypes.Log, message); } warning(...message: any[]) { this.message(LoggingTypes.Warning, message); } logWithCustomColors(type: LoggingTypes, colors: string, ...message: any[]) { this.message(type, message, colors); } error(error: Error | string) { if (!error) error = "Empty ERROR was passed, so no informations available"; if (typeof error === "string") { let e = new Error() this.message(LoggingTypes.Error, [error, "\n", e.stack]); } else { this.message(LoggingTypes.Error, [error.message, "\n", error.stack], undefined, getCallerFromExisting(error)); } } errorMessage(...message: any[]) { this.message(LoggingTypes.Error, message); } private message(type: LoggingTypes, message: any[] | string, customColors?: string, caller?: { file: string, line: number }) { let file_raw = caller || getCallerFile(); let file = `${file_raw.file}:${String(file_raw.line).padEnd(3, " ")}`; let mb = ""; if (typeof message === "string") { mb = message; } else { message.forEach((e, i) => { if (typeof e !== "string") e = util.inspect(e, false, null); if (e.endsWith("\n") || i === message.length - 1) { mb += e; } else { mb += e + " "; } }); } let lines = mb.split("\n"); let date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); let prefix = `[ ${date} ][${LoggingTypes[type].toUpperCase().padEnd(5, " ")}][${file}]: `; let formatted = lines.map(line => prefix + line); let msg: Message = { date: new Date(), file, name: this.name, text: { raw: lines, formatted }, type, customColors } this.messageObservable.send(msg); } } export let Logging: LoggingBase = undefined; if (process.env.LOGGING_NO_DEFAULT !== "true") { Logging = new LoggingBase(); } export default Logging; 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 getCallerFile() { try { let stack = getStack() let current_file = stack.shift().getFileName(); while (stack.length) { let caller_file = stack.shift(); const util = require("util") if (current_file !== caller_file.getFileName()) return { file: path.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"); let current = path.basename(__filename); 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(":") if (f != current) { return { file: f, line: Number(line) }; } } } }