From 91a2fa4df3797d9dae90ecb806db3d91fbdc3a06 Mon Sep 17 00:00:00 2001 From: Jacob Paulin Date: Sat, 23 Mar 2024 18:13:20 -0400 Subject: [PATCH] feat(logger/file): Added ability to close the logger stream --- examples/example-1.js | 32 +++++++++++++- examples/example-2.js | 5 +++ examples/example-3.js | 27 ++++++++++++ src/classes/Logger.ts | 65 ++++++++++++++++++++++++++--- src/classes/OptionsBuilder.ts | 4 +- src/interfaces/FileOutputOptions.ts | 4 +- 6 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 examples/example-2.js create mode 100644 examples/example-3.js diff --git a/examples/example-1.js b/examples/example-1.js index 41e02c7..ddd40e7 100644 --- a/examples/example-1.js +++ b/examples/example-1.js @@ -1,5 +1,33 @@ const logger = require("../lib/index.js"); -const LOG = new logger.Logger(); +(async () => { + const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); }); + const LOG = new logger.Logger({ + output: { + file: { + enabled: true + } + } + }); -LOG.log("Test"); \ No newline at end of file + await sleep (5); + LOG.log("Test 1"); + + let resC; + do { + const [ x, y ] = LOG.close(); + console.log("CLOSE", x, y); + resC = x; + } while (!resC); + + LOG.log("Test 2"); + + let resO; + do { + const [ x, y ] = LOG.open(); + console.log("OPEN", x, y); + resO = x; + } while (!resO); + + LOG.log("Test 3"); +})() \ No newline at end of file diff --git a/examples/example-2.js b/examples/example-2.js new file mode 100644 index 0000000..2b83e74 --- /dev/null +++ b/examples/example-2.js @@ -0,0 +1,5 @@ +const logger = require("../lib/index.js"); + +const LOG = new logger.Logger(); + +LOG.log("Test 1"); diff --git a/examples/example-3.js b/examples/example-3.js new file mode 100644 index 0000000..8988670 --- /dev/null +++ b/examples/example-3.js @@ -0,0 +1,27 @@ +const logger = require("../lib/index.js"); + +const LOG = new logger.Logger({ + output: { + file: { + enabled: true + } + } +}); + +LOG.log("Test 1"); + +setTimeout(() => { + LOG.close(); +}, 50) + +setTimeout(() => { + LOG.log("Test 2"); +}, 150) + +setTimeout(() => { + LOG.open(); +}, 250) + +setTimeout(() => { + LOG.log("Test 3"); +}, 350) diff --git a/src/classes/Logger.ts b/src/classes/Logger.ts index e74ba0e..ec75e53 100644 --- a/src/classes/Logger.ts +++ b/src/classes/Logger.ts @@ -1,5 +1,5 @@ import { hostname } from "node:os"; -import { WriteStream, mkdirSync, statSync, createWriteStream, readdirSync, unlinkSync } from "node:fs"; +import { WriteStream, mkdirSync, statSync, createWriteStream, readdirSync, unlinkSync, existsSync } from "node:fs"; import dateFormat from "./DateFormat"; import { OptionsBuilder } from "./OptionsBuilder"; import { DirectoryCreationError } from "./errors/DirectoryCreationError"; @@ -8,6 +8,8 @@ import { LogLevelValues } from "../enums/LogLevelValues"; import { LoggerOptions } from "../interfaces/LoggerOptions"; import { PartialLoggerOptions } from "../interfaces/partials/PartialLoggerOptions"; import { LogLevels } from "../types/LogLevels"; +import { format } from "node:util"; +import path from "node:path"; export class Logger { private readonly _options: LoggerOptions; @@ -15,6 +17,8 @@ export class Logger { private writeStream: WriteStream | null = null; private tempFileBuffer: string[] = []; private initComplete: boolean = false; + private closing: boolean = false; + private opening: boolean = false; private fileSwitchTimeout?: NodeJS.Timeout; constructor(options: PartialLoggerOptions = {}) { @@ -26,7 +30,8 @@ export class Logger { ? LogFormatting[this.options.output.formatting].bind(this) : this.options.output.formatting.bind(this); // Run out init function - this.init(); + const [ openSucceeded, msg ] = this.open(); + if (!openSucceeded) throw new Error(`Init Error: ${msg}`); } public get options(): LoggerOptions { @@ -43,7 +48,8 @@ export class Logger { private init(): void { // Make sure we haven't already ran the init function - if (this.initComplete) return; + if (this.initComplete || this.opening) return; + this.opening = true; // Create the log file and schedule it's swap if needed if (this.options.output.file.enabled) { @@ -82,9 +88,16 @@ export class Logger { private setUpLogFile() { // Setup the new file - this.createDirIfNeeded(this.options.output.file.outputDirectory); - const fileName = this.getFileNameFromDate(); - const filePath = this.options.output.file.outputDirectory + fileName; + const trueFileDir = path.normalize(this.options.output.file.outputDirectory); + this.createDirIfNeeded(trueFileDir); + let fileName: string; + let filePath: string; + let iterations = 0; + do { + fileName = iterations > 0 ? format("%s (%s).log", this.getFileNameFromDate(), iterations) : format("%s.log", this.getFileNameFromDate()); + filePath = trueFileDir + fileName; + iterations++; + } while (existsSync(format("%s\\%s", process.cwd(), filePath))); const oldStream = this.writeStream; const newStream = createWriteStream(filePath, { flags: "wx", encoding: "utf8" }); this.writeStream = null; @@ -106,6 +119,8 @@ export class Logger { oldStream.write(`--- Log file closed as of ${dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss.l Z", this.options.output.useZuluTime)} ---`); oldStream.end(); } + + this.opening = false; }); return; @@ -145,6 +160,9 @@ export class Logger { } private print(level: LogLevels, message?: string, error?: Error): void { + // If we are in the process of closing or opening the file & logger don't log + if (this.closing || this.opening) return; + // If we have disabled bot console and file logging stop if (!this.options.output.console.enabled && this.options.output.file.enabled) return; @@ -176,6 +194,41 @@ export class Logger { } } + public close(): [boolean, string | null] { + if (this.opening) return [ false, "Cannot close while opening" ]; + if (this.closing) return [ false, "Cannot close while already closing" ]; + if (!this.initComplete) return [ false, "Cannot close already closed logger" ]; + + this.closing = true; + clearTimeout(this.fileSwitchTimeout); + + const oldStream = this.writeStream; + const oldBuffer = this.tempFileBuffer; + this.writeStream = null; + this.tempFileBuffer = []; + + if (oldStream !== null) { + // Write the temp storage to the stream + if (oldBuffer.length > 0) oldBuffer.forEach(elem => oldStream.write(elem)); + + oldStream.write(`--- Log file closed as of ${dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss.l Z", this.options.output.useZuluTime)} ---`); + oldStream.end(); + } + + this.initComplete = false; + this.closing = false; + + return [ true, null ]; + } + + public open(): [boolean, string | null] { + if (this.closing) return [ false, "Cannot open while closing" ]; + if (this.opening) return [ false, "Cannot open while already opening" ]; + + this.init(); + return [ true, null ]; + } + public debug(message: string): void { this.print("debug", message); } diff --git a/src/classes/OptionsBuilder.ts b/src/classes/OptionsBuilder.ts index ede2ded..dcfb35a 100644 --- a/src/classes/OptionsBuilder.ts +++ b/src/classes/OptionsBuilder.ts @@ -16,8 +16,8 @@ export class OptionsBuilder { }, file: { enabled: false, - outputDirectory: "./logs/", - outputFileName: "yyyy-mm-dd'T'HH-MM-ss'.log'" + outputDirectory: ".\\logs\\", + outputFileName: "yyyy-mm-dd'T'HH-MM-ss" } } } diff --git a/src/interfaces/FileOutputOptions.ts b/src/interfaces/FileOutputOptions.ts index 7352eaa..f099e5d 100644 --- a/src/interfaces/FileOutputOptions.ts +++ b/src/interfaces/FileOutputOptions.ts @@ -7,14 +7,14 @@ export interface FileOutputOptions { /** * The directory to create the log files in - * Default: './logs/' + * Default: '.\\logs\\' */ outputDirectory: string; /** * The format for the name of the log files * Supports DateFormat variables - * Default: 'yyyy-mm-dd'T'hh-MM-ss.log' + * Default: 'yyyy-mm-dd'T'hh-MM-ss' */ outputFileName: string; } \ No newline at end of file