Browse Source

feat: 添加logger和setting模块并重构日志系统

- 新增logger模块,提供统一的日志管理功能,支持不同日志级别和输出到文件/控制台
- 新增setting模块,用于管理应用配置,支持动态更新和持久化
- 重构主进程和渲染进程的日志系统,使用logger模块替代原有实现
- 删除原有的setting模块实现,使用新的setting模块替代
feat/icon
npmrun 2 weeks ago
parent
commit
05f83e2a08
  1. 2
      package.json
  2. 10
      packages/logger/common.ts
  3. 274
      packages/logger/main.ts
  4. 7
      packages/logger/package.json
  5. 119
      packages/logger/preload.ts
  6. 14
      packages/setting/main.ts
  7. 7
      packages/setting/package.json
  8. 10
      pnpm-lock.yaml
  9. 2
      src/main/App.ts
  10. 77
      src/main/index.ts
  11. 3
      src/main/modules/_ioc.ts
  12. 8
      src/main/modules/db/index.ts
  13. 1
      src/preload/index.ts
  14. 6
      src/renderer/src/App.vue
  15. 5
      src/types/logger.d.ts
  16. 11
      tsconfig.node.json
  17. 6
      tsconfig.web.json

2
package.json

@ -56,10 +56,12 @@
"extract-zip": "^2.0.1",
"locales": "workspace:*",
"lodash-es": "^4.17.21",
"logger": "workspace:^",
"monaco-editor": "^0.52.2",
"prettier": "^3.5.1",
"rotating-file-stream": "^3.2.6",
"sass": "^1.85.0",
"setting": "workspace:^",
"simplebar-vue": "^2.4.0",
"typescript": "^5.7.3",
"unocss": "^0.64.1",

10
packages/logger/common.ts

@ -0,0 +1,10 @@
// 日志级别定义
export enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
FATAL = 5,
OFF = 6,
}

274
packages/logger/main.ts

@ -0,0 +1,274 @@
import { app, ipcMain } from "electron"
import fs from "fs"
import path from "path"
import * as rfs from "rotating-file-stream"
import { LogLevel } from "./common"
// 日志级别名称映射
const LogLevelName: Record<LogLevel, string> = {
[LogLevel.TRACE]: "TRACE",
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
[LogLevel.FATAL]: "FATAL",
[LogLevel.OFF]: "OFF",
}
// 日志颜色映射(控制台输出用)
const LogLevelColor: Record<LogLevel, string> = {
[LogLevel.TRACE]: "\x1b[90m", // 灰色
[LogLevel.DEBUG]: "\x1b[36m", // 青色
[LogLevel.INFO]: "\x1b[32m", // 绿色
[LogLevel.WARN]: "\x1b[33m", // 黄色
[LogLevel.ERROR]: "\x1b[31m", // 红色
[LogLevel.FATAL]: "\x1b[35m", // 紫色
[LogLevel.OFF]: "", // 无色
}
// 重置颜色的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: LogLevel.INFO,
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)
}
}
// 默认实例
const logger = Logger.getInstance()
logger.init()
// 应用退出时关闭日志流
if (process.type === "browser" && app) {
app.on("before-quit", () => {
logger.info("app", "应用关闭")
logger.close()
})
}
export default logger

7
packages/logger/package.json

