diff --git a/app/config/config.py b/app/config/config.py index 5fe315e..5d811bd 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -199,7 +199,7 @@ async def sync_initial_settings(): if type_match: setattr(settings, key, parsed_db_value) - logger.info(f"Updated setting '{key}' in memory from database value ({target_type}).") + logger.debug(f"Updated setting '{key}' in memory from database value ({target_type}).") updated_in_memory = True else: logger.warning(f"Parsed DB value type mismatch for key '{key}'. Expected {target_type}, got {type(parsed_db_value)}. Skipping update.") diff --git a/app/core/application.py b/app/core/application.py index b152028..da1bf6d 100644 --- a/app/core/application.py +++ b/app/core/application.py @@ -1,4 +1,5 @@ from contextlib import asynccontextmanager +from pathlib import Path # Add pathlib import from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -9,7 +10,6 @@ from app.middleware.middleware import setup_middlewares from app.exception.exceptions import setup_exception_handlers from app.router.routes import setup_routers from app.service.key.key_manager import get_key_manager_instance -from app.core.initialization import initialize_app from app.database.connection import connect_to_db, disconnect_from_db from app.database.initialization import initialize_database from app.scheduler.key_checker import start_scheduler, stop_scheduler @@ -17,23 +17,30 @@ from app.service.update.update_service import check_for_updates logger = get_application_logger() -VERSION_FILE_PATH = "VERSION" # Path relative to project root +# Define project paths using pathlib +# Assuming this file is at app/core/application.py +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +VERSION_FILE_PATH = PROJECT_ROOT / "VERSION" +STATIC_DIR = PROJECT_ROOT / "app" / "static" +TEMPLATES_DIR = PROJECT_ROOT / "app" / "templates" + def _get_current_version(default_version: str = "0.0.0") -> str: """Reads the current version from the VERSION file.""" + version_file = VERSION_FILE_PATH # Use Path object try: - # Assuming execution from project root d:/develop/pythonProjects/gemini-balance - with open(VERSION_FILE_PATH, 'r', encoding='utf-8') as f: + # Use Path object's open method + with version_file.open('r', encoding='utf-8') as f: version = f.read().strip() if not version: - logger.warning(f"VERSION file ('{VERSION_FILE_PATH}') is empty. Using default version '{default_version}'.") + logger.warning(f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'.") return default_version return version except FileNotFoundError: - logger.warning(f"VERSION file not found at '{VERSION_FILE_PATH}'. Using default version '{default_version}'.") + logger.warning(f"VERSION file not found at '{version_file}'. Using default version '{default_version}'.") return default_version except IOError as e: - logger.error(f"Error reading VERSION file ('{VERSION_FILE_PATH}'): {e}. Using default version '{default_version}'.") + logger.error(f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'.") return default_version # 初始化模板引擎,并添加全局变量 @@ -48,67 +55,87 @@ def update_template_globals(app: FastAPI, update_info: dict): logger.info(f"Update info stored in app.state: {update_info}") -@asynccontextmanager -async def lifespan(app: FastAPI): - """ - 应用程序生命周期管理器 - - Args: - app: FastAPI应用实例 - """ - # 启动事件 - logger.info("Application starting up...") - try: - # 初始化数据库 - initialize_database() - logger.info("Database initialized successfully") - - # 连接到数据库 - await connect_to_db() - - # 同步初始配置(DB优先,然后同步回DB) - await sync_initial_settings() +# --- Helper functions for lifespan --- - # 初始化KeyManager (使用可能已从DB更新的settings) - await get_key_manager_instance(settings.API_KEYS) - logger.info("KeyManager initialized successfully") - except Exception as e: - logger.error(f"Failed to initialize application: {str(e)}") - # 不重新抛出,允许应用继续运行,但记录错误 - # raise # 取消注释以在初始化失败时停止应用 +async def _setup_database_and_config(app_settings): + """Initializes database, syncs settings, and initializes KeyManager.""" + initialize_database() + logger.info("Database initialized successfully") + await connect_to_db() + await sync_initial_settings() + # Initialize KeyManager using potentially updated settings + await get_key_manager_instance(app_settings.API_KEYS) + logger.info("Database, config sync, and KeyManager initialized successfully") - # 检查更新 (在核心初始化之后) - update_available, latest_version, error_message = await check_for_updates() - update_info = { - "update_available": update_available, - "latest_version": latest_version, - "error_message": error_message, - "current_version": _get_current_version() # Read from VERSION file - } - # 将更新信息存储在 app.state 中 - app.state.update_info = update_info - logger.info(f"Update check completed. Info: {update_info}") +async def _shutdown_database(): + """Disconnects from the database.""" + await disconnect_from_db() + logger.info("Disconnected from database.") - - # 启动调度器 (如果初始化成功) +def _start_scheduler(): + """Starts the background scheduler.""" try: start_scheduler() logger.info("Scheduler started successfully.") except Exception as e: logger.error(f"Failed to start scheduler: {e}") - - yield # 应用程序运行期间 - - # 关闭事件 - logger.info("Application shutting down...") - - # 停止调度器 +def _stop_scheduler(): + """Stops the background scheduler.""" stop_scheduler() logger.info("Scheduler stopped.") - # 断开数据库连接 - await disconnect_from_db() +async def _perform_update_check(app: FastAPI): + """Checks for updates and stores the info in app.state.""" + update_available, latest_version, error_message = await check_for_updates() + current_version = _get_current_version() # Read from VERSION file + update_info = { + "update_available": update_available, + "latest_version": latest_version, + "error_message": error_message, + "current_version": current_version + } + # Ensure app.state exists and store update info + if not hasattr(app, "state"): + from starlette.datastructures import State + app.state = State() + app.state.update_info = update_info + logger.info(f"Update check completed. Info: {update_info}") + +# --- Application Lifespan --- + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Manages the application startup and shutdown events. + + Args: + app: FastAPI应用实例 + """ + # Startup events + logger.info("Application starting up...") + try: + # Setup database, config, and KeyManager + await _setup_database_and_config(settings) # Pass settings object + + # Perform update check after core components are ready + await _perform_update_check(app) + + # Start the scheduler + _start_scheduler() + + except Exception as e: + logger.critical(f"Critical error during application startup: {str(e)}", exc_info=True) + # Depending on the severity, you might want to prevent the app from fully starting + # For now, we log critically and let it yield, potentially in a broken state. + # Consider adding more robust error handling here if startup failures should halt the app. + + yield # Application runs + + # Shutdown events + logger.info("Application shutting down...") + _stop_scheduler() + await _shutdown_database() def create_app() -> FastAPI: """ @@ -117,28 +144,33 @@ def create_app() -> FastAPI: Returns: FastAPI: 配置好的FastAPI应用程序实例 """ - # 初始化应用程序 - initialize_app() - + # Removed: initialize_app() call + # 创建FastAPI应用 + # Read version from file for consistency + current_version = _get_current_version() app = FastAPI( title="Gemini Balance API", description="Gemini API代理服务,支持负载均衡和密钥管理", - version="1.0.0", + version=current_version, lifespan=lifespan ) - # 初始化 app.state (如果尚未存在) + # Initialize app.state early to ensure it exists before lifespan potentially uses it if not hasattr(app, "state"): from starlette.datastructures import State app.state = State() - # 确保 update_info 即使在 lifespan 之前访问也不会出错 - app.state.update_info = {"update_available": False, "latest_version": None, "error_message": "Checking...", "current_version": _get_current_version()} # Read from VERSION file for initial state - + # Set a default/initial state for update_info + app.state.update_info = { + "update_available": False, + "latest_version": None, + "error_message": "Initializing...", + "current_version": current_version # Use version read earlier + } # 配置静态文件 - app.mount("/static", StaticFiles(directory="app/static"), name="static") - + app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + # 配置中间件 setup_middlewares(app) diff --git a/app/core/initialization.py b/app/core/initialization.py deleted file mode 100644 index 3e970ee..0000000 --- a/app/core/initialization.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -应用程序初始化模块 -""" -from pathlib import Path -from typing import List - -from app.log.logger import get_initialization_logger - -logger = get_initialization_logger() - - -def ensure_directories_exist(directories: List[str]) -> None: - """ - 确保指定的目录存在,如果不存在则创建 - - Args: - directories: 要确保存在的目录列表 - """ - for directory in directories: - try: - Path(directory).mkdir(parents=True, exist_ok=True) - logger.info(f"Ensured directory exists: {directory}") - except Exception as e: - logger.error(f"Failed to create directory {directory}: {str(e)}") - - -def initialize_app() -> None: - """ - 初始化应用程序,确保所需的目录和文件都存在 - """ - # 确保必要的目录存在 - required_directories = [ - "app/static/css", - "app/static/js", - "app/static/icons", - "app/templates", - ] - - ensure_directories_exist(required_directories) - logger.info("core initialization completed") diff --git a/app/log/logger.py b/app/log/logger.py index 9a19a56..a3491f1 100644 --- a/app/log/logger.py +++ b/app/log/logger.py @@ -35,7 +35,7 @@ class ColoredFormatter(logging.Formatter): # 日志格式 FORMATTER = ColoredFormatter( - "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s" + "%(asctime)s | %(name)-15s | %(levelname)-8s | [%(filename)-20s:%(lineno)-4d] | %(message)s" ) # 日志级别映射