ts
This commit is contained in:
+55
-131
@@ -1,153 +1,77 @@
|
||||
|
||||
"""log_manager.py
|
||||
|
||||
简单的线程安全日志管理器:
|
||||
- 追加日志(level/message/thread_id)
|
||||
- 持久化到 data/logs.json
|
||||
- 提供读取最近 N 条、清空
|
||||
|
||||
说明:采集器是异步运行,但日志写入使用 threading.Lock 足够。
|
||||
"""
|
||||
Log Manager - 持久化日志管理器
|
||||
支持日志加载、保存、追加、清空
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from app import config
|
||||
|
||||
|
||||
class LogManager:
|
||||
"""日志管理器 - 线程安全"""
|
||||
|
||||
def __init__(self, log_file: str = None, max_logs: int = None):
|
||||
self.log_file = log_file or config.LOG_FILE
|
||||
self.max_logs = max_logs or config.MAX_LOGS
|
||||
def __init__(self, log_file: Optional[str] = None, max_logs: int = 2000):
|
||||
self.log_file = log_file or os.path.join(config.DATA_DIR, "logs.json")
|
||||
self.max_logs = max_logs
|
||||
self._lock = threading.Lock()
|
||||
self.logs: List[Dict] = []
|
||||
self._load_logs()
|
||||
self._load()
|
||||
|
||||
def _load_logs(self):
|
||||
"""启动时加载历史日志"""
|
||||
with self._lock:
|
||||
if os.path.exists(self.log_file):
|
||||
try:
|
||||
with open(self.log_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.logs = data.get('logs', [])
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
print(f"[LogManager] 加载日志文件失败: {e}")
|
||||
self.logs = []
|
||||
else:
|
||||
self.logs = []
|
||||
def _load(self) -> None:
|
||||
if not os.path.exists(self.log_file):
|
||||
self.logs = []
|
||||
return
|
||||
try:
|
||||
with open(self.log_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f) or {}
|
||||
self.logs = data.get("logs", []) or []
|
||||
except Exception:
|
||||
self.logs = []
|
||||
|
||||
def _save_locked(self) -> None:
|
||||
os.makedirs(os.path.dirname(self.log_file), exist_ok=True)
|
||||
tmp = self.log_file + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{"logs": self.logs, "last_updated": datetime.now().isoformat(timespec="seconds")},
|
||||
f,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
os.replace(tmp, self.log_file)
|
||||
|
||||
def append(self, level: str, message: str, thread_id: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
追加新日志
|
||||
返回日志条目
|
||||
"""
|
||||
with self._lock:
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": level,
|
||||
"message": message,
|
||||
"thread_id": thread_id
|
||||
}
|
||||
self.logs.append(log_entry)
|
||||
entry = {
|
||||
"time": datetime.now().isoformat(timespec="seconds"),
|
||||
"level": level.upper(),
|
||||
"message": message,
|
||||
}
|
||||
if thread_id:
|
||||
entry["thread_id"] = thread_id
|
||||
|
||||
# 限制日志数量(保留最新的 max_logs 条)
|
||||
with self._lock:
|
||||
self.logs.append(entry)
|
||||
if len(self.logs) > self.max_logs:
|
||||
self.logs = self.logs[-self.max_logs:]
|
||||
self.logs = self.logs[-self.max_logs :]
|
||||
self._save_locked()
|
||||
return entry
|
||||
|
||||
# 异步保存到文件
|
||||
self._save()
|
||||
|
||||
return log_entry
|
||||
|
||||
def _save(self):
|
||||
"""保存到文件(需在锁内调用)"""
|
||||
try:
|
||||
with open(self.log_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
"logs": self.logs,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
except IOError as e:
|
||||
print(f"[LogManager] 保存日志文件失败: {e}")
|
||||
|
||||
def get_logs(self, level: Optional[str] = None, limit: Optional[int] = None) -> List[Dict]:
|
||||
"""
|
||||
获取日志
|
||||
:param level: 过滤级别 (None/INFO/SUCCESS/WARNING/ERROR/DEBUG)
|
||||
:param limit: 限制返回数量
|
||||
"""
|
||||
with self._lock:
|
||||
if level:
|
||||
filtered = [log for log in self.logs if log['level'] == level]
|
||||
else:
|
||||
filtered = self.logs
|
||||
|
||||
if limit:
|
||||
return filtered[-limit:]
|
||||
return filtered
|
||||
|
||||
def search_logs(self, keyword: str, limit: Optional[int] = None) -> List[Dict]:
|
||||
"""
|
||||
搜索日志
|
||||
:param keyword: 搜索关键词
|
||||
:param limit: 限制返回数量
|
||||
"""
|
||||
with self._lock:
|
||||
filtered = [
|
||||
log for log in self.logs
|
||||
if keyword.lower() in log['message'].lower()
|
||||
]
|
||||
|
||||
if limit:
|
||||
return filtered[-limit:]
|
||||
return filtered
|
||||
|
||||
def clear(self):
|
||||
"""清空日志"""
|
||||
def clear(self) -> None:
|
||||
with self._lock:
|
||||
self.logs = []
|
||||
self._save()
|
||||
self._save_locked()
|
||||
|
||||
def get_stats(self) -> Dict:
|
||||
"""获取日志统计信息"""
|
||||
def get_latest(self, limit: int = 200) -> List[Dict]:
|
||||
with self._lock:
|
||||
stats = {
|
||||
"total": len(self.logs),
|
||||
"by_level": {}
|
||||
}
|
||||
|
||||
for log in self.logs:
|
||||
level = log['level']
|
||||
stats["by_level"][level] = stats["by_level"].get(level, 0) + 1
|
||||
|
||||
return stats
|
||||
|
||||
def export_to_text(self) -> str:
|
||||
"""导出为文本格式"""
|
||||
with self._lock:
|
||||
lines = []
|
||||
lines.append("=" * 80)
|
||||
lines.append("小红书采集器日志")
|
||||
lines.append(f"导出时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
lines.append(f"总日志数: {len(self.logs)}")
|
||||
lines.append("=" * 80)
|
||||
lines.append("")
|
||||
|
||||
for log in self.logs:
|
||||
timestamp = log['timestamp'][:19] # 去掉微秒
|
||||
level = log['level'].ljust(8)
|
||||
thread = f"[{log['thread_id']}]" if log.get('thread_id') else ""
|
||||
message = log['message']
|
||||
lines.append(f"{timestamp} | {level} | {thread} {message}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# 全局日志管理器实例
|
||||
_log_manager_instance = None
|
||||
|
||||
|
||||
def get_log_manager() -> LogManager:
|
||||
"""获取全局日志管理器实例"""
|
||||
global _log_manager_instance
|
||||
if _log_manager_instance is None:
|
||||
_log_manager_instance = LogManager()
|
||||
return _log_manager_instance
|
||||
return list(self.logs[-max(1, int(limit)) :])
|
||||
|
||||
Reference in New Issue
Block a user