You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

275 lines
7.0 KiB

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