import { app, ipcMain } from "electron" import fs from "fs" import path from "path" import setting from "setting/main" import * as rfs from "rotating-file-stream" import { LogLevel, LogLevelColor, LogLevelName } from "./common" // 重置颜色的ANSI代码 const RESET_COLOR = "\x1b[0m" // 日志配置接口 export interface LoggerOptions { level?: LogLevel // 日志级别 namespace?: string // 日志命名空间 console?: boolean // 是否输出到控制台 file?: boolean // 是否输出到文件 maxSize?: string // 单个日志文件最大大小 maxFiles?: number // 保留的最大日志文件数量 } // 默认配置 const DEFAULT_OPTIONS: LoggerOptions = { level: setting.values("debug"), namespace: "app", console: true, file: true, maxSize: "10M", maxFiles: 10, } let logDir const isElectronApp = !!process.versions.electron if (isElectronApp && app) { logDir = path.join(app.getPath("logs")) } else { // 非Electron环境下使用当前目录下的logs文件夹 logDir = path.join(process.cwd(), "logs") } /** * 日志管理类 */ export class Logger { private static instance: Logger private options: LoggerOptions = DEFAULT_OPTIONS private logStream: rfs.RotatingFileStream | null = null private logDir: string = logDir private currentLogFile: string = "" private isElectronApp: boolean = !!process.versions.electron private callInitialize: boolean = false /** * 获取单例实例 */ public static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger() } if (Logger.instance.callInitialize) { return Logger.instance } else { // 创建代理对象,确保只有在初始化后才能访问除init之外的方法 const handler = { get: function (target: any, prop: string) { if (prop === "init") { return target[prop] } if (!target.callInitialize) { throw new Error(`Logger未初始化,不能调用${prop}方法,请先调用init()方法`) } return target[prop] }, } Logger.instance = new Proxy(new Logger(), handler) } return Logger.instance } /** * 构造函数 */ private constructor() {} public init(options?: LoggerOptions): void { this.callInitialize = true this.options = { ...this.options, ...options } // 确保日志目录存在 if (!fs.existsSync(this.logDir)) { fs.mkdirSync(this.logDir, { recursive: true }) } // 初始化日志文件 this.initLogFile() // 如果在主进程中,设置IPC监听器接收渲染进程的日志 if (this.isElectronApp && process.type === "browser") { this.setupIPC() } } /** * 初始化日志文件 */ private initLogFile(): void { if (!this.options.file) return // 生成日志文件名 const now = new Date() const timestamp = now.toISOString().replace(/[:.]/g, "-") this.currentLogFile = `app-logger-${timestamp}.log` // 创建日志流 this.logStream = rfs.createStream(this.currentLogFile, { path: this.logDir, size: this.options.maxSize, rotate: this.options.maxFiles, }) } /** * 设置IPC通信,接收渲染进程日志 */ private setupIPC(): void { if (!ipcMain) return ipcMain.on("logger:log", (_, level: LogLevel, namespace: string, ...messages: any[]) => { this.logWithLevel(level, namespace, ...messages) }) // 处理日志级别设置请求 ipcMain.on("logger:setLevel", (_, level: LogLevel) => { this.setLevel(level) }) } /** * 关闭日志流 */ public close(): void { if (this.logStream) { this.logStream.end() this.logStream.destroy() this.logStream = null } } /** * 设置日志级别 */ public setLevel(level: LogLevel): void { this.options.level = level } /** * 获取当前日志级别 */ public getLevel(): LogLevel { return this.options.level ?? LogLevel.INFO } /** * 根据级别记录日志 */ private logWithLevel(level: LogLevel, namespace: string, ...messages: any[]): void { // 检查日志级别 if (level < this.getLevel() || level === LogLevel.OFF) return const timestamp = new Date().toISOString() const levelName = LogLevelName[level] const prefix = `[${timestamp}] [${namespace}] [${levelName}]` // 格式化消息 const formattedMessages = messages.map(msg => { if (typeof msg === "object") { try { return JSON.stringify(msg) } catch (e) { return String(msg) } } return String(msg) }) const message = formattedMessages.join(" ") // 输出到控制台 if (this.options.console) { const color = LogLevelColor[level] console.log(`${color}${prefix} ${message}${RESET_COLOR}`) } // 写入日志文件 if (this.options.file && this.logStream) { this.logStream.write(`${prefix} ${message}\n`) } } /** * 记录跟踪级别日志 */ public trace(namespace: string, ...messages: any[]): void { this.logWithLevel(LogLevel.TRACE, namespace, ...messages) } /** * 记录调试级别日志 */ public debug(namespace: string, ...messages: any[]): void { this.logWithLevel(LogLevel.DEBUG, namespace, ...messages) } /** * 记录信息级别日志 */ public info(namespace: string, ...messages: any[]): void { this.logWithLevel(LogLevel.INFO, namespace, ...messages) } /** * 记录警告级别日志 */ public warn(namespace: string, ...messages: any[]): void { this.logWithLevel(LogLevel.WARN, namespace, ...messages) } /** * 记录错误级别日志 */ public error(namespace: string, ...messages: any[]): void { this.logWithLevel(LogLevel.ERROR, namespace, ...messages) } /** * 记录致命错误级别日志 */ public fatal(namespace: string, ...messages: any[]): void { this.logWithLevel(LogLevel.FATAL, namespace, ...messages) } /** * 创建一个固定命名空间的日志记录器 * @param namespace 命名空间 * @returns 带有固定命名空间的日志记录器 */ public createNamespace(namespace: string) { return { trace: (...messages: any[]) => this.trace(namespace, ...messages), debug: (...messages: any[]) => this.debug(namespace, ...messages), info: (...messages: any[]) => this.info(namespace, ...messages), warn: (...messages: any[]) => this.warn(namespace, ...messages), error: (...messages: any[]) => this.error(namespace, ...messages), fatal: (...messages: any[]) => this.fatal(namespace, ...messages), setLevel: (level: LogLevel) => this.setLevel(level), getLevel: () => this.getLevel(), } } } // 默认实例 const logger = Logger.getInstance() logger.init() setting.onChange("debug", function(n){ logger.setLevel(n.debug) }) // 应用退出时关闭日志流 if (process.type === "browser" && app) { app.on("before-quit", () => { logger.info("app", "应用关闭") logger.close() }) } export default logger