Captured from feature/tracing-migrationf510940(rebased on master8284509), same Jaeger / request set / DB snapshot procedure as pre-migration. Result: names + semantic attributes preserved, traceparent intact, no orphaned spans, async/streaming paths now single traces. See post-migration/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.5 KiB
Python
150 lines
5.5 KiB
Python
"""#143 parity analysis: after-capture Jaeger dump vs the #137 baseline.
|
|
|
|
Writes span-inventory.json (baseline schema + nesting columns) and prints a
|
|
before/after diff plus targeted nesting / propagation checks.
|
|
"""
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
W = sys.argv[1]
|
|
after = json.load(open(f"{W}/traces-after.json"))["data"]
|
|
before_inv = {r["operation"]: r for r in json.load(open(f"{W}/inventory-before.json"))}
|
|
|
|
# Attributes tracing-opentelemetry adds to every span (location / thread /
|
|
# timing metadata). Reported separately so the diff shows semantic changes.
|
|
TRACING_EXTRAS = {
|
|
"busy_ns", "idle_ns", "target", "thread.id", "thread.name",
|
|
"code.file.path", "code.line.number", "code.module.name",
|
|
}
|
|
# Spans produced by my auth probes, not by the baseline request set.
|
|
PROBE_NOISE = set()
|
|
|
|
spans = {}
|
|
by_trace = defaultdict(list)
|
|
for t in after:
|
|
for s in t["spans"]:
|
|
s["_tags"] = {kv["key"]: kv["value"] for kv in s["tags"]}
|
|
spans[s["spanID"]] = s
|
|
by_trace[s["traceID"]].append(s)
|
|
|
|
|
|
def parent(s):
|
|
for r in s.get("references", []):
|
|
if r["refType"] == "CHILD_OF":
|
|
return r["spanID"], r["traceID"]
|
|
return None, None
|
|
|
|
|
|
def op(sid):
|
|
return spans[sid]["operationName"] if sid in spans else None
|
|
|
|
|
|
# Inventory in the baseline's schema, plus nesting columns.
|
|
inv = defaultdict(lambda: {"span_count": 0, "traces": set(), "kinds": set(), "scopes": set(),
|
|
"statuses": set(), "keys": set(), "parents": defaultdict(int),
|
|
"orphans": 0})
|
|
for s in spans.values():
|
|
r = inv[s["operationName"]]
|
|
r["span_count"] += 1
|
|
r["traces"].add(s["traceID"])
|
|
r["kinds"].add(s["_tags"].get("span.kind", "internal"))
|
|
r["scopes"].add(s["_tags"].get("otel.scope.name", "?"))
|
|
r["statuses"].add(s["_tags"].get("otel.status_code", "(none)"))
|
|
r["keys"].update(s["_tags"])
|
|
pid, _ = parent(s)
|
|
if pid is None:
|
|
r["orphans"] += 1
|
|
else:
|
|
r["parents"][op(pid) or "(remote)"] += 1
|
|
|
|
inventory = []
|
|
for name, r in sorted(inv.items(), key=lambda kv: -kv[1]["span_count"]):
|
|
inventory.append({
|
|
"operation": name,
|
|
"span_count": r["span_count"],
|
|
"trace_count": len(r["traces"]),
|
|
"kind": "/".join(sorted(r["kinds"])),
|
|
"scope": "/".join(sorted(r["scopes"])),
|
|
"status": "/".join(sorted(r["statuses"])),
|
|
"attribute_keys": sorted(r["keys"] - TRACING_EXTRAS),
|
|
"parents": dict(r["parents"]),
|
|
"root_count": r["orphans"],
|
|
})
|
|
json.dump(inventory, open(f"{W}/span-inventory.json", "w"), indent=2)
|
|
|
|
print("== Operations: before -> after ==")
|
|
before_ops, after_ops = set(before_inv), set(inv)
|
|
print("only before:", sorted(before_ops - after_ops))
|
|
print("only after: ", sorted(after_ops - before_ops))
|
|
print()
|
|
print("== Common operations: kind / scope / attribute-key changes ==")
|
|
for name in sorted(before_ops & after_ops):
|
|
b, a = before_inv[name], next(x for x in inventory if x["operation"] == name)
|
|
bk, ak = set(b["attribute_keys"]), set(a["attribute_keys"])
|
|
changes = []
|
|
if b["kind"] != a["kind"]:
|
|
changes.append(f"kind {b['kind']}->{a['kind']}")
|
|
if b["scope"] != a["scope"]:
|
|
changes.append(f"scope {b['scope']}->{a['scope']}")
|
|
if bk - ak:
|
|
changes.append(f"LOST {sorted(bk - ak)}")
|
|
if ak - bk:
|
|
changes.append(f"added {sorted(ak - bk)}")
|
|
nest = f"traces {b['trace_count']}/{b['span_count']} -> {a['trace_count']}/{a['span_count']}, roots={a['root_count']}"
|
|
print(f"{name}: {'; '.join(changes) or 'same'} | {nest}")
|
|
|
|
print()
|
|
print("== Roots in the after capture (span with no parent) ==")
|
|
for s in spans.values():
|
|
if parent(s)[0] is None:
|
|
print(f" {s['operationName']} trace={s['traceID'][:8]} spans_in_trace={len(by_trace[s['traceID']])}")
|
|
|
|
|
|
def chain(s):
|
|
out = [s["operationName"]]
|
|
pid, _ = parent(s)
|
|
while pid:
|
|
if pid not in spans:
|
|
out.append(f"(remote {pid})")
|
|
break
|
|
out.append(spans[pid]["operationName"])
|
|
pid, _ = parent(spans[pid])
|
|
return " <- ".join(out)
|
|
|
|
|
|
print()
|
|
print("== Ancestry of key spans ==")
|
|
seen = set()
|
|
for s in spans.values():
|
|
n = s["operationName"]
|
|
key = (n, s["_tags"].get("llm.operation"))
|
|
if n in ("llm.request", "ai.agentic.loop", "ai.insight.generate_agentic",
|
|
"db.insert.store_insight", "db.update.update_training_messages",
|
|
"ai.chat.turn.stream", "list_photos", "list_memories",
|
|
"generate_video_thumbnail") and key not in seen:
|
|
seen.add(key)
|
|
extra = f" [llm.operation={key[1]}]" if key[1] else ""
|
|
print(f" {chain(s)}{extra}")
|
|
|
|
print()
|
|
print("== traceparent probe ==")
|
|
probe = [s for s in spans.values() if s["traceID"].startswith("11111111")]
|
|
for s in sorted(probe, key=lambda s: s["startTime"]):
|
|
print(f" {chain(s)} trace={s['traceID']}")
|
|
|
|
print()
|
|
print("== llm.request attribute presence ==")
|
|
for s in spans.values():
|
|
if s["operationName"] == "llm.request":
|
|
t = s["_tags"]
|
|
print(f" op={t.get('llm.operation')} model={t.get('llm.model')} provider={t.get('llm.provider')} "
|
|
f"status={t.get('http.status_code')} tokens_in={t.get('llm.tokens.input')} "
|
|
f"parent={op(parent(s)[0])}")
|
|
print()
|
|
print("== turn / insight spans: llm.backend + llm.model ==")
|
|
for s in spans.values():
|
|
if s["operationName"] in ("ai.chat.turn.stream", "ai.insight.generate_agentic"):
|
|
t = s["_tags"]
|
|
print(f" {s['operationName']}: llm.backend={t.get('llm.backend')} llm.model={t.get('llm.model')} backend={t.get('backend')} model={t.get('model')}")
|