"""项目级日志配置模块。 提供统一的日志初始化入口,支持同时输出到终端、文件以及 SQLite 数据库中的 `run_log` 表。数据库写入便于在 UI 中或离线复盘时查看 运行轨迹。 """ from __future__ import annotations import logging import os 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 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: int = logging.INFO, 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() env_level = os.getenv("LLM_QUANT_LOG_LEVEL") if env_level is None: level = logging.DEBUG else: try: level = getattr(logging, env_level.upper()) except AttributeError: logging.getLogger(_LOGGER_NAME).warning( "非法的日志级别 %s,回退到 DEBUG", env_level ) level = logging.DEBUG cfg = get_config() 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(level) root.handlers.clear() formatter = _build_formatter() console_handler = logging.StreamHandler(stream=sys.stdout) console_handler.setLevel(console_level or level) console_handler.setFormatter(formatter) root.addHandler(console_handler) file_handler = logging.FileHandler(logfile, encoding="utf-8") file_handler.setLevel(file_level or level) file_handler.setFormatter(formatter) root.addHandler(file_handler) db_handler = DatabaseLogHandler(level=db_level or level) db_handler.setFormatter(formatter) root.addHandler(db_handler) _IS_CONFIGURED = True cfg = get_config() 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), }, }, ) conversation_logger = logging.getLogger(_CONVERSATION_LOGGER_NAME) conversation_logger.setLevel(level) conversation_logger.handlers.clear() conversation_logger.propagate = False conv_handler = logging.FileHandler(conversation_logfile, encoding="utf-8") conv_handler.setLevel(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()