157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import * as fs from "fs";
|
||
import * as path from "path";
|
||
|
||
type LogLevel = "log" | "info" | "warn" | "error" | "debug";
|
||
type ConsoleMethod = (...args: unknown[]) => void;
|
||
|
||
function getLogDir(): string {
|
||
const isElectron = typeof process.versions?.electron !== "undefined";
|
||
if (isElectron) {
|
||
const { app } = require("electron");
|
||
return path.join(app.getPath("userData"), "logs");
|
||
}
|
||
return path.join(process.cwd(), "logs");
|
||
}
|
||
|
||
const LOG_DIR = getLogDir();
|
||
const LOG_FILE = path.join(LOG_DIR, "app.log");
|
||
const MAX_SIZE = 1000 * 1024 * 1024;
|
||
const LEVELS: LogLevel[] = ["log", "info", "warn", "error", "debug"];
|
||
|
||
class Logger {
|
||
private stream: fs.WriteStream | null = null;
|
||
private originalConsole: Partial<Record<LogLevel, ConsoleMethod>> = {};
|
||
private originalStdoutWrite: typeof process.stdout.write | null = null;
|
||
private originalStderrWrite: typeof process.stderr.write | null = null;
|
||
private isHijacked = false;
|
||
|
||
init(): this {
|
||
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||
this.stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
|
||
this.hijack();
|
||
return this;
|
||
}
|
||
|
||
private formatTime(): string {
|
||
const d = new Date();
|
||
const p = (n: number, l = 2) => String(n).padStart(l, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(
|
||
d.getMilliseconds(),
|
||
3,
|
||
)}`;
|
||
}
|
||
|
||
private stringify(arg: unknown): string {
|
||
if (arg == null) return String(arg);
|
||
if (arg instanceof Error) return `${arg.message}\n${arg.stack || ""}`;
|
||
if (typeof arg === "object") {
|
||
try {
|
||
return JSON.stringify(arg);
|
||
} catch {
|
||
return String(arg);
|
||
}
|
||
}
|
||
return String(arg);
|
||
}
|
||
|
||
private writing = false;
|
||
|
||
private write(level: LogLevel, args: unknown[]): void {
|
||
const line = `[${this.formatTime()}] [${level.toUpperCase()}] ${args.map((a) => this.stringify(a)).join(" ")}\n`;
|
||
if (this.stream && !this.stream.destroyed) this.stream.write(line);
|
||
this.checkRotate();
|
||
}
|
||
|
||
private writeRaw(chunk: any): void {
|
||
if (this.writing) return;
|
||
this.writing = true;
|
||
try {
|
||
let str = typeof chunk === "string" ? chunk : chunk?.toString?.("utf-8") ?? "";
|
||
str = str.replace(/\x1B\[\d*m/g, ""); // 去除 ANSI 颜色码
|
||
if (str.trim() && this.stream && !this.stream.destroyed) this.stream.write(str.endsWith("\n") ? str : str + "\n");
|
||
} finally {
|
||
this.writing = false;
|
||
}
|
||
}
|
||
|
||
private checkRotate(): void {
|
||
try {
|
||
if (!fs.existsSync(LOG_FILE) || fs.statSync(LOG_FILE).size < MAX_SIZE) return;
|
||
this.stream?.end();
|
||
// 单文件轮转:保留后半部分日志
|
||
const content = fs.readFileSync(LOG_FILE, "utf-8");
|
||
const half = content.slice(content.length >>> 1);
|
||
const firstNewline = half.indexOf("\n");
|
||
fs.writeFileSync(LOG_FILE, firstNewline >= 0 ? half.slice(firstNewline + 1) : half);
|
||
this.stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
|
||
} catch {}
|
||
}
|
||
|
||
private hijack(): void {
|
||
if (this.isHijacked) return;
|
||
|
||
// 劫持 console 方法
|
||
for (const level of LEVELS) {
|
||
const original = console[level];
|
||
if (typeof original !== "function") continue;
|
||
this.originalConsole[level] = original.bind(console);
|
||
(console as any)[level] = (...args: unknown[]) => {
|
||
this.writing = true;
|
||
this.write(level, args);
|
||
this.originalConsole[level]!(...args);
|
||
this.writing = false;
|
||
};
|
||
}
|
||
|
||
// 劫持 stdout/stderr(捕获 morgan 等直接写 stdout 的输出)
|
||
this.originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||
this.originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||
|
||
process.stdout.write = ((chunk: any, ...rest: any[]) => {
|
||
this.writeRaw(chunk);
|
||
return this.originalStdoutWrite!(chunk, ...rest);
|
||
}) as typeof process.stdout.write;
|
||
|
||
process.stderr.write = ((chunk: any, ...rest: any[]) => {
|
||
this.writeRaw(chunk);
|
||
return this.originalStderrWrite!(chunk, ...rest);
|
||
}) as typeof process.stderr.write;
|
||
|
||
this.isHijacked = true;
|
||
}
|
||
|
||
/** 导出日志内容 */
|
||
exportLogs(): string {
|
||
if (!fs.existsSync(LOG_FILE)) return "";
|
||
return fs.readFileSync(LOG_FILE, "utf-8");
|
||
}
|
||
|
||
/** 清空日志 */
|
||
clear(): void {
|
||
this.stream?.end();
|
||
if (fs.existsSync(LOG_FILE)) fs.unlinkSync(LOG_FILE);
|
||
this.stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
|
||
}
|
||
|
||
/** 关闭日志 */
|
||
close(): void {
|
||
if (this.isHijacked) {
|
||
for (const level of LEVELS) {
|
||
const original = this.originalConsole[level];
|
||
if (original) (console as any)[level] = original;
|
||
}
|
||
this.originalConsole = {};
|
||
if (this.originalStdoutWrite) process.stdout.write = this.originalStdoutWrite;
|
||
if (this.originalStderrWrite) process.stderr.write = this.originalStderrWrite;
|
||
this.originalStdoutWrite = null;
|
||
this.originalStderrWrite = null;
|
||
this.isHijacked = false;
|
||
}
|
||
this.stream?.end();
|
||
this.stream = null;
|
||
}
|
||
}
|
||
|
||
const logger = new Logger().init();
|
||
export default logger;
|