"""项目级日志配置模块。 提供统一的日志初始化入口,支持同时输出到终端、文件以及 SQLite 数据库中的 `run_log` 表。数据库写入便于在 UI 中或离线复盘时查看 运行轨迹。 """ from __future__ import annotations import logging import sqlite3 import sys from datetime import datetime from logging import Handler, LogRecord from pathlib import Path from typing import Optional from .config import get_config from .db import db_session _LOGGER_NAME = "app.logging" _IS_CONFIGURED = False _CONVERSATION_LOGGER_NAME = "app.conversation" _CONVERSATION_HANDLER: Optional[Handler] = None _CONVERSATION_LOGFILE: Optional[Path] = None logging.getLogger("watchdog").setLevel(logging.WARNING) class DatabaseLogHandler(Handler): """将日志写入 SQLite `run_log` 表的自定义 Handler。""" def emit(self, record: LogRecord) -> None: # noqa: D401 - 标准 logging 接口 try: message = self.format(record) stage = getattr(record, "stage", None) ts = datetime.utcnow().isoformat(timespec="microseconds") + "Z" with db_session() as conn: conn.execute( "INSERT INTO run_log (ts, stage, level, msg) VALUES (?, ?, ?, ?)", (ts, stage, record.levelname, message), ) except sqlite3.OperationalError as exc: # 表不存在时直接跳过,避免首次初始化阶段报错 if "no such table" not in str(exc).lower(): self.handleError(record) except Exception: self.handleError(record) def _build_formatter() -> logging.Formatter: return logging.Formatter("%(asctime)s %(levelname)s %(name)s - %(message)s") def setup_logging( *, level: Optional[int] = None, console_level: Optional[int] = None, file_level: Optional[int] = None, db_level: Optional[int] = None, ) -> logging.Logger: """配置根 logger。重复调用时将复用已存在的配置。""" global _IS_CONFIGURED if _IS_CONFIGURED: return logging.getLogger() cfg = get_config() resolved_level = logging.DEBUG level_name = getattr(cfg, "log_level", None) if isinstance(level_name, str) and level_name.strip(): normalized_name = level_name.strip() try: resolved_level = getattr(logging, normalized_name.upper()) except AttributeError: try: resolved_level = int(normalized_name) except (TypeError, ValueError): logging.getLogger(_LOGGER_NAME).warning( "非法的日志级别 %s,回退到 DEBUG", normalized_name ) resolved_level = logging.DEBUG elif level is not None: resolved_level = int(level) timestamp = datetime.now().strftime("%Y%m%d_%H%M") log_dir: Path = cfg.data_paths.root / "logs" log_dir.mkdir(parents=True, exist_ok=True) logfile = log_dir / f"app_{timestamp}.log" conversation_logfile = log_dir / f"agent_{timestamp}.log" root = logging.getLogger() root.setLevel(resolved_level) root.handlers.clear() formatter = _build_formatter() console_handler = logging.StreamHandler(stream=sys.stdout) console_handler.setLevel(console_level if console_level is not None else resolved_level) console_handler.setFormatter(formatter) root.addHandler(console_handler) file_handler = logging.FileHandler(logfile, encoding="utf-8") file_handler.setLevel(file_level if file_level is not None else resolved_level) file_handler.setFormatter(formatter) root.addHandler(file_handler) db_handler = DatabaseLogHandler(level=db_level if db_level is not None else resolved_level) db_handler.setFormatter(formatter) root.addHandler(db_handler) _IS_CONFIGURED = True root.info( "日志系统初始化完成", extra={ "stage": "config", "config": { "tushare_token_set": bool(cfg.tushare_token), "force_refresh": cfg.force_refresh, "data_root": str(cfg.data_paths.root), "logfile": str(logfile), "log_level": getattr(cfg, "log_level", resolved_level), }, }, ) conversation_logger = logging.getLogger(_CONVERSATION_LOGGER_NAME) conversation_logger.setLevel(resolved_level) conversation_logger.handlers.clear() conversation_logger.propagate = False conv_handler = logging.FileHandler(conversation_logfile, encoding="utf-8") conv_handler.setLevel(resolved_level) conv_handler.setFormatter(formatter) conversation_logger.addHandler(conv_handler) global _CONVERSATION_HANDLER, _CONVERSATION_LOGFILE _CONVERSATION_HANDLER = conv_handler _CONVERSATION_LOGFILE = conversation_logfile return root def get_logger(name: Optional[str] = None) -> logging.Logger: """返回指定名称的 logger,确保全局配置已就绪。""" setup_logging() logger = logging.getLogger(name) # Quiet noisy third-party loggers when default level is DEBUG logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING) logging.getLogger("requests.packages.urllib3").setLevel(logging.WARNING) return logger def get_conversation_logger() -> logging.Logger: """Return conversation logger for agent dialogues.""" setup_logging() logger = logging.getLogger(_CONVERSATION_LOGGER_NAME) logger.propagate = False return logger # 默认在模块导入时完成配置,适配现有调用方式。 setup_logging()