首次提交

This commit is contained in:
2026-09-28 16:09:31 +08:00
commit 4a6839319a
71 changed files with 12144 additions and 0 deletions

0
tool/log/__init__.py Normal file
View File

218
tool/log/log_manager.py Normal file
View File

@@ -0,0 +1,218 @@
# log_manager.py
import json
import logging
import os
import time
import threading
from collections import deque
from datetime import datetime
from typing import Dict, List, Optional, Union
class ProgramFilter(logging.Filter):
"""自定义日志过滤器,为日志记录添加程序名称属性"""
def filter(self, record: logging.LogRecord) -> bool:
if not hasattr(record, 'program'):
record.program = "UNKNOWN"
return True
class UnifiedLogger:
"""统一日志管理器(线程安全单例),支持多程序日志收集"""
_instance: Optional['UnifiedLogger'] = None
_lock = threading.Lock()
def __new__(cls) -> 'UnifiedLogger':
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
# 避免重复初始化
if hasattr(self, '_initialized') and self._initialized:
return
# 每视频状态使用线程本地存储:批处理多线程并发时,每个视频在各自线程内
# 独立记录日志/计时/统计,互不串扰。(嵌套子线程如OCR并发worker不应直接
# 调用本logger的状态方法——它们会落到worker线程的独立状态而丢失。)
self._local = threading.local()
# 是否打印详细日志到控制台。默认 False:控制台只显示"视频→步骤"里程碑,
# 详细日志仍完整存进每视频 JSON(log/<video>.json)。设 LOG_VERBOSE=1 恢复全部。
self.verbose = os.environ.get("LOG_VERBOSE", "0") == "1"
# 自定义 logger(避免污染根日志器)
self.logger = logging.getLogger("UnifiedLogger")
self.logger.setLevel(logging.DEBUG)
self.logger.propagate = False # 不传播到 root,避免每条日志重复打印
self.logger.addFilter(ProgramFilter())
# 控制台 Handler:非 verbose 时只放行 WARNING+,把 INFO 细节挡在控制台外(仍进 JSON)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG if self.verbose else logging.WARNING)
console_formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] [%(program)s] %(message)s'
)
console_handler.setFormatter(console_formatter)
self.logger.addHandler(console_handler)
# 第三方库降噪:jieba 的 DEBUG("Building prefix dict...")太吵
logging.getLogger("jieba").setLevel(logging.WARNING)
# qcloud_cos SDK 噪音大: 每次 get/put/head object 都打一条 INFO(带完整url/headers),
# 而且它把"对象不存在"(HEAD 404, 本项目里大量用来判断分片是否已完成/锁是否存在,
# 属于正常控制流)也当 WARNING 打一条 —— 两种都没信息量, 真正的失败走异常
# (CosServiceError), 本项目自己的 try/except 会记录, 不受这里调级影响。
logging.getLogger("qcloud_cos").setLevel(logging.ERROR)
self._initialized = True
def step(self, video: str, step: str, extra: str = "") -> None:
"""打印一条简洁的"视频 → 步骤"里程碑(始终显示,不受 verbose 影响)。"""
line = f"[{datetime.now().strftime('%H:%M:%S')}] {video} → {step}"
if extra:
line += f" ({extra})"
print(line, flush=True)
try:
self._state()["log_buffer"].append({
"timestamp": datetime.now().isoformat(), "level": "STEP",
"program": "step", "message": f"{step} {extra}".strip(),
})
except Exception:
pass
def _state(self) -> dict:
"""获取当前线程的日志状态,惰性初始化。"""
st = getattr(self._local, 'state', None)
if st is None:
st = {
"log_buffer": deque(maxlen=10000),
"log_file": "unified_log.json",
"program_sequence": [],
"program_start_times": {},
"program_end_times": {},
"custom_stats": {},
}
self._local.state = st
return st
def init_logging(self, log_file: str = "unified_log.json") -> None:
"""初始化日志系统(可重新指定日志文件路径)"""
self._state()["log_file"] = log_file
log_dir = os.path.dirname(log_file)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
def reset(self) -> None:
"""重置当前线程的日志状态(用于多线程场景下每个视频独立记录)"""
st = self._state()
st["log_buffer"].clear()
st["program_sequence"].clear()
st["program_start_times"].clear()
st["program_end_times"].clear()
st["custom_stats"].clear()
def set_stat(self, key: str, value: object) -> None:
"""记录一项自定义统计,会写入日志 metadata.custom_stats"""
self._state()["custom_stats"][key] = value
def _get_level_int(self, level: Union[str, int]) -> int:
"""将日志级别转换为 logging 模块的整数常量"""
if isinstance(level, int):
return level
level_map = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL
}
return level_map.get(level.upper(), logging.INFO)
def log(self, message: str, level: Union[str, int] = "info", program: str = "A") -> None:
"""记录一条日志"""
timestamp = datetime.now().isoformat()
level_str = level if isinstance(level, str) else logging.getLevelName(level)
log_entry = {
"timestamp": timestamp,
"level": level_str,
"program": program,
"message": message
}
self._state()["log_buffer"].append(log_entry)
# 输出到自定义 logger
level_int = self._get_level_int(level)
self.logger.log(level_int, message, extra={"program": program})
def start_program(self, program_name: str) -> None:
"""记录程序开始执行"""
st = self._state()
if program_name in st["program_start_times"]:
self.logger.warning(f"程序 {program_name} 已开始,忽略重复 start 调用", extra={"program": "SYSTEM"})
return
st["program_start_times"][program_name] = time.time()
st["program_sequence"].append(program_name)
self.log(f"程序 {program_name} 开始执行", program=program_name)
def end_program(self, program_name: str) -> None:
"""记录程序执行结束"""
st = self._state()
if program_name not in st["program_start_times"]:
self.logger.error(f"程序 {program_name} 未调用 start_program,无法记录结束", extra={"program": "SYSTEM"})
return
end_time = time.time()
st["program_end_times"][program_name] = end_time
duration = end_time - st["program_start_times"][program_name]
self.log(f"程序 {program_name} 执行完成 (耗时: {duration:.2f}s)", program=program_name)
def save_logs(self) -> None:
"""将当前线程的所有日志(含元数据)保存到 JSON 文件"""
st = self._state()
logs_copy = list(st["log_buffer"])
start_times_copy = dict(st["program_start_times"])
end_times_copy = dict(st["program_end_times"])
sequence_copy = list(st["program_sequence"])
stats_copy = dict(st["custom_stats"])
log_file = st["log_file"]
# 计算程序执行耗时
durations = {}
for prog, start in start_times_copy.items():
end = end_times_copy.get(prog, time.time())
durations[prog] = end - start
metadata = {
"log_created": datetime.now().isoformat(),
"program_sequence": sequence_copy,
"total_entries": len(logs_copy),
"program_durations": durations,
"custom_stats": stats_copy
}
full_log = {
"metadata": metadata,
"logs": logs_copy
}
try:
with open(log_file, 'w', encoding='utf-8') as f:
json.dump(full_log, f, ensure_ascii=False, indent=2)
self.logger.info(f"所有日志已保存到: {log_file}", extra={"program": "SYSTEM"})
except (OSError, IOError) as e:
self.logger.error(f"保存日志失败: {e}", extra={"program": "SYSTEM"})
def get_logs(self) -> List[dict]:
"""获取当前线程缓冲区中的所有日志副本"""
return list(self._state()["log_buffer"])
# 全局单例实例
logger = UnifiedLogger()