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.
This commit is contained in:
fullex 2025-07-21 09:37:48 +08:00 committed by GitHub
parent ebe7cce161
commit 5204438c0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 204 additions and 60 deletions

View File

@ -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.** <br> This is the highest-priority log, usually requiring immediate reporting or user notification. | - Main or renderer process crash. <br> - Failure to read/write critical user data files (e.g., database, configuration files), preventing the application from running. <br> - All unhandled exceptions. |
| **`warn`** | **Potential issue or unexpected situation that does not affect the program's core functionality.** <br> The program can recover or use a fallback. | - Configuration file `settings.json` is missing; started with default settings. <br> - Auto-update check failed, but does not affect the use of the current version. <br> - A non-essential plugin failed to load. |

View File

@ -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中应该遵循的规范

View File

@ -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",

View File

@ -0,0 +1,28 @@
export type LogSourceWithContext = {
process: 'main' | 'renderer'
window?: string // only for renderer process
module?: string
context?: Record<string, any>
}
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<string, LogLevel>
export const LEVEL_MAP: Record<LogLevel, number> = {
error: 10,
warn: 8,
info: 6,
debug: 4,
verbose: 2,
silly: 0,
none: -1
}

View File

@ -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<string, any>
}
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly'

View File

@ -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('<ERROR>', 'RED'), 'BOLD')}${moduleString}${message}`,
...meta
)
break
case 'warn':
case LEVEL.WARN:
// eslint-disable-next-line no-restricted-syntax
console.warn(
`${datetimeColored} ${colorText(colorText('<WARN>', 'YELLOW'), 'BOLD')}${moduleString}${message}`,
...meta
)
break
case 'info':
case LEVEL.INFO:
// eslint-disable-next-line no-restricted-syntax
console.info(
`${datetimeColored} ${colorText(colorText('<INFO>', 'GREEN'), 'BOLD')}${moduleString}${message}`,
...meta
)
break
case 'debug':
case LEVEL.DEBUG:
// eslint-disable-next-line no-restricted-syntax
console.debug(
`${datetimeColored} ${colorText(colorText('<DEBUG>', 'BLUE'), 'BOLD')}${moduleString}${message}`,
...meta
)
break
case 'verbose':
case LEVEL.VERBOSE:
// eslint-disable-next-line no-restricted-syntax
console.log(`${datetimeColored} ${colorText('<VERBOSE>', 'BOLD')}${moduleString}${message}`, ...meta)
break
case 'silly':
case LEVEL.SILLY:
// eslint-disable-next-line no-restricted-syntax
console.log(`${datetimeColored} ${colorText('<SILLY>', '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
}
/**

View File

@ -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,

View File

@ -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<LogLevel, number> = {
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<string, any> = {}
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)
}
/**