171 lines
4.9 KiB
Python
171 lines
4.9 KiB
Python
|
|
"""
|
|||
|
|
日志工具模块
|
|||
|
|
提供将终端输出同时保存到文件的功能
|
|||
|
|
"""
|
|||
|
|
import sys
|
|||
|
|
import os
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TeeLogger:
|
|||
|
|
"""
|
|||
|
|
双向输出类:同时输出到终端和文件
|
|||
|
|
"""
|
|||
|
|
def __init__(self, filename, mode='w', terminal=None):
|
|||
|
|
"""
|
|||
|
|
Args:
|
|||
|
|
filename: 日志文件路径
|
|||
|
|
mode: 文件打开模式 ('w'=覆盖, 'a'=追加)
|
|||
|
|
terminal: 原始输出流(通常是sys.stdout或sys.stderr)
|
|||
|
|
"""
|
|||
|
|
self.terminal = terminal or sys.stdout
|
|||
|
|
self.log_file = open(filename, mode, encoding='utf-8')
|
|||
|
|
|
|||
|
|
def write(self, message):
|
|||
|
|
"""写入消息到终端和文件"""
|
|||
|
|
self.terminal.write(message)
|
|||
|
|
self.log_file.write(message)
|
|||
|
|
self.log_file.flush() # 立即写入磁盘
|
|||
|
|
|
|||
|
|
def flush(self):
|
|||
|
|
"""刷新缓冲区"""
|
|||
|
|
self.terminal.flush()
|
|||
|
|
self.log_file.flush()
|
|||
|
|
|
|||
|
|
def close(self):
|
|||
|
|
"""关闭日志文件"""
|
|||
|
|
if self.log_file:
|
|||
|
|
self.log_file.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class LoggerContext:
|
|||
|
|
"""
|
|||
|
|
日志上下文管理器
|
|||
|
|
使用with语句自动管理日志的开启和关闭
|
|||
|
|
"""
|
|||
|
|
def __init__(self, log_file=None, log_dir="logs", mode='w',
|
|||
|
|
redirect_stdout=True, redirect_stderr=True):
|
|||
|
|
"""
|
|||
|
|
Args:
|
|||
|
|
log_file: 日志文件名(None则自动生成时间戳文件名)
|
|||
|
|
log_dir: 日志目录
|
|||
|
|
mode: 文件打开模式 ('w'=覆盖, 'a'=追加)
|
|||
|
|
redirect_stdout: 是否重定向标准输出
|
|||
|
|
redirect_stderr: 是否重定向标准错误
|
|||
|
|
"""
|
|||
|
|
self.log_dir = log_dir
|
|||
|
|
self.mode = mode
|
|||
|
|
self.redirect_stdout = redirect_stdout
|
|||
|
|
self.redirect_stderr = redirect_stderr
|
|||
|
|
|
|||
|
|
# 创建日志目录
|
|||
|
|
os.makedirs(log_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 生成日志文件名
|
|||
|
|
if log_file is None:
|
|||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
|
log_file = f"run_{timestamp}.log"
|
|||
|
|
|
|||
|
|
self.log_path = os.path.join(log_dir, log_file)
|
|||
|
|
|
|||
|
|
# 保存原始的stdout和stderr
|
|||
|
|
self.original_stdout = sys.stdout
|
|||
|
|
self.original_stderr = sys.stderr
|
|||
|
|
|
|||
|
|
# 日志对象
|
|||
|
|
self.stdout_logger = None
|
|||
|
|
self.stderr_logger = None
|
|||
|
|
|
|||
|
|
def __enter__(self):
|
|||
|
|
"""进入上下文:开启日志"""
|
|||
|
|
print(f"📝 日志记录已启用")
|
|||
|
|
print(f"📁 日志文件: {self.log_path}")
|
|||
|
|
print("-" * 60)
|
|||
|
|
|
|||
|
|
# 创建TeeLogger对象
|
|||
|
|
if self.redirect_stdout:
|
|||
|
|
self.stdout_logger = TeeLogger(
|
|||
|
|
self.log_path,
|
|||
|
|
mode=self.mode,
|
|||
|
|
terminal=self.original_stdout
|
|||
|
|
)
|
|||
|
|
sys.stdout = self.stdout_logger
|
|||
|
|
|
|||
|
|
if self.redirect_stderr:
|
|||
|
|
self.stderr_logger = TeeLogger(
|
|||
|
|
self.log_path,
|
|||
|
|
mode='a', # stderr总是追加模式
|
|||
|
|
terminal=self.original_stderr
|
|||
|
|
)
|
|||
|
|
sys.stderr = self.stderr_logger
|
|||
|
|
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|||
|
|
"""退出上下文:关闭日志"""
|
|||
|
|
# 恢复原始输出
|
|||
|
|
sys.stdout = self.original_stdout
|
|||
|
|
sys.stderr = self.original_stderr
|
|||
|
|
|
|||
|
|
# 关闭日志文件
|
|||
|
|
if self.stdout_logger:
|
|||
|
|
self.stdout_logger.close()
|
|||
|
|
if self.stderr_logger:
|
|||
|
|
self.stderr_logger.close()
|
|||
|
|
|
|||
|
|
print("-" * 60)
|
|||
|
|
print(f"✅ 日志已保存到: {self.log_path}")
|
|||
|
|
|
|||
|
|
# 返回False表示不抑制异常
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def setup_logger(log_file=None, log_dir="logs", mode='w'):
|
|||
|
|
"""
|
|||
|
|
快速设置日志记录
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
log_file: 日志文件名(None则自动生成)
|
|||
|
|
log_dir: 日志目录
|
|||
|
|
mode: 文件模式 ('w'=覆盖, 'a'=追加)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
LoggerContext对象
|
|||
|
|
|
|||
|
|
Example:
|
|||
|
|
with setup_logger("my_test.log"):
|
|||
|
|
print("这条消息会同时输出到终端和文件")
|
|||
|
|
"""
|
|||
|
|
return LoggerContext(log_file=log_file, log_dir=log_dir, mode=mode)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_default_log_filename(prefix="run"):
|
|||
|
|
"""
|
|||
|
|
生成默认的日志文件名(带时间戳)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
prefix: 文件名前缀
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
str: 格式为 "prefix_YYYYMMDD_HHMMSS.log"
|
|||
|
|
"""
|
|||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
|
return f"{prefix}_{timestamp}.log"
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
# 测试代码
|
|||
|
|
print("测试1: 使用默认配置")
|
|||
|
|
with setup_logger():
|
|||
|
|
print("这是测试消息1")
|
|||
|
|
print("这是测试消息2")
|
|||
|
|
print("日志记录已结束\n")
|
|||
|
|
|
|||
|
|
print("测试2: 使用自定义文件名")
|
|||
|
|
with setup_logger(log_file="test_custom.log"):
|
|||
|
|
print("自定义文件名测试")
|
|||
|
|
for i in range(3):
|
|||
|
|
print(f" 消息 {i+1}")
|
|||
|
|
print("完成")
|
|||
|
|
|