@ -0,0 +1,7 @@
{
"name": "logger",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

119
packages/logger/preload.ts

@ -0,0 +1,119 @@
import { contextBridge, ipcRenderer } from "electron"
import { LogLevel } from "./common"
/**
*
*/
interface IRendererLogger {
trace(namespace: string, ...messages: any[]): void
debug(namespace: string, ...messages: any[]): void
info(namespace: string, ...messages: any[]): void
warn(namespace: string, ...messages: any[]): void
error(namespace: string, ...messages: any[]): void
fatal(namespace: string, ...messages: any[]): void
setLevel(level: LogLevel): void
}
// 日志级别名称映射
const LogLevelName: Record<LogLevel, string> = {
[LogLevel.TRACE]: "TRACE",
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
[LogLevel.FATAL]: "FATAL",
[LogLevel.OFF]: "OFF",
}
// 日志颜色映射(控制台输出用)
const LogLevelColor: Record<LogLevel, string> = {
[LogLevel.TRACE]: "\x1b[90m", // 灰色
[LogLevel.DEBUG]: "\x1b[36m", // 青色
[LogLevel.INFO]: "\x1b[32m", // 绿色
[LogLevel.WARN]: "\x1b[33m", // 黄色
[LogLevel.ERROR]: "\x1b[31m", // 红色
[LogLevel.FATAL]: "\x1b[35m", // 紫色
[LogLevel.OFF]: "", // 无色
}
// 重置颜色的ANSI代码
const RESET_COLOR = "\x1b[0m"
/**
*
*/
const createRendererLogger = (): IRendererLogger => {
// 当前日志级别
let currentLevel: LogLevel = LogLevel.INFO
// 格式化消息
const formatMessages = (messages: any[]): string => {
return messages.map(msg => {
if (typeof msg === "object") {
try {
return JSON.stringify(msg)
} catch (e) {
return String(msg)
}
}
return String(msg)
}).join(" ")
}
// 本地打印日志
const printLog = (level: LogLevel, namespace: string, ...messages: any[]): void => {
// 检查日志级别
if (level < currentLevel || level === LogLevel.OFF) return
const timestamp = new Date().toISOString()
const levelName = LogLevelName[level]
const prefix = `[${timestamp}] [${namespace}] [${levelName}]`
const message = formatMessages(messages)
// 输出到控制台
const color = LogLevelColor[level]
console.log(`${color}${prefix} ${message}${RESET_COLOR}`)
}
// 通过IPC发送日志到主进程
const sendLog = (level: LogLevel, namespace: string, ...messages: any[]) => {
// 本地打印
printLog(level, namespace, ...messages)
// 发送到主进程
ipcRenderer.send("logger:log", level, namespace, ...messages)
}
return {
trace(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.TRACE, namespace, ...messages)
},
debug(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.DEBUG, namespace, ...messages)
},
info(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.INFO, namespace, ...messages)
},
warn(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.WARN, namespace, ...messages)
},
error(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.ERROR, namespace, ...messages)
},
fatal(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.FATAL, namespace, ...messages)
},
setLevel(level: LogLevel): void {
// 更新本地日志级别
currentLevel = level
// 设置日志级别(可选,如果需要在渲染进程中动态调整日志级别)
ipcRenderer.send("logger:setLevel", level)
},
}
}
// 暴露logger对象到渲染进程全局
contextBridge.exposeInMainWorld("logger", createRendererLogger())
// 导出类型定义,方便在渲染进程中使用
export type { IRendererLogger }

14
src/main/modules/setting/index.ts → packages/setting/main.ts

