Merge branch 'main' of http://jeason.online:3000/zhaojie/iov_data_analysis_agent
This commit is contained in:
113
utils/logger.py
Normal file
113
utils/logger.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user