Files
ocr_data/tool/log/log_manager.py
2026-09-28 16:09:31 +08:00

218 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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()