|
|
@@ -1,105 +1,110 @@
|
|
|
-// electron/modules/logger.ts - 日志模块
|
|
|
+// electron/modules/logger.ts - 日志模块 (基于 electron-log)
|
|
|
|
|
|
-import * as fs from 'fs/promises'
|
|
|
-import * as fsSync from 'fs'
|
|
|
+import log from 'electron-log/main'
|
|
|
import * as path from 'path'
|
|
|
import { app } from 'electron'
|
|
|
-import type { LogEntry } from './types'
|
|
|
+import type { LogEntry, LogCategory } from './types'
|
|
|
|
|
|
-type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
|
|
|
-
|
|
|
-interface LoggerOptions {
|
|
|
- level?: LogLevel
|
|
|
- enableFile?: boolean
|
|
|
- maxFileSize?: number
|
|
|
- maxFiles?: number
|
|
|
-}
|
|
|
-
|
|
|
-const LOG_LEVELS: Record<LogLevel, number> = {
|
|
|
- DEBUG: 0,
|
|
|
- INFO: 1,
|
|
|
- WARN: 2,
|
|
|
- ERROR: 3
|
|
|
-}
|
|
|
-
|
|
|
-// 日志文件大小常量
|
|
|
+// 日志配置常量
|
|
|
const MAX_LOG_FILE_SIZE = 5 * 1024 * 1024 // 5MB
|
|
|
const DEFAULT_MAX_FILES = 3
|
|
|
const DEFAULT_MAX_RECENT_LOGS = 500
|
|
|
|
|
|
+// 创建两个独立的日志实例
|
|
|
+const appLog = log.create({ logId: 'app' })
|
|
|
+const installLog = log.create({ logId: 'install' })
|
|
|
+
|
|
|
class Logger {
|
|
|
- private level: LogLevel = 'INFO'
|
|
|
- private enableFile = true
|
|
|
- private maxFileSize = MAX_LOG_FILE_SIZE
|
|
|
- private maxFiles = DEFAULT_MAX_FILES
|
|
|
- private logDir: string = ''
|
|
|
- private currentLogFile: string = ''
|
|
|
private recentLogs: LogEntry[] = []
|
|
|
private maxRecentLogs = DEFAULT_MAX_RECENT_LOGS
|
|
|
+ private logDir: string = ''
|
|
|
+ private initialized = false
|
|
|
|
|
|
- init(options: LoggerOptions = {}): void {
|
|
|
- this.level = options.level || 'INFO'
|
|
|
- this.enableFile = options.enableFile ?? true
|
|
|
- this.maxFileSize = options.maxFileSize || this.maxFileSize
|
|
|
- this.maxFiles = options.maxFiles || this.maxFiles
|
|
|
+ init(): void {
|
|
|
+ if (this.initialized) return
|
|
|
+ this.initialized = true
|
|
|
|
|
|
- if (this.enableFile) {
|
|
|
- this.logDir = path.join(app.getPath('userData'), 'logs')
|
|
|
- if (!fsSync.existsSync(this.logDir)) {
|
|
|
- fsSync.mkdirSync(this.logDir, { recursive: true })
|
|
|
- }
|
|
|
- this.currentLogFile = path.join(this.logDir, 'app.log')
|
|
|
- this.rotateLogsIfNeeded()
|
|
|
- }
|
|
|
+ this.logDir = path.join(app.getPath('userData'), 'logs')
|
|
|
+
|
|
|
+ // 配置应用日志 (app.log)
|
|
|
+ this.configureLogger(appLog, 'app.log')
|
|
|
+
|
|
|
+ // 配置安装日志 (install.log)
|
|
|
+ this.configureLogger(installLog, 'install.log')
|
|
|
+
|
|
|
+ this.info('日志系统初始化完成')
|
|
|
}
|
|
|
|
|
|
- private rotateLogsIfNeeded(): void {
|
|
|
- if (!fsSync.existsSync(this.currentLogFile)) return
|
|
|
-
|
|
|
- const stats = fsSync.statSync(this.currentLogFile)
|
|
|
- if (stats.size >= this.maxFileSize) {
|
|
|
- // 删除最旧的日志
|
|
|
- for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
|
- const oldFile = path.join(this.logDir, `app.${i}.log`)
|
|
|
- const newFile = path.join(this.logDir, `app.${i + 1}.log`)
|
|
|
- if (fsSync.existsSync(oldFile)) {
|
|
|
- if (i === this.maxFiles - 1) {
|
|
|
- fsSync.unlinkSync(oldFile)
|
|
|
- } else {
|
|
|
- fsSync.renameSync(oldFile, newFile)
|
|
|
+ private configureLogger(logger: typeof log, fileName: string): void {
|
|
|
+ // 设置日志文件路径
|
|
|
+ logger.transports.file.resolvePathFn = () => path.join(this.logDir, fileName)
|
|
|
+
|
|
|
+ // 文件日志配置
|
|
|
+ logger.transports.file.level = 'debug'
|
|
|
+ logger.transports.file.maxSize = MAX_LOG_FILE_SIZE
|
|
|
+ logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
|
|
|
+
|
|
|
+ // 控制台日志配置
|
|
|
+ logger.transports.console.level = 'debug'
|
|
|
+ logger.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}] [{level}] {text}'
|
|
|
+
|
|
|
+ // 日志轮转:保留最近的几个文件
|
|
|
+ logger.transports.file.archiveLogFn = (oldLogFile) => {
|
|
|
+ const info = path.parse(oldLogFile.path)
|
|
|
+ // 删除旧的轮转文件
|
|
|
+ for (let i = DEFAULT_MAX_FILES; i >= 1; i--) {
|
|
|
+ const oldFile = path.join(info.dir, `${info.name}.${i}${info.ext}`)
|
|
|
+ const newFile = path.join(info.dir, `${info.name}.${i + 1}${info.ext}`)
|
|
|
+ try {
|
|
|
+ const fs = require('fs')
|
|
|
+ if (fs.existsSync(oldFile)) {
|
|
|
+ if (i === DEFAULT_MAX_FILES) {
|
|
|
+ fs.unlinkSync(oldFile)
|
|
|
+ } else {
|
|
|
+ fs.renameSync(oldFile, newFile)
|
|
|
+ }
|
|
|
}
|
|
|
+ } catch {
|
|
|
+ // 忽略轮转错误
|
|
|
}
|
|
|
}
|
|
|
- fsSync.renameSync(this.currentLogFile, path.join(this.logDir, 'app.1.log'))
|
|
|
+ // 重命名当前日志文件
|
|
|
+ try {
|
|
|
+ const fs = require('fs')
|
|
|
+ fs.renameSync(oldLogFile.path, path.join(info.dir, `${info.name}.1${info.ext}`))
|
|
|
+ } catch {
|
|
|
+ // 忽略重命名错误
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private shouldLog(level: LogLevel): boolean {
|
|
|
- return LOG_LEVELS[level] >= LOG_LEVELS[this.level]
|
|
|
+ /**
|
|
|
+ * 获取日志目录路径
|
|
|
+ */
|
|
|
+ getLogDir(): string {
|
|
|
+ return this.logDir
|
|
|
}
|
|
|
|
|
|
- private formatMessage(level: LogLevel, message: string, data?: unknown): string {
|
|
|
- const timestamp = new Date().toISOString()
|
|
|
- let formatted = `[${timestamp}] [${level}] ${message}`
|
|
|
- if (data !== undefined) {
|
|
|
- formatted += ` ${JSON.stringify(data)}`
|
|
|
- }
|
|
|
- return formatted
|
|
|
+ /**
|
|
|
+ * 获取应用日志文件路径
|
|
|
+ */
|
|
|
+ getAppLogPath(): string {
|
|
|
+ return path.join(this.logDir, 'app.log')
|
|
|
}
|
|
|
|
|
|
- private writeToFile(formatted: string): void {
|
|
|
- if (!this.enableFile || !this.currentLogFile) return
|
|
|
-
|
|
|
- fs.appendFile(this.currentLogFile, formatted + '\n')
|
|
|
- .then(() => this.rotateLogsIfNeeded())
|
|
|
- .catch((error) => console.error('写入日志文件失败:', error))
|
|
|
+ /**
|
|
|
+ * 获取安装日志文件路径
|
|
|
+ */
|
|
|
+ getInstallLogPath(): string {
|
|
|
+ return path.join(this.logDir, 'install.log')
|
|
|
}
|
|
|
|
|
|
- private addToRecent(level: LogLevel, message: string, data?: unknown): void {
|
|
|
+ private addToRecent(level: LogEntry['level'], message: string, category: LogCategory, data?: unknown): void {
|
|
|
const entry: LogEntry = {
|
|
|
level,
|
|
|
message,
|
|
|
timestamp: new Date().toISOString(),
|
|
|
+ category,
|
|
|
data
|
|
|
}
|
|
|
this.recentLogs.push(entry)
|
|
|
@@ -108,51 +113,93 @@ class Logger {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private log(level: LogLevel, message: string, data?: unknown): void {
|
|
|
- if (!this.shouldLog(level)) return
|
|
|
-
|
|
|
- const formatted = this.formatMessage(level, message, data)
|
|
|
-
|
|
|
- // 控制台输出
|
|
|
- switch (level) {
|
|
|
- case 'DEBUG':
|
|
|
- console.debug(formatted)
|
|
|
- break
|
|
|
- case 'INFO':
|
|
|
- console.info(formatted)
|
|
|
- break
|
|
|
- case 'WARN':
|
|
|
- console.warn(formatted)
|
|
|
- break
|
|
|
- case 'ERROR':
|
|
|
- console.error(formatted)
|
|
|
- break
|
|
|
- }
|
|
|
-
|
|
|
- // 文件输出
|
|
|
- this.writeToFile(formatted)
|
|
|
-
|
|
|
- // 保存到最近日志
|
|
|
- this.addToRecent(level, message, data)
|
|
|
- }
|
|
|
+ // ==================== 应用日志方法 ====================
|
|
|
|
|
|
debug(message: string, data?: unknown): void {
|
|
|
- this.log('DEBUG', message, data)
|
|
|
+ if (data !== undefined) {
|
|
|
+ appLog.debug(message, data)
|
|
|
+ } else {
|
|
|
+ appLog.debug(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('DEBUG', message, 'app', data)
|
|
|
}
|
|
|
|
|
|
info(message: string, data?: unknown): void {
|
|
|
- this.log('INFO', message, data)
|
|
|
+ if (data !== undefined) {
|
|
|
+ appLog.info(message, data)
|
|
|
+ } else {
|
|
|
+ appLog.info(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('INFO', message, 'app', data)
|
|
|
}
|
|
|
|
|
|
warn(message: string, data?: unknown): void {
|
|
|
- this.log('WARN', message, data)
|
|
|
+ if (data !== undefined) {
|
|
|
+ appLog.warn(message, data)
|
|
|
+ } else {
|
|
|
+ appLog.warn(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('WARN', message, 'app', data)
|
|
|
}
|
|
|
|
|
|
error(message: string, data?: unknown): void {
|
|
|
- this.log('ERROR', message, data)
|
|
|
+ if (data !== undefined) {
|
|
|
+ appLog.error(message, data)
|
|
|
+ } else {
|
|
|
+ appLog.error(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('ERROR', message, 'app', data)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 安装日志方法 ====================
|
|
|
+
|
|
|
+ installDebug(message: string, data?: unknown): void {
|
|
|
+ if (data !== undefined) {
|
|
|
+ installLog.debug(message, data)
|
|
|
+ } else {
|
|
|
+ installLog.debug(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('DEBUG', message, 'install', data)
|
|
|
+ }
|
|
|
+
|
|
|
+ installInfo(message: string, data?: unknown): void {
|
|
|
+ if (data !== undefined) {
|
|
|
+ installLog.info(message, data)
|
|
|
+ } else {
|
|
|
+ installLog.info(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('INFO', message, 'install', data)
|
|
|
+ }
|
|
|
+
|
|
|
+ installWarn(message: string, data?: unknown): void {
|
|
|
+ if (data !== undefined) {
|
|
|
+ installLog.warn(message, data)
|
|
|
+ } else {
|
|
|
+ installLog.warn(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('WARN', message, 'install', data)
|
|
|
}
|
|
|
|
|
|
- getRecentLogs(limit = 200): LogEntry[] {
|
|
|
+ installError(message: string, data?: unknown): void {
|
|
|
+ if (data !== undefined) {
|
|
|
+ installLog.error(message, data)
|
|
|
+ } else {
|
|
|
+ installLog.error(message)
|
|
|
+ }
|
|
|
+ this.addToRecent('ERROR', message, 'install', data)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 日志查询方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取最近的日志
|
|
|
+ * @param limit 返回的日志条数
|
|
|
+ * @param category 可选,筛选特定类别的日志
|
|
|
+ */
|
|
|
+ getRecentLogs(limit = 200, category?: LogCategory): LogEntry[] {
|
|
|
+ if (category) {
|
|
|
+ return this.recentLogs.filter(log => log.category === category).slice(-limit)
|
|
|
+ }
|
|
|
return this.recentLogs.slice(-limit)
|
|
|
}
|
|
|
|