Files
ImageApi/docs/tracing-baseline/post-migration/analyze.py
T
Cameron CordesandClaude Opus 5.5 e754e149d7 parity(#143): post-migration span capture + diff vs #137 baseline
Captured from feature/tracing-migration f510940 (rebased on master 8284509),
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>
2026-09-29 17:29:31 -04:00

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')}")