From 1e3bb4cfec8abb5f3dd20bac0abe3781554626c8 Mon Sep 17 00:00:00 2001
From: Space_Fox <44732812+emanuelfranklyn@users.noreply.github.com>
Date: Tue, 26 Mar 2024 15:03:34 -0300
Subject: [PATCH 01/10] (feat): Initial Rewrite
Logging to console and main base Logger class already works, Saving to file and AutoLogEnd are WIP
---
.eslintignore | 3 +-
.gitignore | 1 +
src/main/autoLogEnd.ts | 2 +-
src/main/defaults/standard.ts | 27 ++
src/main/interfaces/IDefault.ts | 18 +
src/main/interfaces/IEngineSettings.ts | 3 +
src/main/interfaces/IFileStorageSettings.ts | 10 +
src/main/interfaces/ILogMessage.ts | 44 ++
src/main/interfaces/ILoggerOption.ts | 62 +--
src/main/interfaces/index.ts | 3 +
src/main/logger.ts | 475 +++++++++-----------
src/main/outputEngines/consoleEngine.ts | 192 ++++++++
src/main/outputEngines/engine.ts | 42 ++
src/main/outputEngines/fileStorageEngine.ts | 60 +++
src/main/outputEngines/index.ts | 3 +
15 files changed, 643 insertions(+), 302 deletions(-)
mode change 100644 => 100755 src/main/autoLogEnd.ts
create mode 100755 src/main/defaults/standard.ts
create mode 100755 src/main/interfaces/IDefault.ts
create mode 100755 src/main/interfaces/IEngineSettings.ts
create mode 100755 src/main/interfaces/IFileStorageSettings.ts
create mode 100644 src/main/interfaces/ILogMessage.ts
mode change 100644 => 100755 src/main/interfaces/ILoggerOption.ts
mode change 100644 => 100755 src/main/interfaces/index.ts
mode change 100644 => 100755 src/main/logger.ts
create mode 100755 src/main/outputEngines/consoleEngine.ts
create mode 100755 src/main/outputEngines/engine.ts
create mode 100755 src/main/outputEngines/fileStorageEngine.ts
create mode 100755 src/main/outputEngines/index.ts
diff --git a/.eslintignore b/.eslintignore
index 63063a1..ae13b2d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
-build/**/*
\ No newline at end of file
+build/**/*
+*.js
diff --git a/.gitignore b/.gitignore
index c7addbb..1f6cec1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,3 +105,4 @@ dist
# Typescript compiled files
build/
+.directory
diff --git a/src/main/autoLogEnd.ts b/src/main/autoLogEnd.ts
old mode 100644
new mode 100755
index 6274e02..dcd4943
--- a/src/main/autoLogEnd.ts
+++ b/src/main/autoLogEnd.ts
@@ -33,7 +33,7 @@ function exitHandler({ err, options, exitCode }: {err?: {stack: any, message: an
}
export function activate(uncaughtException?: boolean, logger?: Logger): void {
- logger = logger ?? new Logger({ prefix: 'SYSTEM' });
+ logger = logger ?? new Logger({ prefixes: ['SYSTEM'] });
logger?.debug('AutoLogEnd activated!');
process.on('exit', (exitCode) => exitHandler({ exitCode, options: { uncaughtException: false } }));
process.on('SIGINT', (error) => { exitHandler({ err: { message: error, name: null, stack: null }, options: { uncaughtException: false }, exitCode: 'SIGINT' }); });
diff --git a/src/main/defaults/standard.ts b/src/main/defaults/standard.ts
new file mode 100755
index 0000000..1133e86
--- /dev/null
+++ b/src/main/defaults/standard.ts
@@ -0,0 +1,27 @@
+import { EStyles } from '../interfaces';
+import { IDefault } from '../interfaces/IDefault';
+
+export default {
+ logLevelMainColors: {
+ 0: '#cc80ff',
+ 1: '#ff8a1c',
+ 2: '#ff4a4a',
+ 3: '#ffffff',
+ 4: '#555555',
+ },
+ logLevelAccentColors: {
+ 0: '#ffffff',
+ 1: '#ffffff',
+ 2: '#ffffff',
+ 3: '#ff0000',
+ 4: '#ffffff',
+ },
+ prefixMainColor: '#777777',
+ prefixAccentColor: '#000000',
+ redactionText: '[REDACTED]',
+ undefinedColor: '#5555aa',
+ causedByTextColor: '#ffffff',
+ causedByBackgroundColor: '#ff0000',
+ variableStyling: [EStyles.bold, EStyles.textColor],
+ variableStylingParams: ['', '#55ff55'],
+} as IDefault;
diff --git a/src/main/interfaces/IDefault.ts b/src/main/interfaces/IDefault.ts
new file mode 100755
index 0000000..5a1239c
--- /dev/null
+++ b/src/main/interfaces/IDefault.ts
@@ -0,0 +1,18 @@
+import { EStyles } from './ILogMessage';
+
+export interface IDefault {
+ logLevelMainColors: {
+ [key: number]: string,
+ },
+ logLevelAccentColors: {
+ [key: number]: string,
+ },
+ prefixMainColor: string,
+ prefixAccentColor: string,
+ redactionText: string,
+ undefinedColor: string,
+ causedByTextColor?: string,
+ causedByBackgroundColor?: string,
+ variableStyling: EStyles[],
+ variableStylingParams: string[],
+}
diff --git a/src/main/interfaces/IEngineSettings.ts b/src/main/interfaces/IEngineSettings.ts
new file mode 100755
index 0000000..fbddda2
--- /dev/null
+++ b/src/main/interfaces/IEngineSettings.ts
@@ -0,0 +1,3 @@
+export interface IEngineSettings {
+ debug?: boolean;
+}
diff --git a/src/main/interfaces/IFileStorageSettings.ts b/src/main/interfaces/IFileStorageSettings.ts
new file mode 100755
index 0000000..ebacf50
--- /dev/null
+++ b/src/main/interfaces/IFileStorageSettings.ts
@@ -0,0 +1,10 @@
+import { IEngineSettings } from './';
+
+export interface IFileStorageSettings extends IEngineSettings {
+ logFolderPath: string;
+ enableLatestLog?: boolean;
+ enableDebugLog?: boolean;
+ enableErrorLog?: boolean;
+ enableFatalLog?: boolean;
+ compressLogFilesAfterNewExecution?: boolean;
+}
diff --git a/src/main/interfaces/ILogMessage.ts b/src/main/interfaces/ILogMessage.ts
new file mode 100644
index 0000000..5c741de
--- /dev/null
+++ b/src/main/interfaces/ILogMessage.ts
@@ -0,0 +1,44 @@
+import { ILogSettings, IPrefix } from "./ILoggerOption";
+
+export enum EStyles {
+ bold = 1,
+ italic = 2,
+ textColor = 3,
+ backgroundColor = 4,
+ specialSubLine = 5,
+ reset = 6,
+}
+
+export enum ELoggerLevel {
+ INFO = 0,
+ WARN = 1,
+ ALERT = 1,
+ ERROR = 2,
+ SEVERE = 2,
+ FATAL = 3,
+ DEBUG = 4
+}
+
+export enum ELoggerLevelNames {
+ 'INFO',
+ 'WARN',
+ 'ERROR',
+ 'FATAL',
+ 'DEBUG'
+}
+
+export interface IMessageChunk {
+ content: string;
+ styling: EStyles[];
+ stylingParams: string[];
+ subLine: boolean;
+}
+
+export interface ILogMessage {
+ messageChunks: IMessageChunk[];
+ subLines: IMessageChunk[];
+ prefixes: IPrefix[];
+ timestamp: Date;
+ logLevel: ELoggerLevel;
+ settings: ILogSettings;
+}
diff --git a/src/main/interfaces/ILoggerOption.ts b/src/main/interfaces/ILoggerOption.ts
old mode 100644
new mode 100755
index 425326b..264fc68
--- a/src/main/interfaces/ILoggerOption.ts
+++ b/src/main/interfaces/ILoggerOption.ts
@@ -1,59 +1,27 @@
/* eslint-disable no-unused-vars */
-export enum ELoggerLevel {
- INFO = 0,
- LOG = 0,
- WARN = 1,
- ALERT = 1,
- ERROR = 2,
- SEVERE = 2,
- FATAL = 3,
- DEBUG = 4
-}
+import { IDefault } from './IDefault';
+import { ELoggerLevel } from './ILogMessage';
-export enum ELoggerLevelNames {
- 'INFO',
- 'WARN',
- 'ERROR',
- 'FATAL',
- 'DEBUG'
+export interface IPrefix {
+ content: string;
+ color: ((text: string) => string | string[]) | string | string[];
+ backgroundColor: ((text: string) => string | string[]) | string | string[] | null;
}
-// color for text or colored background
-export enum ELoggerLevelBaseColors {
- '#cc80ff',
- '#ff8a1c',
- '#ff4a4a',
- '#ffffff',
- '#555555',
+export interface ISharedLogSettings {
+ coloredBackground?: boolean;
}
-// color for text on colored background
-export const ELoggerLevelAlternateColors = [
- '#000000',
- '#000000',
- '#000000',
- '#ff0000',
- '#D4D4D4'
-]
-
-export interface ILoggerFileProperties {
- enable: boolean;
- logFolderPath: string;
- enableLatestLog?: boolean;
- enableDebugLog?: boolean;
- enableErrorLog?: boolean;
- enableFatalLog?: boolean;
- generateHTMLLog?: boolean;
- compressLogFilesAfterNewExecution?: boolean;
+export interface ILogSettings extends ISharedLogSettings {
+ default: IDefault;
}
-export interface ILoggerOptions {
+export interface ILoggerOptions extends ISharedLogSettings {
defaultLevel?: ELoggerLevel;
- prefix?: string;
- debug?: boolean;
- coloredBackground?: boolean;
- allLineColored?: boolean;
+ prefixes: (IPrefix | string)[];
disableFatalCrash?: boolean;
- fileProperties?: ILoggerFileProperties
+ redactedContent?: string[];
+ allLineColored?: boolean;
+ defaultSettings?: IDefault;
}
diff --git a/src/main/interfaces/index.ts b/src/main/interfaces/index.ts
old mode 100644
new mode 100755
index e43a52e..b6097b4
--- a/src/main/interfaces/index.ts
+++ b/src/main/interfaces/index.ts
@@ -1 +1,4 @@
export * from './ILoggerOption';
+export * from './IFileStorageSettings';
+export * from './ILogMessage';
+export * from './IEngineSettings';
diff --git a/src/main/logger.ts b/src/main/logger.ts
old mode 100644
new mode 100755
index d3b6d37..8c2529c
--- a/src/main/logger.ts
+++ b/src/main/logger.ts
@@ -1,300 +1,269 @@
-import { ILoggerOptions, ELoggerLevel, ILoggerFileProperties, ELoggerLevelNames, ELoggerLevelBaseColors, ELoggerLevelAlternateColors } from './interfaces';
-import escape from 'escape-html';
-import admZip from 'adm-zip';
-import chalk from 'chalk';
-import Path from 'path';
-import fs from 'fs';
+import { ELoggerLevel, EStyles, ILogMessage, IMessageChunk } from './interfaces/ILogMessage';
+import { ILoggerOptions, IPrefix } from './interfaces';
+import { Engine } from './outputEngines/engine';
import utils from 'util';
+import { IDefault } from './interfaces/IDefault';
+import Standard from './defaults/standard';
export class Logger {
- private defaultLevel: ELoggerLevel = ELoggerLevel.LOG;
- private debugActive = false;
- private prefix?: string;
- private coloredBackground: boolean;
+ private defaultLevel: ELoggerLevel = ELoggerLevel.INFO;
+ private prefixes: IPrefix[];
private disableFatalCrash: boolean;
+ private logListeners: Engine[] = [];
+ private redactedContent: string[];
+ private coloredBackground: boolean;
private allLineColored: boolean;
- private fileProperties: ILoggerFileProperties;
- private latestFileStream?: fs.WriteStream;
- private debugLogStream?: fs.WriteStream;
- private errorLogStream?: fs.WriteStream;
- private htmlBackgroundColor: string;
- private htmlTextColor: string;
- private defaultHeader = '';
-
- constructor({ prefix, debug, defaultLevel, coloredBackground, disableFatalCrash, allLineColored, fileProperties }: ILoggerOptions) {
- this.prefix = prefix ?? '';
- this.debugActive = debug ?? false;
+ private defaultSettings: IDefault;
+
+ constructor({ prefixes, defaultLevel, disableFatalCrash, redactedContent, allLineColored, coloredBackground, defaultSettings }: ILoggerOptions) {
this.defaultLevel = defaultLevel ?? ELoggerLevel.INFO;
- this.coloredBackground = coloredBackground ?? false;
this.disableFatalCrash = disableFatalCrash ?? false;
+ this.redactedContent = redactedContent ?? [];
+
this.allLineColored = allLineColored ?? false;
+ this.coloredBackground = coloredBackground ?? false;
+ this.defaultSettings = defaultSettings ?? Standard;
- this.htmlBackgroundColor = '#0a002b';
- this.htmlTextColor = '#ffffff';
-
- this.fileProperties = {
- enable: false,
- logFolderPath: Path.join(__dirname, 'logs'),
- enableLatestLog: true,
- enableDebugLog: false,
- enableErrorLog: false,
- enableFatalLog: true,
- generateHTMLLog: false,
- compressLogFilesAfterNewExecution: true,
- };
+ this.prefixes = this.parseMessagePrefix(prefixes);
+ }
- this.fileProperties = { ...this.fileProperties, ...fileProperties ?? {} };
+ private parseMessagePrefix(prefixes: (IPrefix | string)[]): IPrefix[] {
+ if (!prefixes || prefixes.length === 0) return [];
+ return prefixes.map((prefix) => {
+ if (typeof prefix !== 'string') return prefix;
+ return {
+ content: prefix,
+ color: this.defaultSettings.prefixMainColor,
+ backgroundColor: null,
+ };
+ });
+ }
- if (this.fileProperties.enable) {
- // create log folder if not exists
- if (!fs.existsSync(this.fileProperties.logFolderPath)) fs.mkdirSync(this.fileProperties.logFolderPath);
- else this.compressLastSessionLogs();
+ private redactText(text: string): string {
+ let modifiedString = text;
+ this.redactedContent.forEach((redaction) => {
+ const reg = new RegExp(redaction, 'gi');
+ const matches = modifiedString.matchAll(reg);
+ let accumulator = 0;
+ for (const match of matches) {
+ if (typeof match.index !== 'number') continue;
+ modifiedString =
+ `${modifiedString.slice(0, match.index + accumulator)}${this.defaultSettings.redactionText}${modifiedString.slice(match.index + match[0].length + accumulator)}`;
+ accumulator += this.defaultSettings.redactionText.length - match[0].length;
+ }
+ });
+ return modifiedString;
+ }
- // creates folders for fatal-crash and latest logs
- if (!fs.existsSync(Path.join(this.fileProperties.logFolderPath, 'fatal-crash'))) fs.mkdirSync(Path.join(this.fileProperties.logFolderPath, 'fatal-crash'));
- if (!fs.existsSync(Path.join(this.fileProperties.logFolderPath, 'latestLogs'))) fs.mkdirSync(Path.join(this.fileProperties.logFolderPath, 'latestLogs'));
+ /**
+ * Parses the message and returns an array of IMessageChunk objects
+ * the first IMessageChunk object is the main message, the rest are subLines
+ * @param text - The content to be parsed
+ * @param args - The arguments to be passed to the content
+ * @returns IMessageChunk[]
+ */
+ private processMessage(text: string | string[] | Error, forceSubline: boolean, ...args: any[]): IMessageChunk[] {
+ if (!text) {
+ return [{
+ content: 'undefined',
+ styling: [EStyles.textColor],
+ stylingParams: [this.defaultSettings.undefinedColor],
+ subLine: forceSubline,
+ }];
+ }
- // eslint-disable-next-line max-len
- this.defaultHeader = `
\n`;
+ // String handling
+ if (typeof text !== 'object') {
+ return [...text.toString().split('\n').map((line) => {
+ return {
+ content: this.redactText(utils.format(line, ...args)),
+ styling: [],
+ stylingParams: [],
+ subLine: forceSubline,
+ };
+ })];
+
+ // Error handling
+ } else if (text instanceof Error) {
+ const finalMessage = [];
+ finalMessage.push({
+ content: (forceSubline ? 'Error: ' : '') + this.redactText(utils.format(text.message, ...args)),
+ styling: [],
+ stylingParams: [],
+ subLine: forceSubline,
+ });
- if (this.fileProperties.enableLatestLog) {
- this.latestFileStream = fs.createWriteStream(
- Path.join(this.fileProperties.logFolderPath, `latest.${this.fileProperties.generateHTMLLog ? 'html' : 'log'}`), { flags: 'a' },
- );
- if (this.fileProperties.generateHTMLLog) this.latestFileStream.write(this.defaultHeader);
- }
- if (this.fileProperties.enableDebugLog) {
- this.debugLogStream = fs.createWriteStream(
- Path.join(this.fileProperties.logFolderPath, 'latestLogs', `debug.${this.fileProperties.generateHTMLLog ? 'html' : 'log'}`), { flags: 'a' },
- );
- if (this.fileProperties.generateHTMLLog) this.debugLogStream.write(this.defaultHeader);
- }
- if (this.fileProperties.enableErrorLog) {
- this.errorLogStream = fs.createWriteStream(
- Path.join(this.fileProperties.logFolderPath, 'latestLogs', `error.${this.fileProperties.generateHTMLLog ? 'html' : 'log'}`), { flags: 'a' },
- );
- if (this.fileProperties.generateHTMLLog) this.errorLogStream.write(this.defaultHeader);
- }
+ const stack = text.stack?.split('\n');
+ stack?.shift();
- // handles process exists to properly close the streams
- process.on('exit', (exitCode) => {
- // eslint-disable-next-line max-len
- this.closeFileStreams(`${this.fileProperties.generateHTMLLog ? '
\n' : '\n'}Process exited with code (${exitCode})${this.fileProperties.generateHTMLLog ? '\n
' : '\n'}`);
+ stack?.forEach((line) => {
+ finalMessage.push({
+ content: this.redactText(line.trim()),
+ styling: [],
+ stylingParams: [],
+ subLine: true,
+ });
});
- } else {
- this.fileProperties.enableLatestLog = false;
- this.fileProperties.enableDebugLog = false;
- this.fileProperties.enableErrorLog = false;
- this.fileProperties.enableFatalLog = false;
- this.fileProperties.generateHTMLLog = false;
- this.fileProperties.compressLogFilesAfterNewExecution = false;
+
+ if (text.cause) {
+ const causedBy = {
+ content: '# Caused by:',
+ styling: [EStyles.specialSubLine],
+ stylingParams: [''],
+ subLine: true,
+ };
+
+ if (this.defaultSettings.causedByBackgroundColor) {
+ causedBy.styling.push(EStyles.backgroundColor);
+ causedBy.stylingParams.push(this.defaultSettings.causedByBackgroundColor);
+ }
+ if (this.defaultSettings.causedByTextColor) {
+ causedBy.styling.push(EStyles.textColor);
+ causedBy.stylingParams.push(this.defaultSettings.causedByTextColor);
+ }
+
+ finalMessage.push(causedBy);
+
+ if (typeof text.cause === 'string' || Array.isArray(text.cause) || text.cause instanceof Error) {
+ finalMessage.push(...this.processMessage(text.cause, true, ...args));
+ } else {
+ finalMessage.push({
+ content: this.redactText(JSON.stringify(text.cause)),
+ styling: [],
+ stylingParams: [],
+ subLine: true,
+ });
+ }
+ }
+ return finalMessage;
+ } else if (!Array.isArray(text) || (Array.isArray(text) && (!args || args.length === 0))) {
+ return [{
+ content: this.redactText(utils.format(text, ...args)),
+ styling: [EStyles.specialSubLine, EStyles.reset],
+ stylingParams: ['', ''],
+ subLine: true,
+ }];
}
- }
- private closeFileStreams(closeStreamMessage?: string, customFatalMessage?: string): void {
- this.writeToAllStreams(closeStreamMessage ?? '', customFatalMessage);
- this.latestFileStream?.end();
- this.debugLogStream?.end();
- this.errorLogStream?.end();
- }
+ const finalMessage: (IMessageChunk & {subLine: boolean})[] = [];
+
+ let switchedToSublines = forceSubline;
+ text.forEach((line, index) => {
+ const variable = args[index];
+ if (line) {
+ finalMessage.push({
+ content: this.redactText(line.toString()),
+ styling: [EStyles.specialSubLine],
+ stylingParams: [''],
+ subLine: switchedToSublines,
+ });
+ }
+ if (variable) {
+ const chunks = this.processMessage(variable, switchedToSublines);
+ chunks[0].styling.push(...this.defaultSettings.variableStyling);
+ chunks[0].stylingParams.push(...this.defaultSettings.variableStylingParams);
+ if (!forceSubline && chunks.find((sublineFinder) => sublineFinder.subLine)) switchedToSublines = true;
+ finalMessage.push(...chunks);
+ }
+ });
- private writeToAllStreams(message: string, customFatalLog?: string): void {
- if (this.fileProperties.enableLatestLog) this.latestFileStream?.write(message);
- if (this.fileProperties.enableDebugLog) this.debugLogStream?.write(message);
- if (this.fileProperties.enableErrorLog) this.errorLogStream?.write(message);
- if (this.fileProperties.enableFatalLog && customFatalLog) {
- // create a new stream for fatal log
- // 4 random alphanumeric characters
- const uniqueId = Math.random().toString(36).substring(2, 6);
- const fatalLogStream = fs.createWriteStream(
- Path.join(this.fileProperties.logFolderPath, 'fatal-crash', `fatal-${uniqueId}-${this.getTime(true, true)}.${this.fileProperties.generateHTMLLog ? 'html' : 'log'}`),
- );
- fatalLogStream.write(this.defaultHeader);
- fatalLogStream.end(customFatalLog);
- }
+ return finalMessage;
}
- private compressLastSessionLogs(): void {
- if (!this.fileProperties.compressLogFilesAfterNewExecution) return;
-
- const zip = new admZip();
-
- var files = fs.readdirSync(this.fileProperties.logFolderPath);
- // const fatalCrashFiles = fs.readdirSync(Path.join(this.fileProperties.logFolderPath, 'fatal-crash'));
- const latestLogsFiles = fs.readdirSync(Path.join(this.fileProperties.logFolderPath, 'latestLogs'));
- // files = files.concat(fatalCrashFiles.map((file) => Path.join('fatal-crash', file)));
- files = files.concat(latestLogsFiles.map((file) => Path.join('latestLogs', file)));
- // use fs.stat on latest.log/html to get its last modified date
- const latestLogPath = Path.join(this.fileProperties.logFolderPath, `latest.${this.fileProperties.generateHTMLLog ? 'html' : 'log'}`);
- const latestLogStats = fs.statSync(latestLogPath);
- // get mtime and replace : with - to avoid windows file system errors
- const latestLogDate = latestLogStats.mtime.toISOString().replace(/:/g, '-').split('.')[0];
- files.forEach((file) => {
- if (file.endsWith('.log') || file.endsWith('.html')) {
- zip.addLocalFile(Path.join(this.fileProperties.logFolderPath, file));
- // don't delete fatal-crash logs
- if (!file.startsWith('fatal')) fs.unlinkSync(Path.join(this.fileProperties.logFolderPath, file));
+ private handleMessage(text: string | string[] | Error, level: ELoggerLevel, ...args: any[]): void {
+ const chunks = this.processMessage(text, false, ...args);
+ const messageChunks: IMessageChunk[] = [];
+ const subLines: IMessageChunk[] = [];
+
+ chunks.forEach((chunk) => {
+ if (chunk.subLine) subLines.push(chunk);
+ else {
+ if (level === ELoggerLevel.FATAL) {
+ chunk.styling.unshift(EStyles.textColor, EStyles.backgroundColor);
+ chunk.stylingParams.unshift(this.defaultSettings.logLevelAccentColors[level], this.defaultSettings.logLevelMainColors[level]);
+ }
+ if (this.allLineColored) {
+ const txtColor = this.coloredBackground ? this.defaultSettings.logLevelAccentColors[level] : this.defaultSettings.logLevelMainColors[level];
+ const bgColor = this.coloredBackground ? this.defaultSettings.logLevelMainColors[level] : this.defaultSettings.logLevelAccentColors[level];
+ if (txtColor) {
+ chunk.styling.unshift(EStyles.textColor);
+ chunk.stylingParams.unshift(txtColor);
+ }
+ if (bgColor) {
+ chunk.styling.unshift(EStyles.backgroundColor);
+ chunk.stylingParams.unshift(bgColor);
+ }
+ }
+ messageChunks.push(chunk);
}
});
- const uniqueId = Math.random().toString(36).substring(2, 6);
- fs.writeFileSync(Path.resolve(this.fileProperties.logFolderPath, `logs-${uniqueId}-${latestLogDate}.zip`), zip.toBuffer());
- }
- private getFormattedPrefix(): string {
- var prefix = '';
- prefix += chalk.hex('#5c5c5c')('[');
- prefix += chalk.gray(this.prefix);
- prefix += chalk.hex('#5c5c5c')(']');
+ const message: ILogMessage = {
+ messageChunks,
+ subLines,
+ prefixes: this.prefixes,
+ timestamp: new Date(),
+ logLevel: level,
+ settings: {
+ coloredBackground: this.coloredBackground,
+ default: this.defaultSettings,
+ },
+ };
- return this.prefix !== '' ? prefix : '';
+ this.logListeners.forEach((logListener) => {
+ logListener.log(message);
+ });
}
- private getTime(fullDate?: boolean, friendlySymbols?: boolean): string {
- const time = new Date(Date.now());
- const day = time.getDate() < 10 ? '0' + time.getDate() : time.getDate();
- const month = time.getMonth() < 10 ? '0' + time.getMonth() : time.getMonth();
- const year = time.getFullYear();
- const seconds = time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds();
- const minutes = time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes();
- const hours = time.getHours() < 10 ? '0' + time.getHours() : time.getHours();
-
- // eslint-disable-next-line max-len
- return `${friendlySymbols ? '' : '['}${fullDate ? day : ''}${fullDate ? (friendlySymbols ? '-' : ':') : ''}${fullDate ? month : ''}${fullDate ? (friendlySymbols ? '-' : ':') : ''}${fullDate ? year : ''}${fullDate ? (friendlySymbols ? 'T' : '-') : ''}${hours}${friendlySymbols ? '-' : ':'}${minutes}${friendlySymbols ? '-' : ':'}${seconds}${friendlySymbols ? '' : ']'}`;
+ log(text: string | string[], ...args: any[]): void {
+ this.handleMessage(text, this.defaultLevel, ...args);
}
- private generateMessagePrefix(level: ELoggerLevel): { coloredMessagePrefix: string; rawMessagePrefix: string, textColor: string } {
- const fgColor = [ELoggerLevelBaseColors[level], ELoggerLevelAlternateColors[level]];
- var time = chalk.hex(fgColor[Number(this.coloredBackground)])(this.getTime() + ' ');
- var prefix = chalk.hex(fgColor[Number(this.coloredBackground)])(this.getFormattedPrefix() + ' ');
- var levelText = chalk.hex(fgColor[Number(this.coloredBackground)])(ELoggerLevelNames[level].toUpperCase() + ':');
-
- if (this.coloredBackground) {
- time = chalk.bgHex(ELoggerLevelBaseColors[level])(time);
- prefix = chalk.bgHex(ELoggerLevelBaseColors[level])(prefix);
- levelText = chalk.bgHex(ELoggerLevelBaseColors[level])(levelText);
- }
-
- return {
- coloredMessagePrefix: `${time}${prefix}${levelText}`,
- rawMessagePrefix: `${this.getTime()} [${this.prefix}] ${ELoggerLevelNames[level].toUpperCase()}:`,
- textColor: fgColor[Number(this.coloredBackground)],
- };
+ info(text: string | string[], ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.INFO, ...args);
}
- log(text: any, levelToLog?: ELoggerLevel, ...args: any): void {
- const level = levelToLog ?? this.defaultLevel;
- if (text instanceof Error) {
- text = text.toString();
- }
- text = utils.format(text, ...args);
- if (level === ELoggerLevel.FATAL) return this.fatal(text, ...args);
- const consoleLevels = {
- [ELoggerLevel.INFO]: console.log,
- [ELoggerLevel.WARN]: console.warn,
- [ELoggerLevel.ERROR]: console.error,
- [ELoggerLevel.DEBUG]: console.debug,
- };
-
- const { coloredMessagePrefix, rawMessagePrefix, textColor } = this.generateMessagePrefix(level);
-
- const messageToConsole = (this.coloredBackground && this.allLineColored)
- ? chalk.bgHex(ELoggerLevelBaseColors[level])(chalk.hex(ELoggerLevelAlternateColors[level])(' ' + text))
- : (this.allLineColored ? chalk.hex(ELoggerLevelBaseColors[level])(' ' + text) : ' ' + text)
- ;
-
- if ((this.debugActive && level === ELoggerLevel.DEBUG) || (level !== ELoggerLevel.DEBUG)) {
- consoleLevels[level](coloredMessagePrefix + messageToConsole);
- }
-
- // escapes the text to a be secure to be used in html
- const escapedText = escape(text.toString());
-
- // eslint-disable-next-line max-len
- const textSpanElement = this.allLineColored ? `${escapedText}` : `${escapedText}`;
- // eslint-disable-next-line max-len
- const parentSpanElement = `${rawMessagePrefix} ${textSpanElement}\n`;
-
- if (this.fileProperties.enableDebugLog) {
- this.debugLogStream?.write(this.fileProperties.generateHTMLLog ? parentSpanElement : (rawMessagePrefix + ' ' + text + '\n'));
- }
- if (this.fileProperties.enableErrorLog && level === ELoggerLevel.ERROR) {
- // eslint-disable-next-line max-len
- this.errorLogStream?.write(this.fileProperties.generateHTMLLog ? parentSpanElement : (rawMessagePrefix + ' ' + text + '\n'));
- }
- if (this.fileProperties.enableLatestLog && level !== ELoggerLevel.DEBUG) {
- this.latestFileStream?.write(this.fileProperties.generateHTMLLog ? parentSpanElement : (rawMessagePrefix + ' ' + text + '\n'));
- }
+ warn(text: string | string[], ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.WARN, ...args);
}
- info(text: any, ...args: any): void {
- this.log(text, ELoggerLevel.INFO, ...args);
+ error(text: string | string[] | Error, ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.ERROR, ...args);
}
- warn(text: any, ...args: any): void {
- this.log(text, ELoggerLevel.WARN, ...args);
+ debug(text: string | string[], ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.DEBUG, ...args);
}
- error(text: any, ...args: any): void {
- this.log(text, ELoggerLevel.ERROR, ...args);
+ fatal(text: string | string[] | Error, ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.FATAL, ...args);
}
- debug(text: any, ...args: any): void {
- this.log(text, ELoggerLevel.DEBUG, ...args);
+ alert(text: string | string[], ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.ALERT, ...args);
}
- fatal(text: any, ...args: any): void {
- var message = text.toString();
- var stack: string[] | undefined = [];
- var fullString = text.toString();
- if (text instanceof Error) {
- stack = text.stack?.split('\n');
- if (stack) {
- fullString = stack.join('\n');
- message = stack.shift() ?? '';
- }
- }
-
- message = utils.format(message, ...args);
-
- const time = this.getTime();
- const prefix = this.getFormattedPrefix();
- const levelMsg = text.toString().startsWith('Error') ? ELoggerLevelNames[3] : `${ELoggerLevelNames[3]} ${ELoggerLevelNames[2]}:`;
-
- message = `${time} ${prefix} ${levelMsg} ${message.toString()}${stack ? '\n' + stack.join('\n') : ''}`;
-
- const msg = chalk.bgWhite(chalk.redBright(message));
-
- var escapedFullText = escape(fullString);
- const escapedText = escape(text.toString());
-
- // convert tabs to html
- escapedFullText = escapedFullText.replace(/\t/g, ' ');
- escapedFullText = escapedFullText.replace(/ /g, ' ');
-
- const splitFullEscapedText = escapedFullText.split('\n');
- const htmlFullText = '' + splitFullEscapedText.join('') + '';
-
- const textSpan = `${escapedText}`;
- const fullSpan = `${htmlFullText}`;
- // eslint-disable-next-line max-len
- const prefixSpan = `${time} [${this.prefix}] ${levelMsg} ${textSpan}\n`;
- // eslint-disable-next-line max-len
- const fullPrefixSpan = `${time} [${this.prefix}] ${levelMsg} ${fullSpan}\n`;
+ severe(text: string | string[] | Error, ...args: any[]): void {
+ this.handleMessage(text, ELoggerLevel.SEVERE, ...args);
+ }
- // eslint-disable-next-line max-len
- const finalMessage = (this.fileProperties.generateHTMLLog ? prefixSpan : (time + ' [' + this.prefix + '] ' + levelMsg + ' ' + text + '\n')) + 'Please check the fatal log file for more details.\n';
- const finalFatalMessage = this.fileProperties.generateHTMLLog ? fullPrefixSpan : (time + ' [' + this.prefix + '] ' + levelMsg + ' ' + fullString + '\n');
+ /**
+ * Allows the assignment of a listener callback that will be called every log made
+ * @param listenerCallback void function with the actions to be executed every log. Receives ILogMessage object.
+ */
+ registerListener(listenerEngine: Engine): void {
+ this.logListeners.push(listenerEngine);
+ }
- if (this.disableFatalCrash) {
- this.writeToAllStreams(finalMessage, finalFatalMessage);
- } else {
- this.closeFileStreams(finalMessage, finalFatalMessage);
- }
+ unRegisterListener(listenerEngine: Engine): void {
+ this.logListeners = this.logListeners.filter((listener) => listener !== listenerEngine);
+ }
- console.error(msg);
+ setColoredBackground(coloredBackground: boolean): void {
+ this.coloredBackground = coloredBackground;
+ }
- if (!this.disableFatalCrash) {
- process.exit(5);
- }
+ setAllLineColored(allLineColored: boolean): void {
+ this.allLineColored = allLineColored;
}
}
diff --git a/src/main/outputEngines/consoleEngine.ts b/src/main/outputEngines/consoleEngine.ts
new file mode 100755
index 0000000..983368e
--- /dev/null
+++ b/src/main/outputEngines/consoleEngine.ts
@@ -0,0 +1,192 @@
+import { ELoggerLevel, ELoggerLevelNames, EStyles, ILogMessage, IMessageChunk, IPrefix } from '../interfaces';
+import { Engine } from './';
+import chalk from 'chalk';
+
+export class ConsoleEngine extends Engine {
+ private prefixes = new Map();
+
+ private consoleLoggers = {
+ [ELoggerLevel.INFO]: console.info,
+ [ELoggerLevel.WARN]: console.warn,
+ [ELoggerLevel.ERROR]: console.error,
+ [ELoggerLevel.FATAL]: console.error,
+ [ELoggerLevel.DEBUG]: console.debug,
+ };
+
+ private parseTextStyles(chunk: IMessageChunk, subLine?: boolean, backgroundColor?: string, customFGColor?: string): string {
+ let finalMsg = subLine ? (backgroundColor ? chalk.bgHex(backgroundColor).gray : chalk.gray) : chalk.reset;
+ let special = false;
+ chunk.styling.forEach((style, index) => {
+ switch (style) {
+ case EStyles.bold:
+ finalMsg = finalMsg.bold;
+ break;
+ case EStyles.italic:
+ finalMsg = finalMsg.italic;
+ break;
+ case EStyles.backgroundColor:
+ finalMsg = finalMsg.bgHex(chunk.stylingParams[index]);
+ break;
+ case EStyles.textColor:
+ finalMsg = finalMsg.hex(chunk.stylingParams[index]);
+ break;
+ case EStyles.specialSubLine:
+ special = true;
+ break;
+ case EStyles.reset:
+ finalMsg = finalMsg.reset;
+ break;
+ default:
+ break;
+ }
+ });
+ let finalMessage = '';
+ const fullLineTxt = chunk.content.padEnd(process.stdout.columns - (subLine && !special ? 3 : 0));
+ if (subLine && !special) {
+ finalMessage += (customFGColor ? (backgroundColor ? chalk.bgHex(backgroundColor) : chalk).hex(customFGColor)('| ') : finalMsg('| '));
+ finalMessage += (customFGColor ? finalMsg.hex(customFGColor)(fullLineTxt) : finalMsg(fullLineTxt));
+ } else if (subLine && special) {
+ finalMessage += finalMsg(fullLineTxt);
+ } else {
+ finalMessage += (customFGColor ? chalk.hex(customFGColor)(finalMsg(chunk.content)) : finalMsg(chunk.content));
+ }
+ return finalMessage;
+ }
+
+ private parsePrefix(prefixes: IPrefix[], defaultBg?: string): (string | undefined)[] {
+ return prefixes.map((prefix) => {
+ if (this.prefixes.has(prefix.content)) return this.prefixes.get(prefix.content);
+
+ let bgColor = '';
+ let bgColorArray: string[] = [];
+ if (prefix.backgroundColor && !Array.isArray(prefix.backgroundColor)) {
+ if (typeof prefix.backgroundColor === 'function') {
+ const result = prefix.backgroundColor(prefix.content);
+ if (!Array.isArray(result)) bgColor = result;
+ else bgColorArray = result;
+ } else {
+ bgColor = prefix.backgroundColor;
+ }
+ } else if (prefix.backgroundColor && Array.isArray(prefix.backgroundColor)) {
+ bgColorArray = prefix.backgroundColor;
+ }
+
+ let fgColor = '';
+ let fgArray: string[] = [];
+ if (!Array.isArray(prefix.color)) {
+ if (typeof prefix.color === 'function') {
+ const result = prefix.color(prefix.content);
+ if (!Array.isArray(result)) fgColor = result;
+ else fgArray = result;
+ } else {
+ fgColor = prefix.color;
+ }
+ } else {
+ fgArray = prefix.color;
+ }
+
+ // static colors
+ if (bgColor && fgColor) {
+ const result = chalk.bgHex(bgColor).hex(fgColor)(prefix.content);
+ this.prefixes.set(prefix.content, result);
+ return result;
+ }
+ if (!bgColor && bgColorArray.length <= 0 && fgColor) {
+ const result = (defaultBg ? chalk.bgHex(defaultBg) : chalk).hex(fgColor)(prefix.content);
+ this.prefixes.set(prefix.content, result);
+ return result;
+ }
+
+ // Gradients
+ let finalMsg = '';
+
+ // repeat the last color so that fgArray size matches prefix.content size
+ if (fgArray.length > 0 && fgArray.length < prefix.content.length) fgArray.push(...Array(prefix.content.length - fgArray.length).fill(fgArray[0]));
+
+ // Has background color gradient
+ if (bgColorArray.length > 0) {
+ // repeat the last color so that bgColorArray size matches prefix.content size
+ if (bgColorArray.length < prefix.content.length) bgColorArray.push(...Array(prefix.content.length - bgColorArray.length).fill(bgColorArray[0]));
+ bgColorArray.forEach((color, index) => {
+ if (fgArray.length > 0) finalMsg += chalk.bgHex(color).hex(fgArray[index])(prefix.content[index]);
+ else finalMsg += chalk.bgHex(color).hex(fgColor)(prefix.content[index]);
+ });
+
+ // Doesn't have background color or it is a static color
+ } else {
+ fgArray.forEach((color, index) => {
+ if (bgColor) finalMsg += chalk.bgHex(bgColor).hex(color)(prefix.content[index]);
+ else finalMsg += (defaultBg ? chalk.bgHex(defaultBg) : chalk).hex(color)(prefix.content[index]);
+ });
+ }
+ this.prefixes.set(prefix.content, finalMsg);
+ return finalMsg;
+ });
+ }
+
+ log(message: ILogMessage): void {
+ if (!this.debug && message.logLevel === ELoggerLevel.DEBUG) return;
+
+ const defaultSettings = message.settings.default;
+ const shouldColorBg = message.settings.coloredBackground || message.logLevel === ELoggerLevel.FATAL;
+
+ if (shouldColorBg) this.prefixes.clear();
+
+ let formatter: chalk.Chalk = chalk.reset;
+ if (shouldColorBg) formatter = formatter.bgHex(defaultSettings.logLevelMainColors[message.logLevel]);
+
+ if (shouldColorBg && defaultSettings.logLevelAccentColors[message.logLevel]) formatter = formatter.hex(defaultSettings.logLevelAccentColors[message.logLevel]);
+
+ if (!message.settings.coloredBackground && message.logLevel !== ELoggerLevel.FATAL) formatter = formatter.hex(defaultSettings.logLevelMainColors[message.logLevel]);
+ const timestamp = formatter(this.getTime(message.timestamp));
+
+ const prefixes = this.parsePrefix(message.prefixes, shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined);
+
+ if (message.logLevel === ELoggerLevel.FATAL) this.prefixes.clear();
+
+ let prefixTxt = '';
+ prefixes.forEach((prefix) => {
+ prefixTxt += formatter(' [');
+ prefixTxt += prefix;
+ prefixTxt += formatter(']');
+ });
+ const level = formatter(` ${ELoggerLevelNames[message.logLevel]}:`);
+
+ // adds a space before the first chunk to separate the message from the : in the log without coloring the background if allLineColored is false
+ const firstChunk = message.messageChunks[0];
+ message.messageChunks.unshift({
+ content: ' ',
+ styling: firstChunk.styling,
+ stylingParams: firstChunk.stylingParams,
+ subLine: false,
+ });
+
+ const txt = message.messageChunks.map((chunk): string => this.parseTextStyles(chunk, false, shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined));
+
+ this.consoleLoggers[message.logLevel](`${timestamp}${prefixTxt}${level}${txt.join('')}`);
+
+ if (!message.subLines || message.subLines.length <= 0) return;
+
+ message.subLines.forEach((line) =>
+ this.consoleLoggers[message.logLevel](this.parseTextStyles(
+ line,
+ true,
+ shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined,
+ shouldColorBg ? defaultSettings.logLevelAccentColors[message.logLevel] : undefined,
+ )),
+ );
+
+ this.consoleLoggers[message.logLevel](
+ this.parseTextStyles(
+ {
+ content: '#'.padEnd(process.stdout.columns, '-'),
+ styling: [EStyles.specialSubLine, EStyles.textColor],
+ stylingParams: ['', defaultSettings.logLevelAccentColors[message.logLevel] || '#ffffff'],
+ subLine: true,
+ },
+ true,
+ shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined,
+ ),
+ );
+ }
+}
diff --git a/src/main/outputEngines/engine.ts b/src/main/outputEngines/engine.ts
new file mode 100755
index 0000000..351a258
--- /dev/null
+++ b/src/main/outputEngines/engine.ts
@@ -0,0 +1,42 @@
+import { ILogMessage, IEngineSettings } from '../interfaces';
+import { Logger } from '../logger';
+
+export abstract class Engine {
+ debug: boolean;
+ private loggers: Logger[] = [];
+
+ constructor(settings?: IEngineSettings, ...loggers: Logger[]) {
+ if (Array.isArray(settings)) {
+ loggers = settings;
+ settings = undefined;
+ }
+
+ this.debug = settings?.debug || false;
+ this.loggers = loggers;
+
+ this.loggers.forEach((logger) => {
+ logger.registerListener(this);
+ });
+ }
+
+ destroy(): void {
+ this.loggers.forEach((logger) => {
+ logger.unRegisterListener(this);
+ });
+ }
+
+ getTime(time: Date, fullDate?: boolean): string {
+ const day = `0${time.getDate()}`.slice(-2);
+ const month = `0${time.getMonth()}`.slice(-2);
+ const year = `0${time.getFullYear()}`.slice(-2);
+ const seconds = `0${time.getSeconds()}`.slice(-2);
+ const minutes = `0${time.getMinutes()}`.slice(-2);
+ const hours = `0${time.getHours()}`.slice(-2);
+
+ return `[${fullDate ? `${day}:${month}:${year}-` : ''}${hours}:${minutes}:${seconds}]`;
+ }
+
+ log(message: ILogMessage): void {
+ throw new Error('Method not implemented.', { cause: message });
+ }
+}
diff --git a/src/main/outputEngines/fileStorageEngine.ts b/src/main/outputEngines/fileStorageEngine.ts
new file mode 100755
index 0000000..c38f977
--- /dev/null
+++ b/src/main/outputEngines/fileStorageEngine.ts
@@ -0,0 +1,60 @@
+import { ELoggerLevel, ELoggerLevelNames, EStyles, IFileStorageSettings, ILogMessage, IMessageChunk } from '../interfaces';
+import { Logger } from '../logger';
+import { Engine } from './';
+
+export default class FileStorage extends Engine {
+ constructor(settings?: IFileStorageSettings, ...loggers: Logger[]) {
+ super(settings, ...loggers);
+ let a;
+ }
+
+ private parseTextStyles(chunk: IMessageChunk, subLine?: boolean): string {
+ let special = false;
+ chunk.styling.forEach((style) => {
+ switch (style) {
+ case EStyles.specialSubLine:
+ special = true;
+ break;
+ default:
+ break;
+ }
+ });
+ return (subLine && !special ? '| ' : '') + chunk.content;
+ }
+
+ log(message: ILogMessage): void {
+ if (!this.debug && message.logLevel === ELoggerLevel.DEBUG) return;
+
+ const defaultSettings = message.settings.default;
+
+ const timestamp = this.getTime(message.timestamp);
+
+ const prefixes = message.prefixes;
+
+ const prefixTxt = prefixes.map((prefix) => ` [${prefix}]`);
+ const level = ` ${ELoggerLevelNames[message.logLevel]}: `;
+
+ const txt = message.messageChunks.map((chunk): string => this.parseTextStyles(chunk, false));
+
+ // this.consoleLoggers[message.logLevel](`${timestamp}${prefixTxt}${level}${txt.join('')}`);
+
+ if (!message.subLines || message.subLines.length <= 0) return;
+
+ let biggestLine = 0;
+ message.subLines.forEach((line) => {
+ if (line.content.length > biggestLine) biggestLine = line.content.length;
+ // this.consoleLoggers[message.logLevel](this.parseTextStyles(line, true));
+ });
+
+ this.consoleLoggers[message.logLevel](
+ this.parseTextStyles({
+ content: '#'.padEnd(biggestLine, '-'),
+ styling: [EStyles.specialSubLine],
+ stylingParams: [''],
+ subLine: true,
+ },
+ true,
+ ),
+ );
+ }
+}
diff --git a/src/main/outputEngines/index.ts b/src/main/outputEngines/index.ts
new file mode 100755
index 0000000..cf38c86
--- /dev/null
+++ b/src/main/outputEngines/index.ts
@@ -0,0 +1,3 @@
+export * from './engine';
+export * from './consoleEngine';
+export * from './fileStorageEngine';
From 41cf13f95019cce22078a0ed4567a924e677e27d Mon Sep 17 00:00:00 2001
From: Space_Fox <44732812+emanuelfranklyn@users.noreply.github.com>
Date: Mon, 10 Jun 2024 17:13:40 -0300
Subject: [PATCH 02/10] (feat): FileSystemEngine, AutoLogEnd
Also changes ConsoleEngine styling
---
package-lock.json | 26 +-
package.json | 4 +-
src/main/autoLogEnd.ts | 159 +++++++----
src/main/defaults/standard.ts | 9 +-
src/main/index.ts | 3 +-
src/main/interfaces/IDefault.ts | 9 +-
src/main/interfaces/IFileStorageSettings.ts | 1 -
src/main/interfaces/ILogMessage.ts | 1 +
src/main/logger.ts | 288 ++++++++++++++++----
src/main/outputEngines/consoleEngine.ts | 126 +++++----
src/main/outputEngines/engine.ts | 32 ++-
src/main/outputEngines/fileStorageEngine.ts | 279 ++++++++++++++++---
src/tests/tester.js | 124 +++++++--
13 files changed, 802 insertions(+), 259 deletions(-)
mode change 100644 => 100755 package-lock.json
mode change 100644 => 100755 package.json
mode change 100644 => 100755 src/main/index.ts
mode change 100644 => 100755 src/main/interfaces/ILogMessage.ts
mode change 100644 => 100755 src/tests/tester.js
diff --git a/package-lock.json b/package-lock.json
old mode 100644
new mode 100755
index 78dda03..71abc26
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,12 +10,10 @@
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.10",
- "chalk": "^4.1.2",
- "escape-html": "^1.0.3"
+ "chalk": "^4.1.2"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
- "@types/escape-html": "^1.0.2",
"@types/node": "^18.15.3",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
@@ -163,12 +161,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/escape-html": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.2.tgz",
- "integrity": "sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA==",
- "dev": true
- },
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -806,11 +798,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
- },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3018,12 +3005,6 @@
"@types/node": "*"
}
},
- "@types/escape-html": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.2.tgz",
- "integrity": "sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA==",
- "dev": true
- },
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -3462,11 +3443,6 @@
"is-symbol": "^1.0.2"
}
},
- "escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
- },
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
diff --git a/package.json b/package.json
old mode 100644
new mode 100755
index 64a57d3..591ac8d
--- a/package.json
+++ b/package.json
@@ -26,12 +26,10 @@
"homepage": "https://github.com/PromisePending/logger.js#readme",
"dependencies": {
"adm-zip": "^0.5.10",
- "chalk": "^4.1.2",
- "escape-html": "^1.0.3"
+ "chalk": "^4.1.2"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
- "@types/escape-html": "^1.0.2",
"@types/node": "^18.15.3",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
diff --git a/src/main/autoLogEnd.ts b/src/main/autoLogEnd.ts
index dcd4943..f1c32b5 100755
--- a/src/main/autoLogEnd.ts
+++ b/src/main/autoLogEnd.ts
@@ -1,60 +1,113 @@
import { Logger } from './logger';
-import util from 'util';
-
-var exited = false;
-var logger: Logger | null = null;
-
-function exitHandler({ err, options, exitCode }: {err?: {stack: any, message: any, name: any}, options?: {uncaughtException: boolean}, exitCode?: number | string}): void {
- if (!exited) {
- process.stdin.resume();
- exited = true;
- if (typeof exitCode === 'string') {
- logger?.warn('Manually Finished!');
- } else {
- if (exitCode !== 123654) logger?.info('Program finished, code: ' + exitCode);
- if (exitCode === 123654 && options?.uncaughtException) {
- logger?.fatal(util.format(typeof err === 'string' ? err : err?.stack));
- exitCode = 1;
- } else if (exitCode && exitCode === 123654) {
- logger?.error(util.format(typeof err === 'string' ? err : err?.stack));
- logger?.warn('#===========================================================#');
- logger?.warn('| # AutoLogEnd prevent program exit!');
- logger?.warn('| # Code that is not async or would be runned after the line that generated the error cannot run as per nodejs default behavior.');
- logger?.warn('| # But promises, async code and event based functions will still be executed.');
- logger?.warn('| # In order to prevent sync code to stop, use an try-catch or a promise.');
- logger?.warn('#===========================================================#');
- logger?.warn('If you want to manually exit, you can still use control-c in the process.');
- exited = false;
- return;
+import { ConsoleEngine } from './outputEngines';
+
+/**
+ * Singleton class that handles the end of the program
+ */
+export class AutoLogEnd {
+ private active = false;
+ private exited = false;
+ private logger!: Logger;
+ // eslint-disable-next-line no-use-before-define
+ public static _instance?: AutoLogEnd;
+ private deconstructors: Map Promise> = new Map();
+
+ // callbacks
+ private exitCallback: (exitCode: number) => Promise = async (exitCode) => { await this.exitHandler({ exitCode }); };
+ private sigintCallback: (error: Error) => Promise = async (error) => { await this.exitHandler({ err: error, exitCode: 'SIGINT' }); };
+ private sigusr1Callback: (error: Error) => Promise = async (error) => { await this.exitHandler({ err: error, exitCode: 'SIGUSR1' }); };
+ private sigusr2Callback: (error: Error) => Promise = async (error) => { await this.exitHandler({ err: error, exitCode: 1 }); };
+ private sigtermCallback: (error: Error) => Promise = async (error) => { await this.exitHandler({ err: error, exitCode: 'SIGTERM' }); };
+ private uncaughtExceptionCallback: (error: Error) => Promise = async (error: Error) => { await this.exitHandler({ err: error, exitCode: 123654 }); };
+ private beforeExitCallback: (code: number) => Promise = async (code: number) => { await this.exitHandler({ exitCode: code }); };
+
+ /**
+ * @constructor
+ * @param logger (optional) custom logger to be used
+ * @returns new instance of AutoLogEnd or the existing one
+ */
+ constructor(logger?: Logger) {
+ if (AutoLogEnd._instance) return AutoLogEnd._instance;
+
+ if (logger) this.logger = logger;
+ else {
+ this.logger = new Logger({ prefixes: [{ content: 'SYSTEM', color: '#ffaa00', backgroundColor: null }] });
+ this.logger.registerListener(new ConsoleEngine({ debug: true }));
+ }
+
+ this.activate();
+
+ AutoLogEnd._instance = this;
+ }
+
+ private async exitHandler({ err, exitCode }: {err?: Error | string, exitCode?: number | string}): Promise {
+ if (!this.exited) {
+ process.stdin.resume();
+ this.exited = true;
+ if (typeof exitCode === 'string') this.logger.warn('Manually Finished!');
+ else {
+ if (exitCode !== 123654 && exitCode !== 647412) this.logger.info('Program finished, code: ' + exitCode ?? '?');
+ else if (exitCode && exitCode === 123654 && err) this.logger.error(err);
}
+ const promises: Promise[] = [];
+ this.deconstructors.forEach((deconstructor) => promises.push(deconstructor()));
+ Promise.all(promises).then(() => {
+ process.exit(typeof exitCode === 'string' ? 0 : exitCode);
+ });
}
- process.exit(typeof exitCode === 'string' ? 0 : exitCode);
}
-}
-export function activate(uncaughtException?: boolean, logger?: Logger): void {
- logger = logger ?? new Logger({ prefixes: ['SYSTEM'] });
- logger?.debug('AutoLogEnd activated!');
- process.on('exit', (exitCode) => exitHandler({ exitCode, options: { uncaughtException: false } }));
- process.on('SIGINT', (error) => { exitHandler({ err: { message: error, name: null, stack: null }, options: { uncaughtException: false }, exitCode: 'SIGINT' }); });
- process.on('SIGUSR1', (error) => { exitHandler({ err: { message: error, name: null, stack: null }, options: { uncaughtException: false }, exitCode: 'SIGUSR1' }); });
- process.on('SIGUSR2', (error) => { exitHandler({ err: { message: error, name: null, stack: null }, options: { uncaughtException: false }, exitCode: 1 }); });
- process.on('SIGTERM', (error) => { exitHandler({ err: { message: error, name: null, stack: null }, options: { uncaughtException: false }, exitCode: 'SIGTERM' }); });
- process.on('uncaughtException', (error) => {
- exitHandler({
- err: { message: error.message, name: error.name, stack: error.stack },
- options: { uncaughtException: uncaughtException ?? false },
- exitCode: 123654,
- });
- });
-}
+ /**
+ * Adds a deconstructor function to be runned before the program exits
+ * NOTE: It is uncertain that node.js will execute desconstructor functions that are async.
+ * @param id Identifier for the deconstructor
+ * @param deconstructor Function to be runned before the program exits
+ */
+ appendDeconstructor(id: string, deconstructor: () => Promise): void {
+ if (this.deconstructors.has(id)) this.logger.warn(`Deconstructor with id ${id} has overwritten!`);
+ this.deconstructors.set(id, deconstructor);
+ }
+
+ /**
+ * Removes a deconstructor function
+ * @param id Identifier for the deconstructor
+ */
+ removeDeconstructor(id: string): void {
+ if (!this.deconstructors.has(id)) return this.logger.warn(`Deconstructor with id ${id} not found!`);
+ this.deconstructors.delete(id);
+ }
+
+ /**
+ * Activates the AutoLogEnd
+ * @returns void
+ **/
+ activate(): void {
+ if (this.active) return;
+ process.on('exit', this.exitCallback);
+ process.on('SIGINT', this.sigintCallback);
+ process.on('SIGUSR1', this.sigusr1Callback);
+ process.on('SIGUSR2', this.sigusr2Callback);
+ process.on('SIGTERM', this.sigtermCallback);
+ process.on('uncaughtException', this.uncaughtExceptionCallback);
+ process.on('beforeExit', this.beforeExitCallback);
+ this.active = true;
+ this.logger.debug('AutoLogEnd activated!');
+ }
-export function deactivate(): void {
- process.removeListener('exit', exitHandler);
- process.removeListener('SIGINT', exitHandler);
- process.removeListener('SIGUSR1', exitHandler);
- process.removeListener('SIGUSR2', exitHandler);
- process.removeListener('SIGTERM', exitHandler);
- process.removeListener('uncaughtException', exitHandler);
- logger?.debug('AutoLogEnd deactivated!');
+ /**
+ * Deactivates the AutoLogEnd
+ * @returns void
+ **/
+ deactivate(): void {
+ if (!this.activate) return;
+ process.removeListener('exit', this.exitCallback);
+ process.removeListener('SIGINT', this.sigintCallback);
+ process.removeListener('SIGUSR1', this.sigusr1Callback);
+ process.removeListener('SIGUSR2', this.sigusr2Callback);
+ process.removeListener('SIGTERM', this.sigtermCallback);
+ process.removeListener('uncaughtException', this.uncaughtExceptionCallback);
+ process.removeListener('beforeExit', this.beforeExitCallback);
+ this.active = false;
+ this.logger.debug('AutoLogEnd deactivated!');
+ }
}
diff --git a/src/main/defaults/standard.ts b/src/main/defaults/standard.ts
index 1133e86..d64d6c9 100755
--- a/src/main/defaults/standard.ts
+++ b/src/main/defaults/standard.ts
@@ -19,9 +19,16 @@ export default {
prefixMainColor: '#777777',
prefixAccentColor: '#000000',
redactionText: '[REDACTED]',
- undefinedColor: '#5555aa',
causedByTextColor: '#ffffff',
causedByBackgroundColor: '#ff0000',
variableStyling: [EStyles.bold, EStyles.textColor],
variableStylingParams: ['', '#55ff55'],
+ primitiveColors: {
+ string: '#ff5555',
+ number: '#55ff55',
+ boolean: '#5555ff',
+ null: '#555555',
+ undefined: '#005500',
+ circular: '#ff5555',
+ },
} as IDefault;
diff --git a/src/main/index.ts b/src/main/index.ts
old mode 100644
new mode 100755
index 69f8767..06624be
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1,3 +1,4 @@
-export * as autoLogEnd from './autoLogEnd';
+export * from './autoLogEnd';
export * from './interfaces';
export * from './logger';
+export * from './outputEngines';
diff --git a/src/main/interfaces/IDefault.ts b/src/main/interfaces/IDefault.ts
index 5a1239c..4ca9c5a 100755
--- a/src/main/interfaces/IDefault.ts
+++ b/src/main/interfaces/IDefault.ts
@@ -10,9 +10,16 @@ export interface IDefault {
prefixMainColor: string,
prefixAccentColor: string,
redactionText: string,
- undefinedColor: string,
causedByTextColor?: string,
causedByBackgroundColor?: string,
variableStyling: EStyles[],
variableStylingParams: string[],
+ primitiveColors: {
+ string: string,
+ number: string,
+ boolean: string,
+ null: string,
+ undefined: string,
+ circular: string,
+ },
}
diff --git a/src/main/interfaces/IFileStorageSettings.ts b/src/main/interfaces/IFileStorageSettings.ts
index ebacf50..5c5dbcf 100755
--- a/src/main/interfaces/IFileStorageSettings.ts
+++ b/src/main/interfaces/IFileStorageSettings.ts
@@ -2,7 +2,6 @@ import { IEngineSettings } from './';
export interface IFileStorageSettings extends IEngineSettings {
logFolderPath: string;
- enableLatestLog?: boolean;
enableDebugLog?: boolean;
enableErrorLog?: boolean;
enableFatalLog?: boolean;
diff --git a/src/main/interfaces/ILogMessage.ts b/src/main/interfaces/ILogMessage.ts
old mode 100644
new mode 100755
index 5c741de..e31230a
--- a/src/main/interfaces/ILogMessage.ts
+++ b/src/main/interfaces/ILogMessage.ts
@@ -32,6 +32,7 @@ export interface IMessageChunk {
styling: EStyles[];
stylingParams: string[];
subLine: boolean;
+ breaksLine: boolean;
}
export interface ILogMessage {
diff --git a/src/main/logger.ts b/src/main/logger.ts
index 8c2529c..2e15a63 100755
--- a/src/main/logger.ts
+++ b/src/main/logger.ts
@@ -5,6 +5,9 @@ import utils from 'util';
import { IDefault } from './interfaces/IDefault';
import Standard from './defaults/standard';
+/**
+ * Main class that will process logs before automaticly sending then to registered Engines
+ */
export class Logger {
private defaultLevel: ELoggerLevel = ELoggerLevel.INFO;
private prefixes: IPrefix[];
@@ -15,6 +18,25 @@ export class Logger {
private allLineColored: boolean;
private defaultSettings: IDefault;
+ /**
+ * @constructor
+ * @param param0 ILoggerOptions object:
+ * - prefixes: array of strings or IPrefix objects (can be empty list)
+ * - - IPrefix: object containing `content` field for the text of the prefix, a `color` field that can be a function that receives the text as input and returns a hex color string array for each letter, a array with hex codes for each letter, or a simple hex color string for the whole text, and a `backgroundColor` field that behaves like the color param
+ * - defaultLevel?: optional value from ELoggerLevel enum, determines what kind of log the .log method will execute (default: info)
+ * - disableFatalCrash?: optional value that when set true disables exiting the process when a fatal log happens (default: false)
+ * - redactedContent?: optional list of regex strings that when any match will replace the text with the redacted message
+ * - allLineColored?: optional boolean that sets if the content of the message should be colored the same as the log level color (default: false)
+ * - coloredBackground?: optional boolean that sets if the log level color will be applied to the background instead of the text (default: false)
+ * - defaultSettings?: optional IDefault object containing the colors and redacted text to be used, see /defaults/standard.ts file (default: /defaults/standard.ts file)
+ * @example
+ * const logger = new Logger({
+ * prefix: ['example'],
+ * allLineColored: true,
+ * });
+ * new ConsoleEngine(logger);
+ * logger.info('Hi!');
+ */
constructor({ prefixes, defaultLevel, disableFatalCrash, redactedContent, allLineColored, coloredBackground, defaultSettings }: ILoggerOptions) {
this.defaultLevel = defaultLevel ?? ELoggerLevel.INFO;
this.disableFatalCrash = disableFatalCrash ?? false;
@@ -55,6 +77,47 @@ export class Logger {
return modifiedString;
}
+ private colorPrimitiveValue(text: string): { text: string, colorStyles: EStyles[], colorStylesValues: string[] } {
+ let color = null;
+ if (!isNaN(Number(text))) color = this.defaultSettings.primitiveColors.number;
+ else if (text === 'null') color = this.defaultSettings.primitiveColors.null;
+ else if (text === 'undefined') color = this.defaultSettings.primitiveColors.undefined;
+ else if (text === 'true' || text === 'false') color = this.defaultSettings.primitiveColors.boolean;
+ else if (
+ (text.startsWith('"') && text.endsWith('"')) || (text.startsWith('\'') && text.endsWith('\'')) || (text.startsWith('`') && text.endsWith('`'))
+ ) color = this.defaultSettings.primitiveColors.string;
+ else if (text.includes('Circular') || text.includes('ref')) color = this.defaultSettings.primitiveColors.circular;
+ else if (text.toLowerCase().includes('info')) color = this.defaultSettings.logLevelMainColors[ELoggerLevel.INFO];
+ else if (text.toLowerCase().includes('warn')) color = this.defaultSettings.logLevelMainColors[ELoggerLevel.WARN];
+ else if (text.toLowerCase().includes('error')) color = this.defaultSettings.logLevelMainColors[ELoggerLevel.ERROR];
+ else if (text.toLowerCase().includes('debug')) color = this.defaultSettings.logLevelMainColors[ELoggerLevel.DEBUG];
+ return {
+ text,
+ colorStyles: color ? [EStyles.textColor] : [],
+ colorStylesValues: color ? [color] : [],
+ };
+ }
+
+ private colorPrimitive(text: string): { text: string, colorStyles: EStyles[], colorStylesValues: string[] }[] {
+ // split text by certain characters
+ const splitCharsNonScape = [' ', ',', ':', '<', '>'];
+ const splitCharsScape = ['*', '(', ')', '[', ']'];
+ const result: { text: string, colorStyles: EStyles[], colorStylesValues: string[] }[] = [];
+ let elementGroupBuffer = '';
+ text.split(RegExp(`(\\${splitCharsScape.join('|\\')}|${splitCharsNonScape.join('|')})`, 'gu')).forEach((element) => {
+ if ([...splitCharsNonScape, ...splitCharsScape].includes(element)) elementGroupBuffer += element;
+ else {
+ if (elementGroupBuffer !== '') {
+ result.push({ text: elementGroupBuffer, colorStyles: [], colorStylesValues: [] });
+ elementGroupBuffer = '';
+ }
+ const { text: coloredText, colorStyles, colorStylesValues } = this.colorPrimitiveValue(element);
+ result.push({ text: coloredText, colorStyles, colorStylesValues });
+ }
+ });
+ return result;
+ }
+
/**
* Parses the message and returns an array of IMessageChunk objects
* the first IMessageChunk object is the main message, the rest are subLines
@@ -62,55 +125,71 @@ export class Logger {
* @param args - The arguments to be passed to the content
* @returns IMessageChunk[]
*/
- private processMessage(text: string | string[] | Error, forceSubline: boolean, ...args: any[]): IMessageChunk[] {
- if (!text) {
- return [{
- content: 'undefined',
- styling: [EStyles.textColor],
- stylingParams: [this.defaultSettings.undefinedColor],
- subLine: forceSubline,
- }];
- }
+ private processMessage(text: any, forceSubline: boolean, ...args: any[]): IMessageChunk[] {
+ if (!text) text = 'undefined';
// String handling
if (typeof text !== 'object') {
- return [...text.toString().split('\n').map((line) => {
- return {
- content: this.redactText(utils.format(line, ...args)),
- styling: [],
- stylingParams: [],
- subLine: forceSubline,
- };
- })];
+ const texts: string[] = [];
+ const otherKinds: any[] = [];
+ args.map((arg) => typeof arg === 'string' ? texts.push(arg) : otherKinds.push(arg));
+ const processedOtherKinds: IMessageChunk[] = [];
+ otherKinds.map((otherElement) => this.processMessage(otherElement, true)).forEach((otherPElement) => {
+ otherPElement.map((chunk) => processedOtherKinds.push(chunk));
+ });
+ const processedTexts: IMessageChunk[] = [];
+ (text.toString() as string).split('\n').forEach((line: string, index: number) => {
+ if (!Array.isArray(args)) args = [args];
+ const processedColors = this.colorPrimitive(utils.format(line, ...texts));
+ processedColors.forEach((color, colorIndex) => {
+ processedTexts.push({
+ content: this.redactText(color.text),
+ styling: color.colorStyles,
+ stylingParams: color.colorStylesValues,
+ subLine: index === 0 ? forceSubline : true,
+ breaksLine: (index === 0 ? forceSubline : true) && colorIndex === 0,
+ });
+ });
+ });
+ return [...processedTexts, ...processedOtherKinds];
// Error handling
} else if (text instanceof Error) {
- const finalMessage = [];
- finalMessage.push({
- content: (forceSubline ? 'Error: ' : '') + this.redactText(utils.format(text.message, ...args)),
- styling: [],
- stylingParams: [],
- subLine: forceSubline,
+ const finalMessage: IMessageChunk[] = [];
+ const processedColors = this.colorPrimitive(utils.format((forceSubline ? 'Error: ' : '') + text.message.trim(), ...args));
+ processedColors.forEach((color, colorIndex) => {
+ finalMessage.push({
+ content: this.redactText(color.text),
+ styling: color.colorStyles,
+ stylingParams: color.colorStylesValues,
+ subLine: forceSubline,
+ breaksLine: colorIndex === 0 && forceSubline,
+ });
});
const stack = text.stack?.split('\n');
stack?.shift();
stack?.forEach((line) => {
- finalMessage.push({
- content: this.redactText(line.trim()),
- styling: [],
- stylingParams: [],
- subLine: true,
+ const processedColors = this.colorPrimitive(utils.format(line.trim()));
+ processedColors.forEach((color, colorIndex) => {
+ finalMessage.push({
+ content: this.redactText(color.text),
+ styling: color.colorStyles,
+ stylingParams: color.colorStylesValues,
+ subLine: true,
+ breaksLine: colorIndex === 0,
+ });
});
});
if (text.cause) {
- const causedBy = {
+ const causedBy: IMessageChunk = {
content: '# Caused by:',
styling: [EStyles.specialSubLine],
stylingParams: [''],
subLine: true,
+ breaksLine: true,
};
if (this.defaultSettings.causedByBackgroundColor) {
@@ -123,26 +202,17 @@ export class Logger {
}
finalMessage.push(causedBy);
-
- if (typeof text.cause === 'string' || Array.isArray(text.cause) || text.cause instanceof Error) {
- finalMessage.push(...this.processMessage(text.cause, true, ...args));
- } else {
- finalMessage.push({
- content: this.redactText(JSON.stringify(text.cause)),
- styling: [],
- stylingParams: [],
- subLine: true,
- });
- }
+ finalMessage.push(...this.processMessage(text.cause, true, ...args));
}
return finalMessage;
} else if (!Array.isArray(text) || (Array.isArray(text) && (!args || args.length === 0))) {
- return [{
- content: this.redactText(utils.format(text, ...args)),
- styling: [EStyles.specialSubLine, EStyles.reset],
- stylingParams: ['', ''],
- subLine: true,
- }];
+ const processedArgs: IMessageChunk[] = [];
+ if (args.length > 0) {
+ args.map((arg) => this.processMessage(arg, true)).forEach((processedArg) => {
+ processedArg.map((chunk) => processedArgs.push(chunk));
+ });
+ }
+ return [...this.processMessage(utils.format(text), forceSubline), ...processedArgs];
}
const finalMessage: (IMessageChunk & {subLine: boolean})[] = [];
@@ -156,6 +226,7 @@ export class Logger {
styling: [EStyles.specialSubLine],
stylingParams: [''],
subLine: switchedToSublines,
+ breaksLine: false,
});
}
if (variable) {
@@ -170,7 +241,7 @@ export class Logger {
return finalMessage;
}
- private handleMessage(text: string | string[] | Error, level: ELoggerLevel, ...args: any[]): void {
+ private handleMessage(text: any, level: ELoggerLevel, ...args: any[]): void {
const chunks = this.processMessage(text, false, ...args);
const messageChunks: IMessageChunk[] = [];
const subLines: IMessageChunk[] = [];
@@ -184,7 +255,7 @@ export class Logger {
}
if (this.allLineColored) {
const txtColor = this.coloredBackground ? this.defaultSettings.logLevelAccentColors[level] : this.defaultSettings.logLevelMainColors[level];
- const bgColor = this.coloredBackground ? this.defaultSettings.logLevelMainColors[level] : this.defaultSettings.logLevelAccentColors[level];
+ const bgColor = this.coloredBackground ? this.defaultSettings.logLevelMainColors[level] : undefined;
if (txtColor) {
chunk.styling.unshift(EStyles.textColor);
chunk.stylingParams.unshift(txtColor);
@@ -198,6 +269,10 @@ export class Logger {
}
});
+ // prevents errors where a message would have no content, only sublines
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ if (messageChunks.length === 0 && subLines.length > 0) messageChunks.push(subLines.shift()!);
+
const message: ILogMessage = {
messageChunks,
subLines,
@@ -215,54 +290,153 @@ export class Logger {
});
}
- log(text: string | string[], ...args: any[]): void {
+ /**
+ * Logs a message using the default level
+ * @param text A string, list of strings or Error object to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.log('Hello, world!'); // Logs 'Hello, world!' with the default level
+ * logger.log`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.log`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.log(errorObject); // logs '' followed with the stacktrace
+ */
+ log(text: any, ...args: any[]): void {
this.handleMessage(text, this.defaultLevel, ...args);
}
- info(text: string | string[], ...args: any[]): void {
+ /**
+ * Logs a message using the info level
+ * @param text A string, list of strings or Error object to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.info('Hello, world!'); // Logs 'Hello, world!' with the info level
+ * logger.info`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.info`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.info(errorObject); // logs '' followed with the stacktrace
+ */
+ info(text: any, ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.INFO, ...args);
}
- warn(text: string | string[], ...args: any[]): void {
+ /**
+ * Logs a message using the warn level
+ * @param text A string, list of strings or Error object to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.warn('Hello, world!'); // Logs 'Hello, world!' with the warn level
+ * logger.warn`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.warn`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.warn(errorObject); // logs '' followed with the stacktrace
+ */
+ warn(text: any, ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.WARN, ...args);
}
- error(text: string | string[] | Error, ...args: any[]): void {
+ /**
+ * Logs a message using the error level
+ * @param text A string, list of strings or Error object to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.error('Hello, world!'); // Logs 'Hello, world!' with the error level
+ * logger.error`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.error`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.error(errorObject); // logs '' followed with the stacktrace
+ */
+ error(text: any, ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.ERROR, ...args);
}
- debug(text: string | string[], ...args: any[]): void {
+ /**
+ * Logs a message using the debug level, this level is only logged if the debug mode is enabled
+ * @param text A string or list of strings to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.debug('Hello, world!'); // Logs 'Hello, world!' with the debug level
+ * logger.debug`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.debug`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.debug(errorObject); // logs '' followed with the stacktrace
+ */
+ debug(text: any, ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.DEBUG, ...args);
}
- fatal(text: string | string[] | Error, ...args: any[]): void {
+ /**
+ * Logs a message using the fatal level, will stop the execution of the program unless disableFatalCrash is set to true
+ * @param text A string or list of strings to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.fatal('Hello, world!'); // Logs 'Hello, world!' with the fatal level
+ * logger.fatal`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.fatal`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.fatal(errorObject); // logs '' followed with the stacktrace
+ */
+ fatal(text: any, ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.FATAL, ...args);
+ if (!this.disableFatalCrash) process.exit(647412);
}
+ /**
+ * Logs a message using the Warning level
+ * @param text A string or list of strings to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.alert('Hello, world!'); // Logs 'Hello, world!' with the warning level
+ * logger.alert`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.alert`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.alert(errorObject); // logs '' followed with the stacktrace
+ */
alert(text: string | string[], ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.ALERT, ...args);
}
- severe(text: string | string[] | Error, ...args: any[]): void {
+ /**
+ * Logs a message using the Error level
+ * @param text A string or list of strings to be logged
+ * @param args A list of arguments to be passed to the text
+ * @example
+ * logger.severe('Hello, world!'); // Logs 'Hello, world!' with the Error level
+ * logger.severe`Hello, ${name}!`; // Logs 'Hello, !' where name will be styled as a variable
+ * logger.severe`Hello, ${errorObject}`; // logs 'Hello, ' followed with the stacktrace
+ * logger.severe(errorObject); // logs '' followed with the stacktrace
+ */
+ severe(text: any, ...args: any[]): void {
this.handleMessage(text, ELoggerLevel.SEVERE, ...args);
}
/**
- * Allows the assignment of a listener callback that will be called every log made
- * @param listenerCallback void function with the actions to be executed every log. Receives ILogMessage object.
+ * Registers an engine listener to this logger
+ * @param listenerEngine The engine to be registered
*/
registerListener(listenerEngine: Engine): void {
this.logListeners.push(listenerEngine);
}
+ /**
+ * Unregisters an engine listener from this logger
+ * @param listenerEngine The engine to be unregistered
+ */
unRegisterListener(listenerEngine: Engine): void {
this.logListeners = this.logListeners.filter((listener) => listener !== listenerEngine);
}
+ /**
+ * Sets the colored background state for this logger
+ * @param coloredBackground The new state for colored background
+ * @example
+ * logger.setColoredBackground(true); // All logs will have colored background from now on
+ * logger.setColoredBackground(false); // All logs will not have a background from now on
+ */
setColoredBackground(coloredBackground: boolean): void {
this.coloredBackground = coloredBackground;
}
+ /**
+ * Sets the all line colored state for this logger
+ * @param allLineColored The new state for all line colored
+ * @example
+ * logger.setAllLineColored(true); // The content of the logs will be colored from now on
+ * logger.setAllLineColored(false); // Only the information of the logs will be colored while the actual content will stay unchanged from now on
+ */
setAllLineColored(allLineColored: boolean): void {
this.allLineColored = allLineColored;
}
diff --git a/src/main/outputEngines/consoleEngine.ts b/src/main/outputEngines/consoleEngine.ts
index 983368e..6341bc9 100755
--- a/src/main/outputEngines/consoleEngine.ts
+++ b/src/main/outputEngines/consoleEngine.ts
@@ -14,51 +14,40 @@ export class ConsoleEngine extends Engine {
};
private parseTextStyles(chunk: IMessageChunk, subLine?: boolean, backgroundColor?: string, customFGColor?: string): string {
- let finalMsg = subLine ? (backgroundColor ? chalk.bgHex(backgroundColor).gray : chalk.gray) : chalk.reset;
- let special = false;
+ let textStyler = subLine ? (backgroundColor ? chalk.bgHex(backgroundColor).gray : chalk.gray) : chalk.reset;
chunk.styling.forEach((style, index) => {
switch (style) {
case EStyles.bold:
- finalMsg = finalMsg.bold;
+ textStyler = textStyler.bold;
break;
case EStyles.italic:
- finalMsg = finalMsg.italic;
+ textStyler = textStyler.italic;
break;
case EStyles.backgroundColor:
- finalMsg = finalMsg.bgHex(chunk.stylingParams[index]);
+ textStyler = textStyler.bgHex(chunk.stylingParams[index]);
break;
case EStyles.textColor:
- finalMsg = finalMsg.hex(chunk.stylingParams[index]);
- break;
- case EStyles.specialSubLine:
- special = true;
+ textStyler = textStyler.hex(chunk.stylingParams[index]);
break;
case EStyles.reset:
- finalMsg = finalMsg.reset;
+ textStyler = textStyler.reset;
break;
default:
break;
}
});
- let finalMessage = '';
- const fullLineTxt = chunk.content.padEnd(process.stdout.columns - (subLine && !special ? 3 : 0));
- if (subLine && !special) {
- finalMessage += (customFGColor ? (backgroundColor ? chalk.bgHex(backgroundColor) : chalk).hex(customFGColor)('| ') : finalMsg('| '));
- finalMessage += (customFGColor ? finalMsg.hex(customFGColor)(fullLineTxt) : finalMsg(fullLineTxt));
- } else if (subLine && special) {
- finalMessage += finalMsg(fullLineTxt);
- } else {
- finalMessage += (customFGColor ? chalk.hex(customFGColor)(finalMsg(chunk.content)) : finalMsg(chunk.content));
- }
- return finalMessage;
+ const txt = subLine ? (`${!chunk.styling.includes(EStyles.specialSubLine) && chunk.breaksLine ? '| ' : ''}${chunk.content}`) : chunk.content;
+ return (customFGColor ? textStyler.hex(customFGColor)(txt) : textStyler(txt));
}
- private parsePrefix(prefixes: IPrefix[], defaultBg?: string): (string | undefined)[] {
+ private parsePrefix(prefixes: IPrefix[], defaultBg?: string, dontSaveCache?: boolean): (string | undefined)[] {
return prefixes.map((prefix) => {
+ // if theres cache for this prefix return it
if (this.prefixes.has(prefix.content)) return this.prefixes.get(prefix.content);
- let bgColor = '';
- let bgColorArray: string[] = [];
+ // calculates the backgroundColor for the prefix
+ let bgColor = ''; // used if single color background color
+ let bgColorArray: string[] = []; // used if multiple background colors
if (prefix.backgroundColor && !Array.isArray(prefix.backgroundColor)) {
if (typeof prefix.backgroundColor === 'function') {
const result = prefix.backgroundColor(prefix.content);
@@ -71,8 +60,9 @@ export class ConsoleEngine extends Engine {
bgColorArray = prefix.backgroundColor;
}
- let fgColor = '';
- let fgArray: string[] = [];
+ // calculates the text color for the prefix
+ let fgColor = ''; // used if single color
+ let fgArray: string[] = []; // used if different colors for different characters
if (!Array.isArray(prefix.color)) {
if (typeof prefix.color === 'function') {
const result = prefix.color(prefix.content);
@@ -88,12 +78,12 @@ export class ConsoleEngine extends Engine {
// static colors
if (bgColor && fgColor) {
const result = chalk.bgHex(bgColor).hex(fgColor)(prefix.content);
- this.prefixes.set(prefix.content, result);
+ if (!dontSaveCache) this.prefixes.set(prefix.content, result);
return result;
}
if (!bgColor && bgColorArray.length <= 0 && fgColor) {
const result = (defaultBg ? chalk.bgHex(defaultBg) : chalk).hex(fgColor)(prefix.content);
- this.prefixes.set(prefix.content, result);
+ if (!dontSaveCache) this.prefixes.set(prefix.content, result);
return result;
}
@@ -119,38 +109,37 @@ export class ConsoleEngine extends Engine {
else finalMsg += (defaultBg ? chalk.bgHex(defaultBg) : chalk).hex(color)(prefix.content[index]);
});
}
- this.prefixes.set(prefix.content, finalMsg);
+
+ // saves to cache
+ if (!dontSaveCache) this.prefixes.set(prefix.content, finalMsg);
return finalMsg;
});
}
+ /**
+ * Logs a message to the console
+ * @param message The message to be logged
+ * @returns void
+ */
log(message: ILogMessage): void {
if (!this.debug && message.logLevel === ELoggerLevel.DEBUG) return;
const defaultSettings = message.settings.default;
- const shouldColorBg = message.settings.coloredBackground || message.logLevel === ELoggerLevel.FATAL;
+ const isFatal = message.logLevel === ELoggerLevel.FATAL;
+ const shouldColorBg = message.settings.coloredBackground || isFatal;
+ const currentMainColor = shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined;
+ const currentAccentColor = shouldColorBg ? defaultSettings.logLevelAccentColors[message.logLevel] : undefined;
+ // clears prefixes cache in order to apply correct background color
if (shouldColorBg) this.prefixes.clear();
- let formatter: chalk.Chalk = chalk.reset;
- if (shouldColorBg) formatter = formatter.bgHex(defaultSettings.logLevelMainColors[message.logLevel]);
-
- if (shouldColorBg && defaultSettings.logLevelAccentColors[message.logLevel]) formatter = formatter.hex(defaultSettings.logLevelAccentColors[message.logLevel]);
+ let styleText = currentMainColor ? chalk.bgHex(currentMainColor) : chalk.reset;
+ styleText = !shouldColorBg ? styleText.hex(defaultSettings.logLevelMainColors[message.logLevel]) : (currentAccentColor ? styleText.hex(currentAccentColor) : styleText);
- if (!message.settings.coloredBackground && message.logLevel !== ELoggerLevel.FATAL) formatter = formatter.hex(defaultSettings.logLevelMainColors[message.logLevel]);
- const timestamp = formatter(this.getTime(message.timestamp));
+ const timestamp = styleText(this.getTime(message.timestamp));
- const prefixes = this.parsePrefix(message.prefixes, shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined);
-
- if (message.logLevel === ELoggerLevel.FATAL) this.prefixes.clear();
-
- let prefixTxt = '';
- prefixes.forEach((prefix) => {
- prefixTxt += formatter(' [');
- prefixTxt += prefix;
- prefixTxt += formatter(']');
- });
- const level = formatter(` ${ELoggerLevelNames[message.logLevel]}:`);
+ const prefixes = this.parsePrefix(message.prefixes, currentMainColor, isFatal).map((prefix) => `${styleText(' [')}${prefix}${styleText(']')}`).join('');
+ const logKind = styleText(` ${ELoggerLevelNames[message.logLevel]}:`);
// adds a space before the first chunk to separate the message from the : in the log without coloring the background if allLineColored is false
const firstChunk = message.messageChunks[0];
@@ -159,33 +148,50 @@ export class ConsoleEngine extends Engine {
styling: firstChunk.styling,
stylingParams: firstChunk.stylingParams,
subLine: false,
+ breaksLine: false,
});
- const txt = message.messageChunks.map((chunk): string => this.parseTextStyles(chunk, false, shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined));
+ const parsedText = message.messageChunks.map((chunk): string => this.parseTextStyles(chunk, false, currentMainColor)).join('');
- this.consoleLoggers[message.logLevel](`${timestamp}${prefixTxt}${level}${txt.join('')}`);
+ // writes the final log into the console
+ this.consoleLoggers[message.logLevel](`${timestamp}${prefixes}${logKind}${parsedText}`);
if (!message.subLines || message.subLines.length <= 0) return;
- message.subLines.forEach((line) =>
- this.consoleLoggers[message.logLevel](this.parseTextStyles(
- line,
- true,
- shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined,
- shouldColorBg ? defaultSettings.logLevelAccentColors[message.logLevel] : undefined,
- )),
- );
+ const subLinesBuffer: string[] = [];
+ let lineBuffer = '';
+ let lineSizeBuffer = 0;
+ message.subLines.forEach((line, index, arr) => {
+ lineBuffer += this.parseTextStyles(line, true, currentMainColor, currentAccentColor);
+ lineSizeBuffer += line.content.length;
+ if (arr[index + 1]?.breaksLine || index === arr.length - 1) {
+ const spaceFill = ''.padEnd(process.stdout.columns - lineSizeBuffer - 3);
+ subLinesBuffer.push(lineBuffer + this.parseTextStyles({
+ content: spaceFill,
+ styling: line.styling,
+ stylingParams: line.stylingParams,
+ subLine: true,
+ breaksLine: false,
+ }, true, currentMainColor, currentAccentColor));
+ lineBuffer = '';
+ lineSizeBuffer = 0;
+ }
+ });
+ this.consoleLoggers[message.logLevel](subLinesBuffer.join('\n'));
+ // prints an indication at the end of the sublines
this.consoleLoggers[message.logLevel](
this.parseTextStyles(
{
content: '#'.padEnd(process.stdout.columns, '-'),
- styling: [EStyles.specialSubLine, EStyles.textColor],
- stylingParams: ['', defaultSettings.logLevelAccentColors[message.logLevel] || '#ffffff'],
+ styling: [EStyles.specialSubLine],
+ stylingParams: [''],
subLine: true,
+ breaksLine: false,
},
true,
- shouldColorBg ? defaultSettings.logLevelMainColors[message.logLevel] : undefined,
+ currentMainColor,
+ currentAccentColor,
),
);
}
diff --git a/src/main/outputEngines/engine.ts b/src/main/outputEngines/engine.ts
index 351a258..3af90a4 100755
--- a/src/main/outputEngines/engine.ts
+++ b/src/main/outputEngines/engine.ts
@@ -1,15 +1,27 @@
import { ILogMessage, IEngineSettings } from '../interfaces';
import { Logger } from '../logger';
+/**
+ * Engine
+ * @description Abstract class that all engines should extend
+ * @abstract
+ * @class Engine
+ **/
export abstract class Engine {
+ // debug mode
debug: boolean;
- private loggers: Logger[] = [];
+ // list of loggers that this engine is listening to
+ loggers: Logger[] = [];
constructor(settings?: IEngineSettings, ...loggers: Logger[]) {
if (Array.isArray(settings)) {
loggers = settings;
settings = undefined;
}
+ if (settings instanceof Logger) {
+ loggers = [settings];
+ settings = undefined;
+ }
this.debug = settings?.debug || false;
this.loggers = loggers;
@@ -19,12 +31,24 @@ export abstract class Engine {
});
}
+ /**
+ * Deconstructs the engine
+ */
destroy(): void {
this.loggers.forEach((logger) => {
logger.unRegisterListener(this);
});
}
+ /**
+ * Converts a Date object to a string that can be used on logs
+ * @param time Date object with the time to be converted
+ * @param fullDate Boolean to indicate if result string should include days, months and years
+ * @returns {string} The formatted time
+ * @example
+ * getTime(new Date(), true); // [2024:07:01-12:00:00]
+ * getTime(new Date(), false); // [12:00:00]
+ */
getTime(time: Date, fullDate?: boolean): string {
const day = `0${time.getDate()}`.slice(-2);
const month = `0${time.getMonth()}`.slice(-2);
@@ -33,9 +57,13 @@ export abstract class Engine {
const minutes = `0${time.getMinutes()}`.slice(-2);
const hours = `0${time.getHours()}`.slice(-2);
- return `[${fullDate ? `${day}:${month}:${year}-` : ''}${hours}:${minutes}:${seconds}]`;
+ return `[${fullDate ? `${year}:${month}:${day}-` : ''}${hours}:${minutes}:${seconds}]`;
}
+ /**
+ * logs a message
+ * @param message The message to be logged
+ */
log(message: ILogMessage): void {
throw new Error('Method not implemented.', { cause: message });
}
diff --git a/src/main/outputEngines/fileStorageEngine.ts b/src/main/outputEngines/fileStorageEngine.ts
index c38f977..2373ece 100755
--- a/src/main/outputEngines/fileStorageEngine.ts
+++ b/src/main/outputEngines/fileStorageEngine.ts
@@ -1,60 +1,269 @@
import { ELoggerLevel, ELoggerLevelNames, EStyles, IFileStorageSettings, ILogMessage, IMessageChunk } from '../interfaces';
import { Logger } from '../logger';
import { Engine } from './';
+import path from 'path';
+import fs from 'fs';
+import AdmZip from 'adm-zip';
+import { AutoLogEnd } from '../autoLogEnd';
-export default class FileStorage extends Engine {
- constructor(settings?: IFileStorageSettings, ...loggers: Logger[]) {
+/**
+ * FileStorageEngine
+ * @description Engine that allows logs to be saved to files on the disk
+ * @extends Engine
+ */
+export class FileStorageEngine extends Engine {
+ private uuid: string;
+ private engineSettings: IFileStorageSettings;
+ private latestLogStream: fs.WriteStream | null = null;
+ private debugLogStream: fs.WriteStream | null = null;
+ private errorLogStream: fs.WriteStream | null = null;
+ private fatalLogStream: fs.WriteStream | null = null;
+ private debugLogFolderPath: string;
+ private errorLogFolderPath: string;
+ private fatalLogFolderPath: string;
+ private logQueue: {txt: string, level: ELoggerLevel}[] = [];
+ private logQueueRunning = false;
+
+ constructor(settings: IFileStorageSettings, ...loggers: Logger[]) {
super(settings, ...loggers);
- let a;
+ if (!settings) throw new Error('settings is required');
+ this.engineSettings = settings;
+ this.engineSettings.logFolderPath = path.resolve(this.engineSettings.logFolderPath); // resolve path to absolute path
+ this.debugLogFolderPath = path.resolve(this.engineSettings.logFolderPath, 'debug');
+ this.errorLogFolderPath = path.resolve(this.engineSettings.logFolderPath, 'error');
+ this.fatalLogFolderPath = path.resolve(this.engineSettings.logFolderPath, 'fatal');
+
+ // generate a uuid for this instance
+ this.uuid = 'FSEngine-' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
+ AutoLogEnd._instance?.appendDeconstructor(this.uuid, async () => { await this.destroy(); });
+
+ // check if logFolderPath exists
+ if (!this.engineSettings.logFolderPath) throw new Error('logFolderPath is required');
+ if (fs.existsSync(this.engineSettings.logFolderPath)) {
+ if (!fs.lstatSync(this.engineSettings.logFolderPath).isDirectory()) throw new Error('logFolderPath is not a directory');
+ // create subfolder if it doesnt exist
+ if (!fs.existsSync(this.debugLogFolderPath)) fs.mkdirSync(this.debugLogFolderPath, { recursive: true });
+ if (!fs.existsSync(this.errorLogFolderPath)) fs.mkdirSync(this.errorLogFolderPath, { recursive: true });
+ if (!fs.existsSync(this.fatalLogFolderPath)) fs.mkdirSync(this.fatalLogFolderPath, { recursive: true });
+
+ // check if theres a latest.log file and rename it to a timestamp
+ const date = new Date();
+ const timestamp = date.toISOString().replace(/:/g, '-').replace(/\./g, '-').replace('T', '-').replace('Z', '');
+ if (fs.existsSync(path.resolve(this.engineSettings.logFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.engineSettings.logFolderPath, 'latest.log'), path.resolve(this.engineSettings.logFolderPath, `${timestamp}.log`));
+ }
+ if (fs.existsSync(path.resolve(this.debugLogFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.debugLogFolderPath, 'latest.log'), path.resolve(this.debugLogFolderPath, `${timestamp}.log`));
+ }
+ if (fs.existsSync(path.resolve(this.errorLogFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.errorLogFolderPath, 'latest.log'), path.resolve(this.errorLogFolderPath, `${timestamp}.log`));
+ }
+ if (fs.existsSync(path.resolve(this.fatalLogFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.fatalLogFolderPath, 'latest.log'), path.resolve(this.fatalLogFolderPath, `${timestamp}.log`));
+ }
+
+ if (this.engineSettings.compressLogFilesAfterNewExecution) {
+ // compress log files
+ const zipperInstance = new AdmZip();
+ const files = fs.readdirSync(this.engineSettings.logFolderPath);
+ // get the only .log file
+ const logFile = files.filter((file) => file.endsWith('.log'))[0];
+ if (logFile) {
+ const logTimestamp = logFile.split('.')[0];
+ // add all files and folders to the zip
+ zipperInstance.addLocalFolder(this.engineSettings.logFolderPath, '', (filename) => !filename.endsWith('.zip'));
+ // save the zip file
+ zipperInstance.writeZip(`${this.engineSettings.logFolderPath}/${logTimestamp}.zip`);
+ // remove all .log files recursively
+ const removeLogFiles = (folderPath: string): void => {
+ const files = fs.readdirSync(folderPath);
+ files.forEach((file) => {
+ const filePath = path.resolve(folderPath, file);
+ if (fs.lstatSync(filePath).isDirectory()) {
+ removeLogFiles(filePath);
+ } else if (file.endsWith('.log')) {
+ fs.unlinkSync(filePath);
+ }
+ });
+ };
+ removeLogFiles(this.engineSettings.logFolderPath);
+ }
+ }
+ } else {
+ fs.mkdirSync(this.engineSettings.logFolderPath, { recursive: true });
+ fs.mkdirSync(this.debugLogFolderPath, { recursive: true });
+ fs.mkdirSync(this.errorLogFolderPath, { recursive: true });
+ fs.mkdirSync(this.fatalLogFolderPath, { recursive: true });
+ }
+
+ this.latestLogStream = fs.createWriteStream(path.resolve(this.engineSettings.logFolderPath, 'latest.log'), { flags: 'a' });
+ if (this.engineSettings.enableDebugLog) this.debugLogStream = fs.createWriteStream(path.resolve(this.debugLogFolderPath, 'latest.log'), { flags: 'a' });
+ if (this.engineSettings.enableErrorLog) this.errorLogStream = fs.createWriteStream(path.resolve(this.errorLogFolderPath, 'latest.log'), { flags: 'a' });
+ if (this.engineSettings.enableFatalLog) this.fatalLogStream = fs.createWriteStream(path.resolve(this.fatalLogFolderPath, 'latest.log'), { flags: 'a' });
+ }
+
+ /**
+ * Deconstructs the FileStorageEngine
+ * @returns void
+ */
+ async destroy(): Promise {
+ this.loggers.forEach((logger) => {
+ logger.unRegisterListener(this);
+ });
+ await this.closeStreams();
}
private parseTextStyles(chunk: IMessageChunk, subLine?: boolean): string {
- let special = false;
- chunk.styling.forEach((style) => {
- switch (style) {
- case EStyles.specialSubLine:
- special = true;
- break;
- default:
- break;
- }
+ return `${subLine && (!chunk.styling.includes(EStyles.specialSubLine) && chunk.breaksLine) ? '| ' : ''}${chunk.content}`;
+ }
+
+ /**
+ * Closes all files streams
+ * NOTE: Only call this method when you are done with the logger, and you're not using autoLogEnd!
+ * @returns void
+ */
+ async closeStreams(): Promise {
+ return new Promise((_resolve) => {
+ const promises: Promise[] = [];
+ const date = new Date();
+ const timestamp = date.toISOString().replace(/:/g, '-').replace(/\./g, '-').replace('T', '-').replace('Z', '');
+ promises.push(new Promise((resolve) => {
+ if (this.latestLogStream) {
+ this.latestLogStream.close(() => {
+ if (fs.existsSync(path.resolve(this.engineSettings.logFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.engineSettings.logFolderPath, 'latest.log'), path.resolve(this.engineSettings.logFolderPath, `${timestamp}.log`));
+ }
+ resolve();
+ });
+ }
+ }));
+ promises.push(new Promise((resolve) => {
+ if (this.debugLogStream) {
+ this.debugLogStream.close(() => {
+ if (fs.existsSync(path.resolve(this.debugLogFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.debugLogFolderPath, 'latest.log'), path.resolve(this.debugLogFolderPath, `${timestamp}.log`));
+ }
+ resolve();
+ });
+ }
+ }));
+ promises.push(new Promise((resolve) => {
+ if (this.errorLogStream) {
+ this.errorLogStream.close(() => {
+ if (fs.existsSync(path.resolve(this.errorLogFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.errorLogFolderPath, 'latest.log'), path.resolve(this.errorLogFolderPath, `${timestamp}.log`));
+ }
+ resolve();
+ });
+ }
+ }));
+ promises.push(new Promise((resolve) => {
+ if (this.fatalLogStream) {
+ this.fatalLogStream.close(() => {
+ if (fs.existsSync(path.resolve(this.fatalLogFolderPath, 'latest.log'))) {
+ fs.renameSync(path.resolve(this.fatalLogFolderPath, 'latest.log'), path.resolve(this.fatalLogFolderPath, `${timestamp}.log`));
+ }
+ resolve();
+ });
+ }
+ }));
+ Promise.all(promises).then(() => {
+ _resolve();
+ });
});
- return (subLine && !special ? '| ' : '') + chunk.content;
}
- log(message: ILogMessage): void {
- if (!this.debug && message.logLevel === ELoggerLevel.DEBUG) return;
+ private async logTextToFile(txt: string, logLevel: ELoggerLevel): Promise {
+ const promises: Promise[] = [];
+ promises.push((new Promise((resolve) => {
+ if (this.latestLogStream) this.latestLogStream.write(txt, () => { resolve(); });
+ else resolve();
+ })));
+ switch (logLevel) {
+ case ELoggerLevel.DEBUG:
+ promises.push((new Promise((resolve) => {
+ if (this.debugLogStream) this.debugLogStream.write(txt, () => { resolve(); });
+ else resolve();
+ })));
+ break;
+ case ELoggerLevel.ERROR:
+ promises.push((new Promise((resolve) => {
+ if (this.errorLogStream) this.errorLogStream.write(txt, () => { resolve(); });
+ else resolve();
+ })));
+ break;
+ case ELoggerLevel.FATAL:
+ promises.push((new Promise((resolve) => {
+ if (this.fatalLogStream) this.fatalLogStream.write(txt, () => { resolve(); });
+ else resolve();
+ })));
+ break;
+ default:
+ break;
+ }
+ await Promise.all(promises);
+ }
- const defaultSettings = message.settings.default;
+ private async runLogQueue(): Promise {
+ if (this.logQueueRunning) return;
+ if (this.logQueue.length <= 0) {
+ this.logQueueRunning = false;
+ return;
+ }
+ this.logQueueRunning = true;
+ const log = this.logQueue.shift();
+ if (!log) {
+ this.logQueueRunning = false;
+ return;
+ }
+ await this.logTextToFile(log.txt, log.level);
+ this.logQueueRunning = false;
+ this.runLogQueue();
+ }
- const timestamp = this.getTime(message.timestamp);
+ private logToFile(txt: string, logLevel: ELoggerLevel): void {
+ this.logQueue.push({ txt, level: logLevel });
+ if (!this.logQueueRunning) this.runLogQueue();
+ }
- const prefixes = message.prefixes;
+ /**
+ * Logs a message to the file
+ * @param message The message to be logged
+ * @returns void
+ */
+ log(message: ILogMessage): void {
+ if (!this.debug && message.logLevel === ELoggerLevel.DEBUG) return;
+ const timestamp = this.getTime(message.timestamp, true);
- const prefixTxt = prefixes.map((prefix) => ` [${prefix}]`);
- const level = ` ${ELoggerLevelNames[message.logLevel]}: `;
+ const prefixes = message.prefixes.map((prefix) => `[${prefix.content}]`).join(' ');
+ const level = ELoggerLevelNames[message.logLevel];
- const txt = message.messageChunks.map((chunk): string => this.parseTextStyles(chunk, false));
+ const textContent = message.messageChunks.map((chunk): string => this.parseTextStyles(chunk, false)).join('');
- // this.consoleLoggers[message.logLevel](`${timestamp}${prefixTxt}${level}${txt.join('')}`);
+ this.logToFile(`${timestamp} ${prefixes} ${level}: ${textContent}\n`, message.logLevel);
if (!message.subLines || message.subLines.length <= 0) return;
let biggestLine = 0;
- message.subLines.forEach((line) => {
- if (line.content.length > biggestLine) biggestLine = line.content.length;
- // this.consoleLoggers[message.logLevel](this.parseTextStyles(line, true));
+ let lineBuffer = '';
+ const lines: string[] = [];
+ message.subLines.forEach((line, index, arr) => {
+ lineBuffer += this.parseTextStyles(line, true);
+ if (arr[index + 1]?.breaksLine || index === arr.length - 1) {
+ if (lineBuffer.length > biggestLine) biggestLine = lineBuffer.length;
+ lines.push(lineBuffer);
+ lineBuffer = '';
+ }
});
+ this.logToFile(lines.join('\n') + '\n', message.logLevel);
- this.consoleLoggers[message.logLevel](
- this.parseTextStyles({
- content: '#'.padEnd(biggestLine, '-'),
- styling: [EStyles.specialSubLine],
- stylingParams: [''],
- subLine: true,
- },
- true,
- ),
- );
+ this.logToFile(this.parseTextStyles({
+ content: '#'.padEnd(biggestLine, '-'),
+ styling: [EStyles.specialSubLine],
+ stylingParams: [''],
+ subLine: true,
+ breaksLine: false,
+ }, true) + '\n',
+ message.logLevel);
}
}
diff --git a/src/tests/tester.js b/src/tests/tester.js
old mode 100644
new mode 100755
index cf11316..67d03d3
--- a/src/tests/tester.js
+++ b/src/tests/tester.js
@@ -1,25 +1,109 @@
-const { Logger } = require('../../build');
+const { Logger, FileStorageEngine, ConsoleEngine, AutoLogEnd } = require('../../build/src');
+const path = require('path');
+console.time('Test execution time');
+
+// Creates a logger instance
const logger = new Logger({
- prefix: 'Logger.JS', // This will be the prefix of all logs (default: null)
- disableFatalCrash: true, // If true, the logger will not crash the process when a fatal error occurs (default: false)
- allLineColored: true, // If true, the whole line will be colored instead of only the prefix (default: false)
- coloredBackground: false, // If true, the background of the lines will be colored instead of the text (default: false)
- debug: false, // If true, the logger will log debug messages (default: false)
- fileProperties: { // This is the configuration of the log files
- enable: true, // If true, the logger will log to files (default: false) [NOTE: If false all below options will be ignored]
- logFolderPath: './logs', // This is the path of the folder where the log files will be saved (default: './logs')
- enableLatestLog: true, // If true, the logger will save the latest log in a file (default: true)
- enableDebugLog: true, // If true, the logger will save the debug logs in a file (default: false)
- enableErrorLog: true, // If true, the logger will save the error logs in a file (default: false)
- enableFatalLog: true, // If true, the logger will save the fatal logs in a file (default: true)
- generateHTMLLog: true, // If true, the logger will generate a HTML file with the logs otherwise a .log file (default: false)
- compressLogFilesAfterNewExecution: true, // If true, the logger will compress the log files to zip after a new execution (default: true)
+ // adds a basic string prefix
+ prefixes: ['Logger.JS',
+ // and a complex long prefix
+ {
+ // prefix text
+ content: 'This is a stupidly long prefix :)',
+ // this function sets the color of the prefix text, the txt parameter is the content value
+ // and it must return a array whos size is equal to the amount of letters in the content value
+ // NOTE: color doesn't need to be a function, it can be a array, or a string!
+ // if it is an array then its size must match the amount of letters of the content value, however
+ // if it is a string then its hex code is used to paint the whole text
+ color: (txt) => {
+ // in this example we set a list of hex colors and repeat it to match the amount of letters
+ const colors = ['#ff5555', '#55ff55', '#5555ff'];
+ return txt.split('').map((t, i) => {
+ return colors[i % colors.length];
+ });
+ },
+ // background color followes the same logic as color, it can be a function, an array or a string
+ backgroundColor: '#553311',
+ }
+ ],
+ // disables fatal crash so that fatal logs won't immediatly end the process
+ disableFatalCrash: true,
+ // makes the message of the log also be colored
+ allLineColored: true,
+ redactedContent: ['true'],
+});
+
+// Creates and registers a ConsoleEngine, all logs will now be displayed on the terminal
+logger.registerListener(new ConsoleEngine({
+ debug: true,
+}));
+
+// Iniciates the AutoLogEnd singleton, this allows us to set custom routines to be executed before
+// our program exits, but also will automaticly be used by the FileStorageEngines to close and
+// rename the log files at the exit of the program.
+// We are giving it our instance of logger, however this is optional and it could be instanciated
+// without any parameter, where it would create its own instance of a logger and Console engine
+// by giving it our instance however this means any log done by it will have our prefixes and will
+// trigger the registered engines
+new AutoLogEnd(logger);
+
+// NOTE: it is always recommended to create AutoLogEnd before any FileStorageEngine
+// as FileStorageEngine automaticly registers it deconstructor if AutoLogEnd already exists
+
+// Creates and registers a FileStorageEngine, all logs from now on will be saved on disk!
+logger.registerListener(new FileStorageEngine({
+ debug: true,
+ logFolderPath: path.resolve(__dirname, 'logs'),
+ enableDebugLog: true,
+ enableErrorLog: true,
+ enableFatalLog: true,
+ compressLogFilesAfterNewExecution: true,
+}));
+
+// Regular usage
+logger.info('Hello, World!');
+logger.warn('Hello, World!');
+logger.error('Hello, World!');
+logger.fatal('Hello, World!');
+logger.debug('Hello, World!');
+
+// Using template literals
+logger.info`Hello, ${'World'}`;
+logger.warn`Hello, ${'World'}`;
+logger.error`Hello, ${'World'}`;
+logger.fatal`Hello, ${'World'}`;
+logger.debug`Hello, ${'World'}`;
+
+// Logging different data types
+const myObj = {
+ kind: 'example',
+ bool: true,
+ number: 1,
+ nested: {
+ result: 'yes',
+ happy: true,
},
+}
+
+const myArray = [1,2,3,4,5,6,7,8,9,10];
+const myErr = new Error('Example Error', {
+ cause: new Error('Another Error', {
+ cause: myObj
+ }),
});
-logger.info('This is an info message');
-logger.warn('This is a warning message');
-logger.error('This is an error message');
-logger.debug('This is a debug message');
-logger.fatal('This is a fatal message');
+logger.info(myObj);
+logger.warn('The object ->', myObj);
+logger.error`Yes an object -> ${myObj}`;
+logger.info(myArray);
+logger.warn('The array ->', myArray);
+logger.error`Yes an array -> ${myArray}`;
+logger.error(myErr);
+logger.error('The error ->', myErr);
+logger.error`Yes an error -> ${myErr}`;
+logger.error(myObj, myArray, myErr);
+logger.warn`${myObj}, ${myErr}, ${myArray}`;
+logger.info('And last\nBut not least\nMultiline Example!');
+
+console.timeEnd('Test execution time');
From 9cc681ef01cca7a2bcafd78a129b90fc12f30d79 Mon Sep 17 00:00:00 2001
From: Space_Fox <44732812+emanuelfranklyn@users.noreply.github.com>
Date: Mon, 10 Jun 2024 20:37:12 -0300
Subject: [PATCH 03/10] (fix): small bug fixes & documentation
Fix some small bugs, makes the code more legible and updates markdown files and images
---
.github/assets/LoggerExample.png | Bin 25970 -> 57898 bytes
CHANGELOG.md | 29 +++
README.md | 73 ++++---
src/main/autoLogEnd.ts | 22 ++-
src/main/interfaces/ILoggerOption.ts | 2 -
src/main/logger.ts | 201 +++++++++++---------
src/main/outputEngines/consoleEngine.ts | 17 +-
src/main/outputEngines/engine.ts | 8 +
src/main/outputEngines/fileStorageEngine.ts | 54 +++---
src/tests/readmeExample.js | 37 ++++
src/tests/tester.js | 15 +-
11 files changed, 298 insertions(+), 160 deletions(-)
mode change 100644 => 100755 CHANGELOG.md
mode change 100644 => 100755 README.md
create mode 100644 src/tests/readmeExample.js
diff --git a/.github/assets/LoggerExample.png b/.github/assets/LoggerExample.png
index b4b65be9f6562974bdd4cfc9249f0cb75c41ea7c..f230eb2845af71cced957ec287302329690967f8 100644
GIT binary patch
literal 57898
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