114 lines
3.3 KiB
Python
114 lines
3.3 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
统一日志模块 - 替代全局 sys.stdout 劫持
|
||
|
||
提供线程安全的日志记录,支持同时输出到终端和文件。
|
||
每个会话拥有独立的日志文件,不会互相干扰。
|
||
"""
|
||
|
||
import logging
|
||
import os
|
||
import sys
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
|
||
def create_session_logger(
|
||
session_id: str,
|
||
log_dir: str,
|
||
log_filename: str = "process.log",
|
||
level: int = logging.INFO,
|
||
) -> logging.Logger:
|
||
"""
|
||
为指定会话创建独立的 Logger 实例。
|
||
|
||
Args:
|
||
session_id: 会话唯一标识
|
||
log_dir: 日志文件所在目录
|
||
log_filename: 日志文件名
|
||
level: 日志级别
|
||
|
||
Returns:
|
||
配置好的 Logger 实例
|
||
"""
|
||
logger = logging.getLogger(f"session.{session_id}")
|
||
logger.setLevel(level)
|
||
|
||
# 避免重复添加 handler
|
||
if logger.handlers:
|
||
return logger
|
||
|
||
formatter = logging.Formatter(
|
||
fmt="%(asctime)s %(message)s",
|
||
datefmt="%H:%M:%S",
|
||
)
|
||
|
||
# 文件 handler — 写入会话专属日志
|
||
os.makedirs(log_dir, exist_ok=True)
|
||
log_path = os.path.join(log_dir, log_filename)
|
||
file_handler = logging.FileHandler(log_path, encoding="utf-8", mode="a")
|
||
file_handler.setFormatter(formatter)
|
||
logger.addHandler(file_handler)
|
||
|
||
# 终端 handler — 输出到 stderr(不干扰 stdout)
|
||
console_handler = logging.StreamHandler(sys.stderr)
|
||
console_handler.setFormatter(formatter)
|
||
logger.addHandler(console_handler)
|
||
|
||
# 不向父 logger 传播
|
||
logger.propagate = False
|
||
|
||
return logger
|
||
|
||
|
||
class PrintCapture:
|
||
"""
|
||
轻量级 print 捕获器,将 print 输出同时写入日志文件。
|
||
用于兼容现有大量使用 print() 的代码,无需逐行改造。
|
||
|
||
用法:
|
||
with PrintCapture(log_path) as cap:
|
||
print("hello") # 同时输出到终端和文件
|
||
# 退出后 sys.stdout 自动恢复
|
||
"""
|
||
|
||
def __init__(self, log_path: str, filter_patterns: Optional[list] = None):
|
||
self.log_path = log_path
|
||
self.filter_patterns = filter_patterns or ["[TOOL] 执行代码:"]
|
||
self._original_stdout = None
|
||
self._log_file = None
|
||
|
||
def __enter__(self):
|
||
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
|
||
self._original_stdout = sys.stdout
|
||
self._log_file = open(self.log_path, "a", encoding="utf-8", buffering=1)
|
||
sys.stdout = self._DualWriter(
|
||
self._original_stdout, self._log_file, self.filter_patterns
|
||
)
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
sys.stdout = self._original_stdout
|
||
if self._log_file:
|
||
self._log_file.close()
|
||
return False
|
||
|
||
class _DualWriter:
|
||
"""同时写入两个流,支持过滤"""
|
||
|
||
def __init__(self, terminal, log_file, filter_patterns):
|
||
self.terminal = terminal
|
||
self.log_file = log_file
|
||
self.filter_patterns = filter_patterns
|
||
|
||
def write(self, message):
|
||
self.terminal.write(message)
|
||
# 过滤不需要写入日志的内容
|
||
if any(p in message for p in self.filter_patterns):
|
||
return
|
||
self.log_file.write(message)
|
||
|
||
def flush(self):
|
||
self.terminal.flush()
|
||
self.log_file.flush()
|