From 5204438c0c46a245c183e601e0dc5f00c441e7be Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:37:48 +0800 Subject: [PATCH] refactor[Logger]: filtering logs with environment variable (#8299) refactor(Logger): enhance logging with environment variable support - Updated LoggerService to utilize environment variables for filtering logs by level and module in development mode. - Modified the logging level handling to use constants from the logger configuration. - Enhanced documentation to include details on using environment variables for log filtering in both English and Chinese documentation files. - Cleaned up unused type definitions related to logging. --- docs/technical/how-to-use-logger-en.md | 26 +++++- docs/technical/how-to-use-logger-zh.md | 24 ++++++ package.json | 2 +- packages/shared/config/logger.ts | 28 +++++++ packages/shared/config/types.ts | 9 --- src/main/services/LoggerService.ts | 81 ++++++++++++++----- src/preload/index.ts | 2 +- src/renderer/src/services/LoggerService.ts | 92 +++++++++++++++------- 8 files changed, 204 insertions(+), 60 deletions(-) create mode 100644 packages/shared/config/logger.ts diff --git a/docs/technical/how-to-use-logger-en.md b/docs/technical/how-to-use-logger-en.md index fea268a6af..0e77c883c4 100644 --- a/docs/technical/how-to-use-logger-en.md +++ b/docs/technical/how-to-use-logger-en.md @@ -135,12 +135,36 @@ If the worker is relatively simple (just one file), you can also use method chai const logger = loggerService.initWindowSource('Worker').withContext('LetsWork') ``` +## Filtering Logs with Environment Variables + +In a development environment, you can define environment variables to filter displayed logs by level and module. This helps developers focus on their specific logs and improves development efficiency. + +Environment variables can be set in the terminal or defined in the `.env` file in the project's root directory. The available variables are as follows: + +| Variable Name | Description | +| ------- | ------- | +| CSLOGGER_MAIN_LEVEL | Log level for the `main` process. Logs below this level will not be displayed. | +| CSLOGGER_MAIN_SHOW_MODULES | Filters log modules for the `main` process. Use a comma (`,`) to separate modules. The filter is case-sensitive. Only logs from modules in this list will be displayed. | +| CSLOGGER_RENDERER_LEVEL | Log level for the `renderer` process. Logs below this level will not be displayed. | +| CSLOGGER_RENDERER_SHOW_MODULES | Filters log modules for the `renderer` process. Use a comma (`,`) to separate modules. The filter is case-sensitive. Only logs from modules in this list will be displayed. | + +Example: + +```bash +CSLOGGER_MAIN_LEVEL=verbose +CSLOGGER_MAIN_SHOW_MODULES=MCPService,SelectionService +``` + +Note: +- Environment variables are only effective in the development environment. +- These variables only affect the logs displayed in the terminal or DevTools. They do not affect file logging or the `logToMain` recording logic. + ## Log Level Usage Guidelines There are many log levels. The following are the guidelines that should be followed in CherryStudio for when to use each level: (Arranged from highest to lowest log level) -| Log Level | Core Definition & Use Case | Example | +| Log Level | Core Definition & Use case | Example | | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`error`** | **Critical error causing the program to crash or core functionality to become unusable.**
This is the highest-priority log, usually requiring immediate reporting or user notification. | - Main or renderer process crash.
- Failure to read/write critical user data files (e.g., database, configuration files), preventing the application from running.
- All unhandled exceptions. | | **`warn`** | **Potential issue or unexpected situation that does not affect the program's core functionality.**
The program can recover or use a fallback. | - Configuration file `settings.json` is missing; started with default settings.
- Auto-update check failed, but does not affect the use of the current version.
- A non-essential plugin failed to load. | diff --git a/docs/technical/how-to-use-logger-zh.md b/docs/technical/how-to-use-logger-zh.md index 16ac964596..a49f1ef537 100644 --- a/docs/technical/how-to-use-logger-zh.md +++ b/docs/technical/how-to-use-logger-zh.md @@ -136,6 +136,30 @@ logger.info('message', { logToMain: true }) const logger = loggerService.initWindowSource('Worker').withContext('LetsWork') ``` +## 使用环境变量来筛选要显示的日志 + +在开发环境中,可以通过环境变量的定义,来筛选要显示的日志的级别和module。开发者可以专注于自己的日志,提高开发效率。 + +环境变量可以在终端中自行设置,或者在开发根目录的`.env`文件中进行定义,可以定义的变量如下: + +| 变量名 | 含义 | +| ----- | ----- | +| CSLOGGER_MAIN_LEVEL | 用于`main`进程的日志级别,低于该级别的日志将不显示 | +| CSLOGGER_MAIN_SHOW_MODULES | 用于`main`进程的日志module筛选,用`,`分隔,区分大小写。只有在该列表中的module的日志才会显示 | +| CSLOGGER_RENDERER_LEVEL | 用于`renderer`进程的日志级别,低于该级别的日志将不显示 | +| CSLOGGER_RENDERER_SHOW_MODULES | 用于`renderer`进程的日志module筛选,用`,`分隔,区分大小写。只有在该列表中的module的日志才会显示 | + +示例: + +```bash +CSLOGGER_MAIN_LEVEL=vebose +CSLOGGER_MAIN_SHOW_MODULES=MCPService,SelectionService +``` + +注意: +- 环境变量仅在开发环境中生效 +- 该变量仅会改变在终端或在devTools中显示的日志,不会影响文件日志和`logToMain`的记录逻辑 + ## 日志级别的使用规范 日志有很多级别,什么时候应该用哪个级别,下面是在CherryStudio中应该遵循的规范: diff --git a/package.json b/package.json index af52be2595..8d9f6ea844 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "scripts": { "start": "electron-vite preview", - "dev": "electron-vite dev", + "dev": "dotenv electron-vite dev", "debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222", "build": "npm run typecheck && electron-vite build", "build:check": "yarn typecheck && yarn check:i18n && yarn test", diff --git a/packages/shared/config/logger.ts b/packages/shared/config/logger.ts new file mode 100644 index 0000000000..943ecacef8 --- /dev/null +++ b/packages/shared/config/logger.ts @@ -0,0 +1,28 @@ +export type LogSourceWithContext = { + process: 'main' | 'renderer' + window?: string // only for renderer process + module?: string + context?: Record +} + +export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly' | 'none' + +export const LEVEL = { + ERROR: 'error', + WARN: 'warn', + INFO: 'info', + DEBUG: 'debug', + VERBOSE: 'verbose', + SILLY: 'silly', + NONE: 'none' +} satisfies Record + +export const LEVEL_MAP: Record = { + error: 10, + warn: 8, + info: 6, + debug: 4, + verbose: 2, + silly: 0, + none: -1 +} diff --git a/packages/shared/config/types.ts b/packages/shared/config/types.ts index cdb5d0ecab..28bb4acf65 100644 --- a/packages/shared/config/types.ts +++ b/packages/shared/config/types.ts @@ -9,12 +9,3 @@ export type LoaderReturn = { message?: string messageSource?: 'preprocess' | 'embedding' } - -export type LogSourceWithContext = { - process: 'main' | 'renderer' - window?: string // only for renderer process - module?: string - context?: Record -} - -export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly' diff --git a/src/main/services/LoggerService.ts b/src/main/services/LoggerService.ts index c971e0adbf..338af503bd 100644 --- a/src/main/services/LoggerService.ts +++ b/src/main/services/LoggerService.ts @@ -1,4 +1,5 @@ -import type { LogLevel, LogSourceWithContext } from '@shared/config/types' +import type { LogLevel, LogSourceWithContext } from '@shared/config/logger' +import { LEVEL, LEVEL_MAP } from '@shared/config/logger' import { IpcChannel } from '@shared/IpcChannel' import { app, ipcMain } from 'electron' import os from 'os' @@ -38,7 +39,7 @@ const SYSTEM_INFO = { } const APP_VERSION = `v${app?.getVersion?.() || 'unknown'}` -const DEFAULT_LEVEL = isDev ? 'silly' : 'info' +const DEFAULT_LEVEL = isDev ? LEVEL.SILLY : LEVEL.INFO /** * IMPORTANT: How to use LoggerService @@ -50,6 +51,10 @@ class LoggerService { private static instance: LoggerService private logger: winston.Logger + // env variables, only used in dev mode + private envLevel: LogLevel = LEVEL.NONE + private envShowModules: string[] = [] + private logsDir: string = '' private module: string = '' @@ -63,6 +68,34 @@ class LoggerService { // Create logs directory path this.logsDir = path.join(app.getPath('userData'), 'logs') + // env variables, only used in dev mode + // only affect console output, not affect file output + if (isDev) { + // load env level if exists + if ( + process.env.CSLOGGER_MAIN_LEVEL && + Object.values(LEVEL).includes(process.env.CSLOGGER_MAIN_LEVEL as LogLevel) + ) { + this.envLevel = process.env.CSLOGGER_MAIN_LEVEL as LogLevel + // eslint-disable-next-line no-restricted-syntax + console.log(colorText(`[LoggerService] env CSLOGGER_MAIN_LEVEL loaded: ${this.envLevel}`, 'BLUE')) + } + + // load env show module if exists + if (process.env.CSLOGGER_MAIN_SHOW_MODULES) { + const showModules = process.env.CSLOGGER_MAIN_SHOW_MODULES.split(',') + .map((module) => module.trim()) + .filter((module) => module !== '') + if (showModules.length > 0) { + this.envShowModules = showModules + // eslint-disable-next-line no-restricted-syntax + console.log( + colorText(`[LoggerService] env CSLOGGER_MAIN_SHOW_MODULES loaded: ${this.envShowModules.join(' ')}`, 'BLUE') + ) + } + } + } + // Configure transports based on environment const transports: winston.transport[] = [] @@ -89,7 +122,8 @@ class LoggerService { // Configure Winston logger this.logger = winston.createLogger({ - level: DEFAULT_LEVEL, // Development: all levels, Production: info and above + // Development: all levels, Production: info and above + level: DEFAULT_LEVEL, format: winston.format.combine( winston.format.splat(), winston.format.timestamp({ @@ -155,6 +189,15 @@ class LoggerService { */ private processLog(source: LogSourceWithContext, level: LogLevel, message: string, meta: any[]): void { if (isDev) { + // skip if env level is set and current level is less than env level + if (this.envLevel !== LEVEL.NONE && LEVEL_MAP[level] < LEVEL_MAP[this.envLevel]) { + return + } + // skip if env show modules is set and current module is not in the list + if (this.module && this.envShowModules.length > 0 && !this.envShowModules.includes(this.module)) { + return + } + const datetimeColored = colorText( new Date().toLocaleString('zh-CN', { hour: '2-digit', @@ -174,39 +217,39 @@ class LoggerService { } switch (level) { - case 'error': + case LEVEL.ERROR: // eslint-disable-next-line no-restricted-syntax console.error( `${datetimeColored} ${colorText(colorText('', 'RED'), 'BOLD')}${moduleString}${message}`, ...meta ) break - case 'warn': + case LEVEL.WARN: // eslint-disable-next-line no-restricted-syntax console.warn( `${datetimeColored} ${colorText(colorText('', 'YELLOW'), 'BOLD')}${moduleString}${message}`, ...meta ) break - case 'info': + case LEVEL.INFO: // eslint-disable-next-line no-restricted-syntax console.info( `${datetimeColored} ${colorText(colorText('', 'GREEN'), 'BOLD')}${moduleString}${message}`, ...meta ) break - case 'debug': + case LEVEL.DEBUG: // eslint-disable-next-line no-restricted-syntax console.debug( `${datetimeColored} ${colorText(colorText('', 'BLUE'), 'BOLD')}${moduleString}${message}`, ...meta ) break - case 'verbose': + case LEVEL.VERBOSE: // eslint-disable-next-line no-restricted-syntax console.log(`${datetimeColored} ${colorText('', 'BOLD')}${moduleString}${message}`, ...meta) break - case 'silly': + case LEVEL.SILLY: // eslint-disable-next-line no-restricted-syntax console.log(`${datetimeColored} ${colorText('', 'BOLD')}${moduleString}${message}`, ...meta) break @@ -225,7 +268,7 @@ class LoggerService { meta.push(sourceWithContext) // add extra system information for error and warn levels - if (level === 'error' || level === 'warn') { + if (level === LEVEL.ERROR || level === LEVEL.WARN) { const extra = { sys: SYSTEM_INFO, appver: APP_VERSION @@ -241,42 +284,42 @@ class LoggerService { * Log error message */ public error(message: string, ...data: any[]): void { - this.processMainLog('error', message, data) + this.processMainLog(LEVEL.ERROR, message, data) } /** * Log warning message */ public warn(message: string, ...data: any[]): void { - this.processMainLog('warn', message, data) + this.processMainLog(LEVEL.WARN, message, data) } /** * Log info message */ public info(message: string, ...data: any[]): void { - this.processMainLog('info', message, data) + this.processMainLog(LEVEL.INFO, message, data) } /** * Log verbose message */ public verbose(message: string, ...data: any[]): void { - this.processMainLog('verbose', message, data) + this.processMainLog(LEVEL.VERBOSE, message, data) } /** * Log debug message */ public debug(message: string, ...data: any[]): void { - this.processMainLog('debug', message, data) + this.processMainLog(LEVEL.DEBUG, message, data) } /** * Log silly level message */ public silly(message: string, ...data: any[]): void { - this.processMainLog('silly', message, data) + this.processMainLog(LEVEL.SILLY, message, data) } /** @@ -304,7 +347,7 @@ class LoggerService { * Set the minimum log level * @param level - The log level to set */ - public setLevel(level: string): void { + public setLevel(level: LogLevel): void { this.logger.level = level } @@ -312,8 +355,8 @@ class LoggerService { * Get the current log level * @returns The current log level */ - public getLevel(): string { - return this.logger.level + public getLevel(): LogLevel { + return this.logger.level as LogLevel } /** diff --git a/src/preload/index.ts b/src/preload/index.ts index 84cff34e64..1fdaffc096 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -3,7 +3,7 @@ import { electronAPI } from '@electron-toolkit/preload' import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { SpanContext } from '@opentelemetry/api' import { UpgradeChannel } from '@shared/config/constant' -import type { LogLevel, LogSourceWithContext } from '@shared/config/types' +import type { LogLevel, LogSourceWithContext } from '@shared/config/logger' import { IpcChannel } from '@shared/IpcChannel' import { AddMemoryOptions, diff --git a/src/renderer/src/services/LoggerService.ts b/src/renderer/src/services/LoggerService.ts index 74142dc710..849b3feede 100644 --- a/src/renderer/src/services/LoggerService.ts +++ b/src/renderer/src/services/LoggerService.ts @@ -1,22 +1,14 @@ -import type { LogLevel, LogSourceWithContext } from '@shared/config/types' +import type { LogLevel, LogSourceWithContext } from '@shared/config/logger' +import { LEVEL, LEVEL_MAP } from '@shared/config/logger' // check if the current process is a worker const IS_WORKER = typeof window === 'undefined' // check if we are in the dev env +// DO NOT use `constants.ts` here, because the files contains other dependencies that will fail in worker process const IS_DEV = IS_WORKER ? false : window.electron?.process?.env?.NODE_ENV === 'development' -// the level number is different from real definition, it only for convenience -const LEVEL_MAP: Record = { - error: 5, - warn: 4, - info: 3, - verbose: 2, - debug: 1, - silly: 0 -} - -const DEFAULT_LEVEL = IS_DEV ? 'silly' : 'info' -const MAIN_LOG_LEVEL = 'warn' +const DEFAULT_LEVEL = IS_DEV ? LEVEL.SILLY : LEVEL.INFO +const MAIN_LOG_LEVEL = LEVEL.WARN /** * IMPORTANT: How to use LoggerService @@ -27,6 +19,11 @@ const MAIN_LOG_LEVEL = 'warn' class LoggerService { private static instance: LoggerService + // env variables, only used in dev mode + // only affect console output, not affect logToMain + private envLevel: LogLevel = LEVEL.NONE + private envShowModules: string[] = [] + private level: LogLevel = DEFAULT_LEVEL private logToMainLevel: LogLevel = MAIN_LOG_LEVEL @@ -35,7 +32,33 @@ class LoggerService { private context: Record = {} private constructor() { - // + if (IS_DEV) { + if ( + window.electron?.process?.env?.CSLOGGER_RENDERER_LEVEL && + Object.values(LEVEL).includes(window.electron?.process?.env?.CSLOGGER_RENDERER_LEVEL as LogLevel) + ) { + this.envLevel = window.electron?.process?.env?.CSLOGGER_RENDERER_LEVEL as LogLevel + // eslint-disable-next-line no-restricted-syntax + console.log( + `%c[LoggerService] env CSLOGGER_RENDERER_LEVEL loaded: ${this.envLevel}`, + 'color: blue; font-weight: bold' + ) + } + + if (window.electron?.process?.env?.CSLOGGER_RENDERER_SHOW_MODULES) { + const showModules = window.electron?.process?.env?.CSLOGGER_RENDERER_SHOW_MODULES.split(',') + .map((module) => module.trim()) + .filter((module) => module !== '') + if (showModules.length > 0) { + this.envShowModules = showModules + // eslint-disable-next-line no-restricted-syntax + console.log( + `%c[LoggerService] env CSLOGGER_RENDERER_SHOW_MODULES loaded: ${this.envShowModules.join(' ')}`, + 'color: blue; font-weight: bold' + ) + } + } + } } /** @@ -96,36 +119,47 @@ class LoggerService { return } + const currentLevel = LEVEL_MAP[level] + + // if in dev mode, check if the env variables are set and use the env level and show modules to skip logs + if (IS_DEV) { + if (this.envLevel !== LEVEL.NONE && currentLevel < LEVEL_MAP[this.envLevel]) { + return + } + if (this.module && this.envShowModules.length > 0 && !this.envShowModules.includes(this.module)) { + return + } + } + // skip log if level is lower than default level - const levelNumber = LEVEL_MAP[level] - if (levelNumber < LEVEL_MAP[this.level]) { + if (currentLevel < LEVEL_MAP[this.level]) { return } const logMessage = this.module ? `[${this.module}] ${message}` : message switch (level) { - case 'error': + case LEVEL.ERROR: // eslint-disable-next-line no-restricted-syntax console.error(logMessage, ...data) break - case 'warn': + case LEVEL.WARN: // eslint-disable-next-line no-restricted-syntax console.warn(logMessage, ...data) break - case 'info': + case LEVEL.INFO: // eslint-disable-next-line no-restricted-syntax console.info(logMessage, ...data) break - case 'verbose': + case LEVEL.VERBOSE: // eslint-disable-next-line no-restricted-syntax console.log(logMessage, ...data) break - case 'debug': + case LEVEL.DEBUG: // eslint-disable-next-line no-restricted-syntax console.debug(logMessage, ...data) break - case 'silly': + case LEVEL.SILLY: // eslint-disable-next-line no-restricted-syntax console.log(logMessage, ...data) break @@ -134,7 +168,7 @@ class LoggerService { // if the last data is an object with logToMain: true, force log to main const forceLogToMain = data.length > 0 && data[data.length - 1]?.logToMain === true - if (levelNumber >= LEVEL_MAP[this.logToMainLevel] || forceLogToMain) { + if (currentLevel >= LEVEL_MAP[this.logToMainLevel] || forceLogToMain) { const source: LogSourceWithContext = { process: 'renderer', window: this.window, @@ -163,42 +197,42 @@ class LoggerService { * Log error message */ public error(message: string, ...data: any[]): void { - this.processLog('error', message, data) + this.processLog(LEVEL.ERROR, message, data) } /** * Log warning message */ public warn(message: string, ...data: any[]): void { - this.processLog('warn', message, data) + this.processLog(LEVEL.WARN, message, data) } /** * Log info message */ public info(message: string, ...data: any[]): void { - this.processLog('info', message, data) + this.processLog(LEVEL.INFO, message, data) } /** * Log verbose message */ public verbose(message: string, ...data: any[]): void { - this.processLog('verbose', message, data) + this.processLog(LEVEL.VERBOSE, message, data) } /** * Log debug message */ public debug(message: string, ...data: any[]): void { - this.processLog('debug', message, data) + this.processLog(LEVEL.DEBUG, message, data) } /** * Log silly level message */ public silly(message: string, ...data: any[]): void { - this.processLog('silly', message, data) + this.processLog(LEVEL.SILLY, message, data) } /**