Files
MoFin/deploy/profile-scripts/json_failure_monitor.py
T
知微 80d59c9331 feat: 统一部署目录——所有运行时文件归入MoFin repo
- deploy/bot/ — XMPP bot核心(xmpp_agent_core + xmpp_zhiwei_bot)
- deploy/profile-scripts/ — cron脚本(price_monitor等)
- 运行时文件已替换为指向MoFin的符号链接
- 改代码只需改MoFin,系统自动生效
2026-07-17 23:12:35 +08:00

169 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""json_failure_monitor.py — JSON解析失败监控 v1
监控知微pipeline中LLM输出JSON解析失败事件,记录模型名+原始输出前200字符。
配合 cron_health_monitor.py 使用。
运行方式:
python3 json_failure_monitor.py # 扫描agent.log最近的JSON解析失败
python3 json_failure_monitor.py --watch # 持续跟踪新条目
输出:JSON解析失败事件写入 ~/.hermes/profiles/position-analyst/logs/json_failures.jsonl
每行一个JSON对象,字段:timestamp, model, provider, session_id, raw_preview(前200字)
"""
import json, os, re, sys, time
from pathlib import Path
from datetime import datetime, timezone
PROFILE_DIR = Path.home() / ".hermes" / "profiles" / "position-analyst"
LOG_DIR = PROFILE_DIR / "logs"
FAILURES_LOG = LOG_DIR / "json_failures.jsonl"
# 匹配JSON解析失败的模式(从 agent.log 中提取)
# 常见模式:json.loads/json.JSONDecodeError/SyntaxError/Invalid JSON
JSON_FAILURE_PATTERNS = [
re.compile(r'json\.loads.*Error', re.I),
re.compile(r'JSONDecodeError', re.I),
re.compile(r'Expecting.*value.*char', re.I),
re.compile(r'Invalid control character', re.I),
re.compile(r'SyntaxError.*invalid syntax.*json', re.I),
re.compile(r'not (valid |)JSON', re.I),
re.compile(r'JSON schema.*mismatch', re.I),
re.compile(r'fail.*parse.*JSON', re.I),
re.compile(r'Failed to decode JSON', re.I),
re.compile(r'output.*not.*valid.*json', re.I),
]
# 提取模型名
MODEL_PATTERN = re.compile(r'model=([\w.-]+)')
PROVIDER_PATTERN = re.compile(r'provider=([\w.-]+)')
SESSION_PATTERN = re.compile(r'\[([\w_-]+)\]')
def scan_log(log_path, since_pos=0):
"""扫描日志文件中的JSON解析失败事件"""
if not log_path.exists():
return [], since_pos
events = []
size = log_path.stat().st_size
with open(log_path, 'r', errors='replace') as f:
if since_pos > 0:
f.seek(since_pos)
for line in f:
# 检查是否匹配JSON失败模式
is_failure = any(p.search(line) for p in JSON_FAILURE_PATTERNS)
if not is_failure:
continue
# 提取元数据
model_m = MODEL_PATTERN.search(line)
provider_m = PROVIDER_PATTERN.search(line)
session_m = SESSION_PATTERN.search(line)
event = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": model_m.group(1) if model_m else "unknown",
"provider": provider_m.group(1) if provider_m else "unknown",
"session_id": session_m.group(1) if session_m else "unknown",
"raw_preview": line[:200].strip(),
}
events.append(event)
since_pos = f.tell()
return events, since_pos
def write_events(events):
"""写入JSON失败事件到jsonl文件"""
if not events:
return 0
with open(FAILURES_LOG, 'a') as f:
for ev in events:
f.write(json.dumps(ev, ensure_ascii=False) + '\n')
return len(events)
def summarize_recent(hours=24):
"""输出最近N小时的JSON解析失败统计"""
if not FAILURES_LOG.exists():
return "暂无JSON解析失败记录"
cutoff = time.time() - hours * 3600
events = []
with open(FAILURES_LOG, 'r') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
ev = json.loads(line)
events.append(ev)
except json.JSONDecodeError:
continue
# 筛选近期
recent = []
for ev in events:
try:
t = datetime.fromisoformat(ev.get("timestamp", "")).timestamp()
if t >= cutoff:
recent.append(ev)
except (ValueError, TypeError):
continue
if not recent:
return f"最近{hours}小时内无JSON解析失败事件"
# 按模型聚合
by_model = {}
for ev in recent:
model = ev.get("model", "unknown")
by_model.setdefault(model, {"count": 0, "sessions": set()})
by_model[model]["count"] += 1
by_model[model]["sessions"].add(ev.get("session_id", ""))
lines = [f"最近{hours}h JSON解析失败统计:共{len(recent)}次"]
for model, info in sorted(by_model.items(), key=lambda x: -x[1]["count"]):
lines.append(f" {model}: {info['count']}次 (涉及{len(info['sessions'])}个session)")
return "\n".join(lines)
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--summarize":
print(summarize_recent())
sys.exit(0)
# 扫描所有agent.log*
log_files = sorted(LOG_DIR.glob("agent.log*"), reverse=True)
total = 0
state_file = PROFILE_DIR / ".json_failure_monitor.state"
since_pos = 0
if state_file.exists():
try:
since_pos = int(state_file.read_text().strip())
except (ValueError, OSError):
since_pos = 0
for lf in log_files:
events, since_pos = scan_log(lf, since_pos)
n = write_events(events)
total += n
if n > 0:
print(f"[{datetime.now().isoformat()}] {lf.name}: 发现{n}个JSON解析失败")
# 保存扫描位置
state_file.write_text(str(since_pos))
if total == 0:
print(f"[{datetime.now().isoformat()}] 未发现新的JSON解析失败")