feat: 添加可配置的日志级别

引入可配置的日志级别功能,允许用户通过配置编辑器和 `.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` 的“特别鸣谢”部分添加了 🎉 表情符号。
This commit is contained in:
snaily
2025-04-18 21:53:54 +08:00
parent 76937aa24f
commit c2eac24175
9 changed files with 90 additions and 14 deletions
+4
View File
@@ -38,3 +38,7 @@ STREAM_SHORT_TEXT_THRESHOLD=10
STREAM_LONG_TEXT_THRESHOLD=50 STREAM_LONG_TEXT_THRESHOLD=50
STREAM_CHUNK_SIZE=5 STREAM_CHUNK_SIZE=5
########################################################################## ##########################################################################
######################### 日志配置 #######################################
# 日志级别 (debug, info, warning, error, critical),默认为 info
LOG_LEVEL=info
##########################################################################
+1 -1
View File
@@ -199,7 +199,7 @@ app/
欢迎提交 Pull Request 或 Issue。 欢迎提交 Pull Request 或 Issue。
## 特别鸣谢 ## 🎉 特别鸣谢
特别鸣谢以下项目和平台为本项目提供图床服务: 特别鸣谢以下项目和平台为本项目提供图床服务:
+13 -2
View File
@@ -10,13 +10,14 @@ from pydantic_settings import BaseSettings
from sqlalchemy import insert, update, select 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.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 中使用 # 延迟导入以避免循环依赖,仅在 sync_initial_settings 中使用
# from app.database.connection import database # from app.database.connection import database
# from app.database.models import Settings as SettingsModel # from app.database.models import Settings as SettingsModel
# from app.database.services import get_all_settings # get_all_settings 可能不适合启动时调用,直接查询 # from app.database.services import get_all_settings # get_all_settings 可能不适合启动时调用,直接查询
logger = get_config_logger() # logger = get_config_logger() # 移除顶层初始化
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -67,6 +68,9 @@ class Settings(BaseSettings):
CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时 CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时
TIMEZONE: str = "Asia/Shanghai" # 默认时区 TIMEZONE: str = "Asia/Shanghai" # 默认时区
# 日志配置
LOG_LEVEL: str = "INFO" # 默认日志级别
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# 设置默认AUTH_TOKEN(如果未提供) # 设置默认AUTH_TOKEN(如果未提供)
@@ -78,6 +82,8 @@ settings = Settings()
def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any:
"""尝试将数据库字符串值解析为目标 Python 类型""" """尝试将数据库字符串值解析为目标 Python 类型"""
from app.log.logger import get_config_logger # 函数内导入
logger = get_config_logger() # 函数内初始化
try: try:
if target_type == List[str]: if target_type == List[str]:
# 尝试解析 JSON 列表,如果失败则按逗号分割 # 尝试解析 JSON 列表,如果失败则按逗号分割
@@ -110,6 +116,8 @@ async def sync_initial_settings():
2. 将数据库设置合并到内存 settings (数据库优先)。 2. 将数据库设置合并到内存 settings (数据库优先)。
3. 将最终的内存 settings 同步回数据库。 3. 将最终的内存 settings 同步回数据库。
""" """
from app.log.logger import get_config_logger # 函数内导入
logger = get_config_logger() # 函数内初始化
# 延迟导入以避免循环依赖和确保数据库连接已初始化 # 延迟导入以避免循环依赖和确保数据库连接已初始化
from app.database.connection import database from app.database.connection import database
from app.database.models import Settings as SettingsModel from app.database.models import Settings as SettingsModel
@@ -258,6 +266,9 @@ async def sync_initial_settings():
else: else:
logger.info("No setting changes detected between memory and database during initial sync.") 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: except Exception as e:
logger.error(f"An unexpected error occurred during initial settings sync: {e}") logger.error(f"An unexpected error occurred during initial settings sync: {e}")
finally: finally:
+32 -5
View File
@@ -56,20 +56,28 @@ class Logger:
@staticmethod @staticmethod
def setup_logger( def setup_logger(
name: str, name: str
level: str = "debug",
) -> logging.Logger: ) -> logging.Logger:
""" """
设置并获取logger 设置并获取logger
:param name: logger名称 :param name: logger名称
:param level: 日志级别
:return: logger实例 :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: 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 = logging.getLogger(name)
logger.setLevel(LOG_LEVELS.get(level.lower(), logging.INFO)) logger.setLevel(level)
logger.propagate = False logger.propagate = False
# 添加控制台输出 # 添加控制台输出
@@ -90,6 +98,25 @@ class Logger:
return Logger._loggers.get(name) 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 # 预定义的loggers
def get_openai_logger(): def get_openai_logger():
return Logger.setup_logger("openai") return Logger.setup_logger("openai")
+7 -2
View File
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from app.core.security import verify_auth_token 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 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") logger.warning("Unauthorized access attempt to config page")
return RedirectResponse(url="/", status_code=302) return RedirectResponse(url="/", status_code=302)
try: 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: 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)) raise HTTPException(status_code=400, detail=str(e))
+2 -2
View File
@@ -99,7 +99,7 @@ async def generate_content(
"""非流式生成内容""" """非流式生成内容"""
logger.info("-" * 50 + "gemini_generate_content" + "-" * 50) logger.info("-" * 50 + "gemini_generate_content" + "-" * 50)
logger.info(f"Handling Gemini content generation request for model: {model_name}") 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}") logger.info(f"Using API key: {api_key}")
if not model_service.check_model_support(model_name): 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("-" * 50 + "gemini_stream_generate_content" + "-" * 50)
logger.info(f"Handling Gemini streaming content generation for model: {model_name}") 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}") logger.info(f"Using API key: {api_key}")
if not model_service.check_model_support(model_name): if not model_service.check_model_support(model_name):
+1 -1
View File
@@ -75,7 +75,7 @@ async def chat_completion(
api_key = await key_manager.get_paid_key() api_key = await key_manager.get_paid_key()
logger.info("-" * 50 + "chat_completion" + "-" * 50) logger.info("-" * 50 + "chat_completion" + "-" * 50)
logger.info(f"Handling chat completion request for model: {request.model}") 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}") logger.info(f"Using API key: {api_key}")
if not model_service.check_model_support(request.model): if not model_service.check_model_support(request.model):
+7 -1
View File
@@ -290,7 +290,12 @@ function populateForm(config) {
element.checked = value; element.checked = value;
} else { } else {
// 处理其他类型 (确保 value 不是 null 或 undefined) // 处理其他类型 (确保 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 的元素,则忽略该配置项 // 如果既不是数组,也找不到对应 ID 的元素,则忽略该配置项
@@ -531,6 +536,7 @@ function collectFormData() {
if (input.type === 'number') { if (input.type === 'number') {
formData[input.name] = parseFloat(input.value); formData[input.name] = parseFloat(input.value);
} else { } else {
// 确保 select 元素的值也被正确收集
formData[input.name] = input.value; formData[input.name] = input.value;
} }
} }
+23
View File
@@ -92,6 +92,9 @@
<button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="scheduler"> <button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="scheduler">
定时任务 定时任务
</button> </button>
<button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="logging">
日志配置
</button>
</div> </div>
<!-- Save Status Banner (Removed - using notification component now) --> <!-- Save Status Banner (Removed - using notification component now) -->
@@ -395,6 +398,26 @@
<small class="text-gray-500 mt-1 block">定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC"</small> <small class="text-gray-500 mt-1 block">定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC"</small>
</div> </div>
</div> </div>
<!-- 日志配置 -->
<div class="config-section bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="logging-section">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-file-alt text-primary-600"></i> 日志配置
</h2>
<!-- 日志级别 -->
<div class="mb-6">
<label for="LOG_LEVEL" class="block font-semibold mb-2 text-gray-700">日志级别</label>
<select id="LOG_LEVEL" name="LOG_LEVEL" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white">
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
<small class="text-gray-500 mt-1 block">设置应用程序的日志记录详细程度</small>
</div>
</div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-col md:flex-row justify-center gap-4 mt-8"> <div class="flex flex-col md:flex-row justify-center gap-4 mt-8">