diff --git a/.gitignore b/.gitignore index 52091f63..38109eac 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,6 @@ dist config/config.toml config/config.internal.toml uploads/ -pages/dist \ No newline at end of file +pages/dist +log.txt +*.log \ No newline at end of file diff --git a/classes/configmanager.ts b/classes/configmanager.ts index 77bd7d74..45e9d4f0 100644 --- a/classes/configmanager.ts +++ b/classes/configmanager.ts @@ -2,6 +2,7 @@ * @file configmanager.ts * @summary ConfigManager system to retrieve and modify system configuration * @description Can read from a hand-written file, config.toml, or from a machine-saved file, config.internal.toml + * @deprecated Use the new ConfigManager class instead * Fuses both and provides a way to retrieve individual values */ diff --git a/packages/log-manager/index.ts b/packages/log-manager/index.ts new file mode 100644 index 00000000..e0e36785 --- /dev/null +++ b/packages/log-manager/index.ts @@ -0,0 +1,61 @@ +import type { BunFile } from "bun"; +import { appendFile } from "fs/promises"; + +export enum LogLevel { + DEBUG = "debug", + INFO = "info", + WARNING = "warning", + ERROR = "error", + CRITICAL = "critical", +} + +/** + * Class for handling logging to disk or to stdout + * @param output BunFile of output (can be a normal file or something like Bun.stdout) + */ +export class LogManager { + constructor(private output: BunFile) { + void this.write( + `--- INIT LogManager at ${new Date().toISOString()} --` + ); + } + + /** + * Logs a message to the output + * @param level Importance of the log + * @param entity Emitter of the log + * @param message Message to log + * @param showTimestamp Whether to show the timestamp in the log + */ + async log( + level: LogLevel, + entity: string, + message: string, + showTimestamp = true + ) { + await this.write( + `${showTimestamp ? new Date().toISOString() + " " : ""}[${level.toUpperCase()}] ${entity}: ${message}` + ); + } + + private async write(text: string) { + if (this.output == Bun.stdout) { + await Bun.write(Bun.stdout, text + "\n"); + } else { + if (!this.output.name) { + throw new Error(`Output file doesnt exist (and isnt stdout)`); + } + await appendFile(this.output.name, text + "\n"); + } + } + + /** + * Logs an error to the output, wrapper for log + * @param level Importance of the log + * @param entity Emitter of the log + * @param error Error to log + */ + async logError(level: LogLevel, entity: string, error: Error) { + await this.log(level, entity, error.message); + } +} diff --git a/packages/log-manager/package.json b/packages/log-manager/package.json new file mode 100644 index 00000000..679a8262 --- /dev/null +++ b/packages/log-manager/package.json @@ -0,0 +1,6 @@ +{ + "name": "log-manager", + "version": "0.0.0", + "main": "index.ts", + "dependencies": { } + } \ No newline at end of file diff --git a/packages/log-manager/tests/log-manager.test.ts b/packages/log-manager/tests/log-manager.test.ts new file mode 100644 index 00000000..9d2a3382 --- /dev/null +++ b/packages/log-manager/tests/log-manager.test.ts @@ -0,0 +1,94 @@ +// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts +import { LogManager, LogLevel } from "../index"; +import type fs from "fs/promises"; +import { + describe, + it, + beforeEach, + expect, + jest, + mock, + type Mock, + test, +} from "bun:test"; +import type { BunFile } from "bun"; + +describe("LogManager", () => { + let logManager: LogManager; + let mockOutput: BunFile; + let mockAppend: Mock; + + beforeEach(async () => { + mockOutput = Bun.file("test.log"); + mockAppend = jest.fn(); + await mock.module("fs/promises", () => ({ + appendFile: mockAppend, + })); + logManager = new LogManager(mockOutput); + }); + + it("should initialize and write init log", () => { + expect(mockAppend).toHaveBeenCalledWith( + mockOutput.name, + expect.stringContaining("--- INIT LogManager at") + ); + }); + + it("should log message with timestamp", async () => { + await logManager.log(LogLevel.INFO, "TestEntity", "Test message"); + expect(mockAppend).toHaveBeenCalledWith( + mockOutput.name, + expect.stringContaining("[INFO] TestEntity: Test message") + ); + }); + + it("should log message without timestamp", async () => { + await logManager.log( + LogLevel.INFO, + "TestEntity", + "Test message", + false + ); + expect(mockAppend).toHaveBeenCalledWith( + mockOutput.name, + "[INFO] TestEntity: Test message\n" + ); + }); + + test.skip("should write to stdout", async () => { + logManager = new LogManager(Bun.stdout); + await logManager.log(LogLevel.INFO, "TestEntity", "Test message"); + + const writeMock = jest.fn(); + + await mock.module("Bun", () => ({ + stdout: Bun.stdout, + write: writeMock, + })); + + expect(writeMock).toHaveBeenCalledWith( + Bun.stdout, + expect.stringContaining("[INFO] TestEntity: Test message") + ); + }); + + it("should throw error if output file does not exist", () => { + mockAppend.mockImplementationOnce(() => { + return Promise.reject( + new Error("Output file doesnt exist (and isnt stdout)") + ); + }); + expect( + logManager.log(LogLevel.INFO, "TestEntity", "Test message") + ).rejects.toThrow(Error); + }); + + it("should log error message", async () => { + const error = new Error("Test error"); + await logManager.logError(LogLevel.ERROR, "TestEntity", error); + expect(mockAppend).toHaveBeenCalledWith( + mockOutput.name, + expect.stringContaining("[ERROR] TestEntity: Test error") + ); + }); +});