# -*- coding: utf-8 -*- """日志配置工具 提供统一的日志配置和格式化。 """ from __future__ import annotations import logging import sys from contextlib import contextmanager from datetime import datetime from pathlib import Path from typing import Iterator, TextIO # 统一日志格式(中文友好) UNIFIED_FORMAT = "[%(asctime)s] %(levelname)-5s | %(name)s | %(message)s" DATE_FORMAT = "%Y-%m-%d %H:%M:%S" class TeeStream: """同时输出到多个流""" def __init__(self, *streams: TextIO) -> None: self._streams = streams def write(self, data: str) -> int: for stream in self._streams: stream.write(data) return len(data) def flush(self) -> None: for stream in self._streams: stream.flush() def isatty(self) -> bool: return False def fileno(self) -> int: return self._streams[0].fileno() def build_log_path(log_dir: Path, prefix: str, tag: str = "") -> Path: """构建日志文件路径""" suffix = f"_{tag}" if tag else "" stamp = datetime.now().strftime("%Y%m%d_%H%M%S") return log_dir / f"{prefix}{suffix}_{stamp}.log" def get_unified_formatter() -> logging.Formatter: """获取统一格式的日志格式器""" return logging.Formatter(UNIFIED_FORMAT, DATE_FORMAT) @contextmanager def configure_logging( name: str, log_file: Path | None, *, level: str = "INFO", console: bool = True, tee_std: bool = True, ) -> Iterator[logging.Logger]: """ 配置日志 Args: name: 日志器名称 log_file: 日志文件路径,None 表示不写文件 level: 日志级别 console: 是否输出到控制台 tee_std: 是否将 stdout/stderr 也写入日志文件 Yields: 配置好的日志器 """ logger = logging.getLogger(name) logger.handlers.clear() logger.setLevel(getattr(logging, level.upper(), logging.INFO)) logger.propagate = False formatter = get_unified_formatter() original_stdout = sys.stdout original_stderr = sys.stderr log_fp: TextIO | None = None try: if log_file: log_file.parent.mkdir(parents=True, exist_ok=True) log_fp = open(log_file, "a", encoding="utf-8", buffering=1) if tee_std: if console: sys.stdout = TeeStream(original_stdout, log_fp) sys.stderr = TeeStream(original_stderr, log_fp) else: sys.stdout = log_fp sys.stderr = log_fp file_handler = logging.StreamHandler(log_fp) file_handler.setFormatter(formatter) logger.addHandler(file_handler) if console: console_handler = logging.StreamHandler(original_stdout) console_handler.setFormatter(formatter) logger.addHandler(console_handler) yield logger finally: for handler in list(logger.handlers): handler.flush() handler.close() logger.removeHandler(handler) if log_fp: log_fp.flush() log_fp.close() sys.stdout = original_stdout sys.stderr = original_stderr def setup_root_logger(level: str = "INFO") -> logging.Logger: """ 配置根日志器 Args: level: 日志级别 Returns: 根日志器 """ root = logging.getLogger() root.setLevel(getattr(logging, level.upper(), logging.INFO)) # 清除已有处理器 root.handlers.clear() # 添加控制台处理器 handler = logging.StreamHandler() handler.setFormatter(get_unified_formatter()) root.addHandler(handler) return root