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)
}
/**