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); } /** * Logs a request to the output * @param req Request to log * @param ip IP of the request * @param logAllDetails Whether to log all details of the request */ async logRequest(req: Request, ip?: string, logAllDetails = false) { let string = ip ? `${ip}: ` : ""; string += `${req.method} ${req.url}`; if (logAllDetails) { string += `\n`; string += ` [Headers]\n`; // Pretty print headers for (const [key, value] of req.headers.entries()) { string += ` ${key}: ${value}\n`; } // Pretty print body string += ` [Body]\n`; const content_type = req.headers.get("Content-Type"); if (content_type && content_type.includes("application/json")) { const json = await req.json(); const stringified = JSON.stringify(json, null, 4) .split("\n") .map(line => ` ${line}`) .join("\n"); string += `${stringified}\n`; } else if ( content_type && (content_type.includes("application/x-www-form-urlencoded") || content_type.includes("multipart/form-data")) ) { const formData = await req.formData(); for (const [key, value] of formData.entries()) { if (value.toString().length < 300) { string += ` ${key}: ${value.toString()}\n`; } else { string += ` ${key}: <${value.toString().length} bytes>\n`; } } } else { const text = await req.text(); string += ` ${text}\n`; } } await this.log(LogLevel.INFO, "Request", string); } } /** * Outputs to multiple LogManager instances at once */ export class MultiLogManager { constructor(private logManagers: LogManager[]) {} /** * Logs a message to all logManagers * @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 ) { for (const logManager of this.logManagers) { await logManager.log(level, entity, message, showTimestamp); } } /** * Logs an error to all logManagers * @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) { for (const logManager of this.logManagers) { await logManager.logError(level, entity, error); } } /** * Logs a request to all logManagers * @param req Request to log * @param ip IP of the request * @param logAllDetails Whether to log all details of the request */ async logRequest(req: Request, ip?: string, logAllDetails = false) { for (const logManager of this.logManagers) { await logManager.logRequest(req, ip, logAllDetails); } } /** * Create a MultiLogManager from multiple LogManager instances * @param logManagers LogManager instances to use * @returns */ static fromLogManagers(...logManagers: LogManager[]) { return new MultiLogManager(logManagers); } }