142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
|
|
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"),
|
||
|
|
}
|