import hashlib import json from datetime import datetime from pathlib import Path from app.infrastructure.service.wechat.config import LOG_ROOT_DIR ALLOWED_MODULES = {"api", "bot", "ocr", "ai", "db", "capture", "audit", "error"} DOMAIN_MODULES = {"api", "bot", "ocr", "ai", "db", "capture"} def _infer_domain(row: dict) -> str: module = str(row.get("module") or "").strip().lower() if module in DOMAIN_MODULES: return module if module == "error": event = str(row.get("event") or "") prefix = event.split(".", 1)[0].strip().lower() if prefix in DOMAIN_MODULES: return prefix return "api" def _event_id(row: dict) -> str: raw = json.dumps(row or {}, ensure_ascii=False, sort_keys=True) return hashlib.md5(raw.encode("utf-8")).hexdigest() def _enrich_row(row: dict) -> dict: x = dict(row or {}) x["domain"] = _infer_domain(x) x["event_id"] = _event_id(x) return x def _read_jsonl(module: str): p = Path(LOG_ROOT_DIR) / f"{module}.jsonl" if not p.exists(): return [] rows = [] with p.open("r", encoding="utf-8", errors="replace") as f: for line in f: line = line.strip() if not line: continue try: rows.append(json.loads(line)) except Exception: continue return rows def query_events(module=None, level=None, event=None, trace_id=None, start_ts=None, end_ts=None, keyword=None, page=1, size=50): modules = [module] if module in ALLOWED_MODULES else sorted(ALLOWED_MODULES) all_rows = [] for m in modules: all_rows.extend(_read_jsonl(m)) def ok(row): if level and str(row.get("level", "")).upper() != str(level).upper(): return False if event and str(row.get("event", "")) != str(event): return False if trace_id and str(row.get("trace_id", "")) != str(trace_id): return False if keyword and keyword not in json.dumps(row, ensure_ascii=False): return False ts = str(row.get("ts") or "") if start_ts and ts < start_ts: return False if end_ts and ts > end_ts: return False return True rows = [_enrich_row(r) for r in all_rows if ok(r)] rows.sort(key=lambda x: str(x.get("ts") or ""), reverse=True) page = max(1, int(page or 1)) size = max(1, min(200, int(size or 50))) start = (page - 1) * size end = start + size return {"total": len(rows), "page": page, "size": size, "items": rows[start:end]} def query_trace(trace_id: str): if not trace_id: return [] result = query_events(trace_id=trace_id, size=500) items = result.get("items") or [] items.sort(key=lambda x: str(x.get("ts") or "")) return items def query_event_json(event_id: str): event_id = str(event_id or "").strip().lower() if not event_id: return None for m in sorted(ALLOWED_MODULES): rows = _read_jsonl(m) for row in rows: x = _enrich_row(row) if str(x.get("event_id") or "") == event_id: return x return None def clear_logs(module=None): modules = [module] if module in ALLOWED_MODULES else sorted(ALLOWED_MODULES) root = Path(LOG_ROOT_DIR) deleted = [] for m in modules: for p in root.glob(f"{m}.log*"): p.write_text("", encoding="utf-8") deleted.append(str(p.name)) for p in root.glob(f"{m}.jsonl*"): p.write_text("", encoding="utf-8") deleted.append(str(p.name)) return {"modules": modules, "files": deleted} def query_summary(limit=300): events = query_events(size=limit).get("items") or [] error_count = sum(1 for e in events if str(e.get("level", "")).upper() in {"ERROR"}) fallback_count = sum(1 for e in events if str(e.get("event", "")).endswith("fallback")) reasons = {} domain_counts = {"api": 0, "bot": 0, "ocr": 0, "ai": 0, "db": 0, "capture": 0} for e in events: reason = str(e.get("reason") or "").strip() if reason: reasons[reason] = reasons.get(reason, 0) + 1 domain = str(e.get("domain") or _infer_domain(e)) if domain in domain_counts: domain_counts[domain] += 1 reason_top = sorted(reasons.items(), key=lambda x: x[1], reverse=True)[:20] domain_top = sorted(domain_counts.items(), key=lambda x: x[1], reverse=True) return { "window": len(events), "error_count": error_count, "fallback_count": fallback_count, "top_reasons": [{"reason": k, "count": v} for k, v in reason_top], "domain_counts": [{"domain": k, "count": v} for k, v in domain_top], "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), }