From c2eac24175cb613cfdf87aa6be317c6b1413e550 Mon Sep 17 00:00:00 2001 From: snaily Date: Fri, 18 Apr 2025 21:53:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9A=84=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入可配置的日志级别功能,允许用户通过配置编辑器和 `.env` 文件设置所需的日志详细程度。 主要变化: - 在 `.env.example` 和 `app/config/config.py` 中添加了 `LOG_LEVEL` 设置。 - 修改了 `app/log/logger.py`,使其从设置中读取日志级别,并实现了对现有 logger 进行动态日志级别更新的功能。 - 更新了 `app/router/config_routes.py`,以便在保存配置后触发日志级别更新。 - 在 `app/templates/config_editor.html` 和 `app/static/js/config_editor.js` 中添加了日志级别选择的 UI 元素。 - 将 `app/router/gemini_routes.py` 和 `app/router/openai_routes.py` 中的一些日志调用从 `info` 调整为 `debug`,以降低默认输出的详细程度。 - 在 `README.md` 的“特别鸣谢”部分添加了 🎉 表情符号。 --- .env.example | 4 ++++ README.md | 2 +- app/config/config.py | 15 +++++++++++-- app/log/logger.py | 37 +++++++++++++++++++++++++++----- app/router/config_routes.py | 9 ++++++-- app/router/gemini_routes.py | 4 ++-- app/router/openai_routes.py | 2 +- app/static/js/config_editor.js | 8 ++++++- app/templates/config_editor.html | 23 ++++++++++++++++++++ 9 files changed, 90 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 8c9bedc..183215b 100644 --- a/.env.example +++ b/.env.example @@ -38,3 +38,7 @@ STREAM_SHORT_TEXT_THRESHOLD=10 STREAM_LONG_TEXT_THRESHOLD=50 STREAM_CHUNK_SIZE=5 ########################################################################## +######################### 日志配置 ####################################### +# 日志级别 (debug, info, warning, error, critical),默认为 info +LOG_LEVEL=info +########################################################################## diff --git a/README.md b/README.md index 0b91f3d..094bac3 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ app/ 欢迎提交 Pull Request 或 Issue。 -## 特别鸣谢 +## 🎉 特别鸣谢 特别鸣谢以下项目和平台为本项目提供图床服务: diff --git a/app/config/config.py b/app/config/config.py index 7043cb3..c0f8bd8 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -10,13 +10,14 @@ from pydantic_settings import BaseSettings from sqlalchemy import insert, update, select from app.core.constants import API_VERSION, DEFAULT_CREATE_IMAGE_MODEL, DEFAULT_FILTER_MODELS, DEFAULT_MODEL, DEFAULT_STREAM_CHUNK_SIZE, DEFAULT_STREAM_LONG_TEXT_THRESHOLD, DEFAULT_STREAM_MAX_DELAY, DEFAULT_STREAM_MIN_DELAY, DEFAULT_STREAM_SHORT_TEXT_THRESHOLD, DEFAULT_TIMEOUT, MAX_RETRIES -from app.log.logger import get_config_logger +from app.log.logger import Logger +# from app.log.logger import get_config_logger # 移除顶层导入 # 延迟导入以避免循环依赖,仅在 sync_initial_settings 中使用 # from app.database.connection import database # from app.database.models import Settings as SettingsModel # from app.database.services import get_all_settings # get_all_settings 可能不适合启动时调用,直接查询 -logger = get_config_logger() +# logger = get_config_logger() # 移除顶层初始化 class Settings(BaseSettings): @@ -67,6 +68,9 @@ class Settings(BaseSettings): CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时 TIMEZONE: str = "Asia/Shanghai" # 默认时区 + # 日志配置 + LOG_LEVEL: str = "INFO" # 默认日志级别 + def __init__(self, **kwargs): super().__init__(**kwargs) # 设置默认AUTH_TOKEN(如果未提供) @@ -78,6 +82,8 @@ settings = Settings() def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: """尝试将数据库字符串值解析为目标 Python 类型""" + from app.log.logger import get_config_logger # 函数内导入 + logger = get_config_logger() # 函数内初始化 try: if target_type == List[str]: # 尝试解析 JSON 列表,如果失败则按逗号分割 @@ -110,6 +116,8 @@ async def sync_initial_settings(): 2. 将数据库设置合并到内存 settings (数据库优先)。 3. 将最终的内存 settings 同步回数据库。 """ + from app.log.logger import get_config_logger # 函数内导入 + logger = get_config_logger() # 函数内初始化 # 延迟导入以避免循环依赖和确保数据库连接已初始化 from app.database.connection import database from app.database.models import Settings as SettingsModel @@ -258,6 +266,9 @@ async def sync_initial_settings(): else: logger.info("No setting changes detected between memory and database during initial sync.") + # 刷新日志等级 + Logger.update_log_levels(final_memory_settings.get("LOG_LEVEL")) + except Exception as e: logger.error(f"An unexpected error occurred during initial settings sync: {e}") finally: diff --git a/app/log/logger.py b/app/log/logger.py index d105589..1ccdbb1 100644 --- a/app/log/logger.py +++ b/app/log/logger.py @@ -56,20 +56,28 @@ class Logger: @staticmethod def setup_logger( - name: str, - level: str = "debug", + name: str ) -> logging.Logger: """ 设置并获取logger :param name: logger名称 - :param level: 日志级别 :return: logger实例 """ + # 导入 settings 对象 + from app.config.config import settings + # 从全局配置获取日志级别 + log_level_str = settings.LOG_LEVEL.lower() + level = LOG_LEVELS.get(log_level_str, logging.INFO) + if name in Logger._loggers: - return Logger._loggers[name] + # 如果 logger 已存在,检查并更新其级别(如果需要) + existing_logger = Logger._loggers[name] + if existing_logger.level != level: + existing_logger.setLevel(level) + return existing_logger logger = logging.getLogger(name) - logger.setLevel(LOG_LEVELS.get(level.lower(), logging.INFO)) + logger.setLevel(level) logger.propagate = False # 添加控制台输出 @@ -90,6 +98,25 @@ class Logger: return Logger._loggers.get(name) + @staticmethod + def update_log_levels(log_level: str): + """ + 根据当前的全局配置更新所有已创建 logger 的日志级别。 + """ + log_level_str = log_level.lower() + new_level = LOG_LEVELS.get(log_level_str, logging.INFO) + + updated_count = 0 + for logger_name, logger_instance in Logger._loggers.items(): + if logger_instance.level != new_level: + logger_instance.setLevel(new_level) + # 可选:记录级别变更日志,但注意避免在日志模块内部产生过多日志 + # print(f"Updated log level for logger '{logger_name}' to {log_level_str.upper()}") + updated_count += 1 + # if updated_count > 0: + # print(f"Updated log level for {updated_count} loggers to {log_level_str.upper()}.") + + # 预定义的loggers def get_openai_logger(): return Logger.setup_logger("openai") diff --git a/app/router/config_routes.py b/app/router/config_routes.py index 4385260..4f6fa5e 100644 --- a/app/router/config_routes.py +++ b/app/router/config_routes.py @@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Request from fastapi.responses import RedirectResponse from app.core.security import verify_auth_token -from app.log.logger import get_config_routes_logger +from app.log.logger import get_config_routes_logger, Logger # 导入 Logger 类 from app.service.config.config_service import ConfigService # 创建路由 @@ -31,8 +31,13 @@ async def update_config(config_data: Dict[str, Any], request: Request): logger.warning("Unauthorized access attempt to config page") return RedirectResponse(url="/", status_code=302) try: - return await ConfigService.update_config(config_data) + result = await ConfigService.update_config(config_data) + # 配置更新成功后,立即更新所有 logger 的级别 + Logger.update_log_levels(config_data["LOG_LEVEL"]) + logger.info("Log levels updated after configuration change.") # 添加日志记录 + return result except Exception as e: + logger.error(f"Error updating config or log levels: {e}", exc_info=True) # 记录详细错误 raise HTTPException(status_code=400, detail=str(e)) diff --git a/app/router/gemini_routes.py b/app/router/gemini_routes.py index 0d0cc7a..c57aa8f 100644 --- a/app/router/gemini_routes.py +++ b/app/router/gemini_routes.py @@ -99,7 +99,7 @@ async def generate_content( """非流式生成内容""" logger.info("-" * 50 + "gemini_generate_content" + "-" * 50) logger.info(f"Handling Gemini content generation request for model: {model_name}") - logger.info(f"Request: \n{request.model_dump_json(indent=2)}") + logger.debug(f"Request: \n{request.model_dump_json(indent=2)}") logger.info(f"Using API key: {api_key}") if not model_service.check_model_support(model_name): @@ -130,7 +130,7 @@ async def stream_generate_content( """流式生成内容""" logger.info("-" * 50 + "gemini_stream_generate_content" + "-" * 50) logger.info(f"Handling Gemini streaming content generation for model: {model_name}") - logger.info(f"Request: \n{request.model_dump_json(indent=2)}") + logger.debug(f"Request: \n{request.model_dump_json(indent=2)}") logger.info(f"Using API key: {api_key}") if not model_service.check_model_support(model_name): diff --git a/app/router/openai_routes.py b/app/router/openai_routes.py index cd9baf3..4757468 100644 --- a/app/router/openai_routes.py +++ b/app/router/openai_routes.py @@ -75,7 +75,7 @@ async def chat_completion( api_key = await key_manager.get_paid_key() logger.info("-" * 50 + "chat_completion" + "-" * 50) logger.info(f"Handling chat completion request for model: {request.model}") - logger.info(f"Request: \n{request.model_dump_json(indent=2)}") + logger.debug(f"Request: \n{request.model_dump_json(indent=2)}") logger.info(f"Using API key: {api_key}") if not model_service.check_model_support(request.model): diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 63ebb4a..427eebf 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -290,7 +290,12 @@ function populateForm(config) { element.checked = value; } else { // 处理其他类型 (确保 value 不是 null 或 undefined) - element.value = value ?? ''; // 使用空字符串作为默认值 + // 特别处理 LOG_LEVEL,确保大小写匹配 option 的 value + if (key === 'LOG_LEVEL' && typeof value === 'string') { + element.value = value.toUpperCase(); + } else { + element.value = value ?? ''; // 使用空字符串作为默认值 + } } } // 如果既不是数组,也找不到对应 ID 的元素,则忽略该配置项 @@ -531,6 +536,7 @@ function collectFormData() { if (input.type === 'number') { formData[input.name] = parseFloat(input.value); } else { + // 确保 select 元素的值也被正确收集 formData[input.name] = input.value; } } diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index dba1d19..e1b1a82 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -92,6 +92,9 @@ + @@ -395,6 +398,26 @@ 定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC" + + +
+

+ 日志配置 +

+ + +
+ + + 设置应用程序的日志记录详细程度 +
+