Files

268 lines
11 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""dm-log-taxonomy.py — READ-ONLY failure taxonomy for fleet DM sidechat reliability.
Reads /home/super/Projects/NetVM/dm-log.jsonl, prints:
1. Event-type counts and send outcome rates
2. Sidechat nav failure taxonomy (per-target, per-agent-pair)
3. Failure timeline (hourly buckets, worst 10-min windows, by node)
4. "Ghost" rate: verified:true sidechat sends with no UUID anywhere
5. Hypothesis evidence tables (nav_failed reasons, placement pairs,
alias sources, autoprovision success, retry distribution)
No writes to any state files. Runs in <1s on the current log size.
"""
import json
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone, timedelta
LOG = "/home/super/Projects/NetVM/dm-log.jsonl"
WINDOW_H = 48
def parse_ts(s):
if not s:
return None
try:
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except Exception:
return None
def main():
now = datetime.now(timezone.utc)
cutoff = now - timedelta(hours=WINDOW_H)
events = []
parse_err = 0
with open(LOG, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
e = json.loads(line)
except Exception:
parse_err += 1
continue
e["_dt"] = parse_ts(e.get("ts"))
events.append(e)
in_win = [e for e in events if e["_dt"] and e["_dt"] >= cutoff]
print(f"dm-log.jsonl: {len(events)} total lines ({parse_err} parse errors)")
print(f"window: last {WINDOW_H}h -> {len(in_win)} events")
if in_win:
print(f" range: {in_win[0]['ts']} .. {in_win[-1]['ts']}")
print("=" * 78)
# ---- 1. event-type counts ------------------------------------------------
types = Counter(e.get("type", "?") for e in in_win)
print("\n[1] EVENT-TYPE COUNTS")
for t, n in types.most_common():
print(f" {n:6d} {t}")
# ---- per-send assembly ---------------------------------------------------
sends = {}
order = []
def S(e):
sid = e.get("id")
if not sid:
return None
if sid not in sends:
sends[sid] = {"id": sid, "events": [], "first_ts": e["_dt"]}
order.append(sid)
s = sends[sid]
s["events"].append(e)
for k in ("agent", "to", "target"):
if k not in s and e.get(k) is not None:
s[k] = e.get(k)
t = e.get("type")
if t == "verified":
s["verified_seen"] = True
if e.get("thread_uuid"):
s["uuids"] = s.get("uuids", set()) | {e["thread_uuid"]}
if t == "sent":
s["sent_seen"] = True
s["sent_verified"] = bool(e.get("verified"))
if e.get("thread_uuid"):
s["uuids"] = s.get("uuids", set()) | {e["thread_uuid"]}
tags = e.get("tags") or {}
if isinstance(tags, dict) and tags.get("thread"):
s["uuids"] = s.get("uuids", set()) | {tags["thread"]}
if t == "send_done":
s["done"] = True
if t == "sidechat_uuid_capture_failed":
s["capture_failed"] = True
s["capture_out"] = (e.get("out") or "")[:80]
if t == "sidechat_autoprovisioned" and e.get("thread_uuid"):
s["uuids"] = s.get("uuids", set()) | {e["thread_uuid"]}
if t == "alias_resolved" and e.get("thread_uuid"):
s["uuids"] = s.get("uuids", set()) | {e["thread_uuid"]}
if t == "placement_mismatch":
s["placement_mismatch"] = True
if t == "pre_send_assert_failed":
s["gate_failed"] = True
s["gate_reason"] = e.get("reason")
if t == "nav_failed":
s["nav_failed"] = True
s["nav_reason"] = e.get("reason") or "transport"
return s
for e in in_win:
S(e)
started = [sends[i] for i in order
if any(e.get("type") == "send_start" for e in sends[i]["events"])]
print(f"\n[1b] SEND OUTCOMES: {len(started)} sends attempted")
oc = Counter()
for s in started:
if s.get("gate_failed"):
oc["loud-fail: pre_send_assert_failed"] += 1
elif s.get("nav_failed"):
oc["loud-fail: nav_failed"] += 1
elif s.get("sent_seen") and s.get("sent_verified"):
oc["sent verified:true"] += 1
elif s.get("sent_seen"):
oc["sent verified:false"] += 1
elif s.get("done"):
oc["send_done, no sent event"] += 1
else:
oc["abandoned (no terminal event)"] += 1
for k, n in oc.most_common():
print(f" {n:5d} {k}")
print(" (note: main_chat_blocked events are policy blocks, not failures;")
print(" followup_register_failed=signing issues, tangential)")
# ---- 2. sidechat taxonomy ------------------------------------------------
def is_sc(s):
return (s.get("target") or "") not in ("main", "", None)
sc = [s for s in started if is_sc(s)]
print(f"\n[2] SIDECHAT TAXONOMY ({len(sc)} sidechat-targeted sends)")
tax = Counter()
per_target = defaultdict(Counter)
per_pair = defaultdict(Counter)
for s in sc:
if s.get("gate_failed"):
cls = "gate_failed:" + str(s.get("gate_reason"))
elif s.get("nav_failed"):
cls = "nav_failed:" + str(s.get("nav_reason"))
elif s.get("capture_failed"):
cls = "uuid_capture_failed"
elif s.get("placement_mismatch"):
cls = "placement_mismatch"
elif s.get("sent_seen") and s.get("sent_verified"):
cls = ("verified:true, NO uuid anywhere (ghost)"
if not s.get("uuids") else "clean verified")
elif s.get("sent_seen"):
cls = "sent verified:false"
elif s.get("done"):
cls = "done, no sent event"
else:
cls = "abandoned"
tax[cls] += 1
per_target[s.get("target") or "?"][cls] += 1
per_pair[f"{s.get('agent') or '?'}->{s.get('to') or '?'}"][cls] += 1
for k, n in tax.most_common():
print(f" {n:5d} {k}")
print("\n per-target (sends, non-clean, top classes):")
for tgt, c in sorted(per_target.items(), key=lambda x: -sum(x[1].values())):
tot = sum(c.values())
bad = tot - c.get("clean verified", 0)
print(f" {tgt}: {tot} sends, {bad} non-clean {dict(c.most_common(4))}")
print("\n per agent-pair:")
for pair, c in sorted(per_pair.items(), key=lambda x: -sum(x[1].values())):
tot = sum(c.values())
bad = tot - c.get("clean verified", 0)
print(f" {pair}: {tot} sends, {bad} non-clean {dict(c.most_common(4))}")
# ---- 4. ghosts ------------------------------------------------------------
ghosts = [s for s in sc if s.get("sent_seen") and s.get("sent_verified")
and not s.get("uuids")]
print(f"\n[4] GHOST RATE: {len(ghosts)}/{len(sc)} "
f"({100.0 * len(ghosts) / len(sc) if sc else 0:.0f}%) verified:true "
f"sidechat sends with no UUID in any event")
gh = Counter(s["first_ts"].strftime("%m-%d %H") for s in ghosts if s["first_ts"])
print(" ghost hours:", dict(sorted(gh.items())))
print(" proxies: uuid_capture_failed="
f"{sum(1 for s in sc if s.get('capture_failed'))}, "
f"placement_mismatch={sum(1 for s in sc if s.get('placement_mismatch'))}, "
f"pre_send_assert_failed={sum(1 for s in sc if s.get('gate_failed'))}")
# ---- 3. timeline ------------------------------------------------------------
print("\n[3] TIMELINE (hourly, sidechat sends; #=bad, .=ok)")
buckets = defaultdict(Counter)
for s in sc:
if not s.get("first_ts"):
continue
hr = s["first_ts"].strftime("%m-%d %H:00")
buckets[hr]["total"] += 1
bad = not (s.get("sent_seen") and s.get("sent_verified")
and not s.get("capture_failed")
and not s.get("placement_mismatch")
and not s.get("gate_failed") and not s.get("nav_failed"))
if bad:
buckets[hr]["bad"] += 1
for hr in sorted(buckets):
t, b = buckets[hr]["total"], buckets[hr]["bad"]
print(f" {hr} total={t:3d} bad={b:3d} {'#' * b}{'.' * (t - b)}")
wins = defaultdict(Counter)
for s in sc:
if not s.get("first_ts"):
continue
w = s["first_ts"].strftime("%m-%d %H:%M")[:-1] + "0"
wins[w]["total"] += 1
if not (s.get("sent_seen") and s.get("sent_verified")):
wins[w]["bad"] += 1
print(" worst 10-min windows (>=3 sends):")
shown = 0
for w, c in sorted(wins.items(), key=lambda x: -x[1]["bad"]):
if c["total"] >= 3 and shown < 8:
print(f" {w} total={c['total']} bad={c['bad']}")
shown += 1
print(" bad rate by sending node:")
by_node = defaultdict(Counter)
for s in sc:
by_node[s.get("agent") or "?"]["total"] += 1
if not (s.get("sent_seen") and s.get("sent_verified")):
by_node[s.get("agent") or "?"]["bad"] += 1
for node, c in sorted(by_node.items(), key=lambda x: -x[1]["total"]):
r = 100.0 * c["bad"] / c["total"] if c["total"] else 0
print(f" {node}: {c['bad']}/{c['total']} bad ({r:.0f}%)")
# ---- 5. hypothesis evidence ---------------------------------------------------
print("\n[5] HYPOTHESIS EVIDENCE")
nfr = Counter(s.get("nav_reason") for s in sc if s.get("nav_failed"))
print(f" nav_failed reasons: {dict(nfr)}")
land = [s for s in sc if s.get("capture_failed")]
print(f" uuid_capture_failed: {len(land)}, "
f"landing-page outs: {sum(1 for s in land if 'muse.ai/' in (s.get('capture_out') or ''))}")
pairs = Counter()
for e in in_win:
if e.get("type") == "placement_mismatch":
pairs[((e.get("expected_uuid") or "?")[:8],
(e.get("actual_uuid") or "?")[:8], e.get("target"))] += 1
print(" placement_mismatch expected->actual:")
for (a, b, t), n in pairs.most_common(6):
print(f" {n:3d} exp={a}.. act={b}.. target={t}")
ar = Counter(e.get("source") for e in in_win if e.get("type") == "alias_resolved")
print(f" alias_resolved sources: {dict(ar)} (None = field absent, older events)")
ap_try = sum(1 for e in in_win if e.get("type") == "sidechat_autoprovision_start")
ap_ok = sum(1 for e in in_win if e.get("type") == "sidechat_autoprovisioned"
and e.get("thread_uuid"))
print(f" autoprovision: {ap_try} attempts -> {ap_ok} with uuid ({100.0 * ap_ok / ap_try if ap_try else 0:.0f}%)")
rt = Counter()
for s in started:
n = sum(1 for e in s["events"] if e.get("type") == "retry")
if n:
rt[n] += 1
print(f" retry distribution (sends with >=1 retry): {dict(sorted(rt.items()))}")
print("\n DONE.")
if __name__ == "__main__":
sys.exit(main())