163 lines
5.0 KiB
TypeScript
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();
|