Files
aurorabot/shared/lib/logger.ts

163 lines
5.0 KiB
TypeScript

import { join, resolve } from "path";
import { appendFile, mkdir, stat } from "fs/promises";
import { existsSync } from "fs";
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
const LogLevelNames = {
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
};
export type LogSource = "bot" | "web" | "shared" | "system";
export interface LogEntry {
timestamp: string;
level: string;
source: LogSource;
message: string;
data?: any;
stack?: string;
}
class Logger {
private logDir: string;
private errorLogPath: string;
private initialized: boolean = false;
private initPromise: Promise<void> | null = null;
constructor() {
// Use resolve with __dirname or process.cwd() but make it more robust
// Since this is in shared/lib/, we can try to find the project root
// For now, let's stick to a resolved path from process.cwd() or a safer alternative
this.logDir = resolve(process.cwd(), "logs");
this.errorLogPath = join(this.logDir, "error.log");
}
private async ensureInitialized() {
if (this.initialized) return;
if (this.initPromise) return this.initPromise;
this.initPromise = (async () => {
try {
await mkdir(this.logDir, { recursive: true });
this.initialized = true;
} catch (err: any) {
if (err.code === "EEXIST" || err.code === "ENOTDIR") {
try {
const stats = await stat(this.logDir);
if (stats.isDirectory()) {
this.initialized = true;
return;
}
} catch (statErr) {}
}
console.error(`[SYSTEM] Failed to initialize logger directory at ${this.logDir}:`, err);
} finally {
this.initPromise = null;
}
})();
return this.initPromise;
}
private safeStringify(data: any): string {
try {
return JSON.stringify(data);
} catch (err) {
const seen = new WeakSet();
return JSON.stringify(data, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) return "[Circular]";
seen.add(value);
}
return value;
});
}
}
private formatMessage(entry: LogEntry): string {
const dataStr = entry.data ? ` | Data: ${this.safeStringify(entry.data)}` : "";
const stackStr = entry.stack ? `\nStack Trace:\n${entry.stack}` : "";
return `[${entry.timestamp}] [${entry.level}] [${entry.source.toUpperCase()}] ${entry.message}${dataStr}${stackStr}`;
}
private async writeToErrorLog(formatted: string) {
await this.ensureInitialized();
try {
await appendFile(this.errorLogPath, formatted + "\n");
} catch (err) {
console.error("[SYSTEM] Failed to write to error log file:", err);
}
}
private log(level: LogLevel, source: LogSource, message: string, errorOrData?: any) {
const timestamp = new Date().toISOString();
const levelName = LogLevelNames[level];
const entry: LogEntry = {
timestamp,
level: levelName,
source,
message,
};
if (level === LogLevel.ERROR && errorOrData instanceof Error) {
entry.stack = errorOrData.stack;
entry.message = `${message}: ${errorOrData.message}`;
} else if (errorOrData !== undefined) {
entry.data = errorOrData;
}
const formatted = this.formatMessage(entry);
// Print to console
switch (level) {
case LogLevel.DEBUG:
console.debug(formatted);
break;
case LogLevel.INFO:
console.log(formatted);
break;
case LogLevel.WARN:
console.warn(formatted);
break;
case LogLevel.ERROR:
console.error(formatted);
break;
}
// Persistent error logging
if (level === LogLevel.ERROR) {
this.writeToErrorLog(formatted).catch(() => {
// Silently fail to avoid infinite loops
});
}
}
debug(source: LogSource, message: string, data?: any) {
this.log(LogLevel.DEBUG, source, message, data);
}
info(source: LogSource, message: string, data?: any) {
this.log(LogLevel.INFO, source, message, data);
}
warn(source: LogSource, message: string, data?: any) {
this.log(LogLevel.WARN, source, message, data);
}
error(source: LogSource, message: string, error?: any) {
this.log(LogLevel.ERROR, source, message, error);
}
}
export const logger = new Logger();