@ -2,11 +2,10 @@ import fs from "fs-extra"
import { app } from "electron"
import path from "path"
import { cloneDeep } from "lodash"
import { injectable } from "inversify"
import Config from "config"
import type { IDefaultConfig } from "config"
import _debug from "debug"
import BaseClass from "main/base/base"
import logger from "logger/main"
const debug = _debug("app:setting")
@ -59,18 +58,13 @@ function isEmptyDir(fPath: string) {
}
}
@injectable()
class Setting extends BaseClass {
class SettingClass {
constructor() {
super()
logger.info("setting", "aaaa")
debug(`Setting inited`)
this.init()
}
destroy() {
// TODO
}
#cb: [IT, IOnFunc][] = []
onChange(fn: IOnFunc, that?: any)
@ -232,5 +226,7 @@ class Setting extends BaseClass {
}
}
const Setting = new SettingClass()
export default Setting
export { Setting }

7
packages/setting/package.json

@ -0,0 +1,7 @@
{
"name": "setting",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

10
pnpm-lock.yaml

@ -96,6 +96,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
logger:
specifier: workspace:^
version: link:packages/logger
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
@ -108,6 +111,9 @@ importers:
sass:
specifier: ^1.85.0
version: 1.85.0
setting:
specifier: workspace:^
version: link:packages/setting
simplebar-vue:
specifier: ^2.4.0
version: 2.4.0(vue@3.5.13(typescript@5.7.3))
@ -153,6 +159,10 @@ importers:
packages/locales: {}
packages/logger: {}
packages/setting: {}
packages:
7zip-bin@5.2.0:

2
src/main/App.ts

@ -3,7 +3,7 @@ import { inject, injectable } from "inversify"
// import DB from "./modules/db"
import Api from "./modules/api"
import WindowManager from "./modules/window-manager"
import { app, nativeTheme, protocol } from "electron"
import { app, protocol } from "electron"
import { electronApp } from "@electron-toolkit/utils"
import Command from "./modules/commands"
import BaseClass from "./base/base"

77
src/main/index.ts

@ -1,4 +1,7 @@
import "reflect-metadata"
import "logger/main"
import "setting/main"
import { _ioc } from "main/_ioc"
import { App } from "main/App"
@ -14,61 +17,45 @@ console.log(`日志地址:${logsPath}`)
const LOG_ROOT = path.join(logsPath)
// 缓存已创建的文件流(避免重复创建)
const streams = new Map()
// 缓存当前应用启动的日志文件流
let currentLogStream: rfs.RotatingFileStream | null = null
// 转换命名空间为安全路径
function sanitizeNamespace(namespace) {
return namespace
.split(":") // 按层级分隔符拆分
.map(part => part.replace(/[\\/:*?"<>|]/g, "_")) // 替换非法字符
.join(path.sep) // 拼接为系统路径分隔符(如 / 或 \)
// 生成当前启动时的日志文件名
const getLogFileName = () => {
const now = new Date()
const timestamp = now.toISOString().replace(/[:.]/g, '-')
return `app-${timestamp}.log`
}
// 覆盖 debug.log 方法
const originalLog = debug.log
debug.log = function (...args) {
// 保留原始控制台输出(可选)
// 保留原始控制台输出
originalLog.apply(this, args)
// 获取当前命名空间
// @ts-ignore ...
const namespace = this.namespace
if (!namespace) {
// TODO 增加容错机制,如果没有命名空间就输出到一个默认文件中
return
}
// 生成日志文件路径(示例:logs/app/server.log)
const sanitizedPath = sanitizeNamespace(namespace)
// const logFilePath = path.join(LOG_ROOT, `${sanitizedPath}.log`)
const today = new Date().toISOString().split("T")[0]
const logFilePath = path.join(LOG_ROOT, sanitizedPath, `${today}.log`)
// 确保目录存在
const dir = path.dirname(logFilePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }) // 自动创建多级目录
// 确保日志目录存在
if (!fs.existsSync(LOG_ROOT)) {
fs.mkdirSync(LOG_ROOT, { recursive: true })
}
// 获取或创建文件流
let stream = streams.get(logFilePath)
if (!stream) {
// stream = fs.createWriteStream(logFilePath, { flags: "a" }) // 追加模式
stream = rfs.createStream(path.parse(logFilePath).base, {
path: dir,
// 延迟初始化日志流,直到第一次写入
if (!currentLogStream) {
const logFileName = getLogFileName()
currentLogStream = rfs.createStream(logFileName, {
path: LOG_ROOT,
size: "10M", // 单个文件最大 10MB
rotate: 5, // 保留最近 5 个文件
rotate: 10, // 保留最近 10 个文件
})
streams.set(logFilePath, stream)
}
// 写入日志(添加时间戳)
const message = args.join(" ")
stream.write(`${message}\n`)
// 获取当前命名空间
// @ts-ignore
const namespace = this.namespace || 'unknown'
// const timestamp = new Date().toISOString()
// stream.write(`[${timestamp}] ${message}\n`)
// 写入日志(添加时间戳和命名空间)
const timestamp = new Date().toISOString()
const message = args.join(" ")
currentLogStream.write(`[${timestamp}] [${namespace}] ${message}\n`)
}
const curApp = _ioc.get(App)
@ -77,9 +64,9 @@ curApp.init()
const _debug = debug("app:app")
app.on("before-quit", () => {
_debug("应用关闭")
streams.forEach(stream => {
stream.end()
stream.destroy()
})
streams.clear()
if (currentLogStream) {
currentLogStream.end()
currentLogStream.destroy()
currentLogStream = null
}
})

3
src/main/modules/_ioc.ts

@ -1,5 +1,4 @@
import { Container, ContainerModule } from "inversify"
import { Setting } from "./setting"
import { DB } from "./db"
import { Api } from "./api"
import { WindowManager } from "./window-manager"
@ -9,7 +8,6 @@ import Zephyr from "./zephyr"
import Updater from "./updater"
const modules = new ContainerModule(bind => {
bind(Setting).toConstantValue(new Setting())
bind(Zephyr).toSelf().inSingletonScope()
bind(Updater).toSelf().inSingletonScope()
bind(Api).toSelf().inSingletonScope()
@ -21,7 +19,6 @@ const modules = new ContainerModule(bind => {
async function destroyAllModules(ioc: Container) {
await Promise.all([
ioc.get(Setting).destroy(),
ioc.get(WindowManager).destroy(),
ioc.get(Commands).destroy(),
ioc.get(Updater).destroy(),

8
src/main/modules/db/index.ts

@ -1,5 +1,5 @@
import { inject, injectable } from "inversify"
import Setting from "../setting"
import Setting from "setting/main"
import { CustomAdapter, CustomLow } from "./custom"
import path from "node:path"
import BaseClass from "main/base/base"
@ -14,7 +14,7 @@ class DB extends BaseClass {
}
Modules: Record<string, CustomLow<any>> = {}
constructor(@inject(Setting) private _setting: Setting) {
constructor() {
super()
}
@ -31,12 +31,12 @@ class DB extends BaseClass {
getDB(dbName: string) {
if (this.Modules[dbName] === undefined) {
const filepath = path.resolve(this._setting.values("storagePath"), "./db/" + dbName + ".json")
const filepath = path.resolve(Setting.values("storagePath"), "./db/" + dbName + ".json")
this.Modules[dbName] = this.create(filepath)
return this.Modules[dbName]
} else {
const cur = this.Modules[dbName]
const filepath = path.resolve(this._setting.values("storagePath"), "./db/" + dbName + ".json")
const filepath = path.resolve(Setting.values("storagePath"), "./db/" + dbName + ".json")
if (cur.filepath != filepath) {
this.Modules[dbName] = this.create(filepath)
}

1
src/preload/index.ts

@ -1,6 +1,7 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"
import { electronAPI } from "@electron-toolkit/preload"
import { call, callLong, callSync } from "./call"
import "logger/preload"
import { IPopupMenuOption } from "#/popup-menu"
document.addEventListener("DOMContentLoaded", () => {
const initStyle = document.createElement("style")

6
src/renderer/src/App.vue

@ -1,4 +1,8 @@
<script setup lang="ts"></script>
<script setup lang="ts">
logger.info('App.vue')
console.log(222);
</script>
<template>
<div h-full flex flex-col overflow-hidden>

5
src/types/logger.d.ts

@ -0,0 +1,5 @@
declare const logger: import("logger/preload").IRendererLogger
interface Window {
logger: import("logger/preload").IRendererLogger
}

11
tsconfig.node.json

@ -5,8 +5,11 @@
"src/main/**/*",
"src/preload/**/*",
"config/**/*",
"src/types/**/*",
"packages/locales/main.ts",
"packages/setting/main.ts",
"packages/logger/main.ts",
"packages/logger/preload.ts",
"packages/logger/common.ts",
"src/common/**/*.main.ts",
"src/common/**/main/**/*",
"src/common/**/main.ts",
@ -47,6 +50,12 @@
"locales/*": [
"packages/locales/*"
],
"setting/*": [
"packages/setting/*"
],
"logger/*": [
"packages/logger/*"
],
}
}
}

6
tsconfig.web.json

@ -7,7 +7,9 @@
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"packages/locales/**/*.ts",
"src/preload/*.d.ts",
"packages/setting/**/*.ts",
"packages/logger/**/*.ts",
"src/preload/**/*",
"src/types/**/*",
"config/**/*",
"./typed-router.d.ts",
@ -15,6 +17,8 @@
],
"exclude": [
"packages/locales/main.ts",
"packages/setting/main.ts",
"packages/logger/main.ts",
"src/common/**/main/**/*",
"src/common/**/*.main.ts",
"src/common/**/main.ts"

Loading…
Cancel
Save