Instruments AI agent runs with structured traces, token accounting, and failure forensics using OpenTelemetry GenAI conventions. Triggers when an agent moves from prototype to service, shows flaky failures with no reproducer, needs cost attribution per user or feature, or adopts Langfuse, LangSmith, Phoenix, or similar tracing.
./install.sh agent-observabilityname: "agent_observability" description: "Instruments AI agent runs with structured traces, token accounting, and failure forensics using OpenTelemetry GenAI conventions. Triggers when an agent moves from prototype to service, shows flaky failures with no reproducer, needs cost attribution per user or feature, or adopts Langfuse, LangSmith, Phoenix, or similar tracing."
Turn an agent from a black box into a replayable record. One user-facing request becomes one root trace with nested spans for every model call, tool execution, retrieval step, and guard check. Each span records what the step saw, what it chose, and what it cost, so failures are debuggable and spend is attributable.
gen_ai.* attribute namespace so the data stays portable across backends. One trace per request, one span per step. Attribute details live in references/span-attributes.md.references/failure-forensics.md.Stdlib-only tracer an agent can drop into a project. Run it with python3, get a JSON trace:
python
import hashlib, json, time, uuid
from contextlib import contextmanager
class Tracer:
def __init__(self):
self.spans = []
self._stack = []
@staticmethod
def _attr_name(key):
# gen_ai_operation_name -> gen_ai.operation.name (matches the OTel spec)
if key.startswith("gen_ai_"):
return "gen_ai." + key[7:].replace("_", ".")
return key
@contextmanager
def span(self, name, **attrs):
attrs = {Tracer._attr_name(k): v for k, v in attrs.items()}
span = {"span_id": uuid.uuid4().hex[:16], "name": name,
"parent_id": self._stack[-1]["span_id"] if self._stack else None,
"start": time.time(), "attrs": attrs}
self._stack.append(span)
try:
yield span
except Exception as e:
span["status"] = {"code": "ERROR", "message": str(e)}
raise
else:
span.setdefault("status", {"code": "OK"})
finally:
span["end"] = time.time()
span["duration_s"] = round(span["end"] - span["start"], 4)
self._stack.pop()
self.spans.append(span)
def fingerprint(*parts):
return hashlib.sha256("|".join(parts).encode()).hexdigest()[:12]
def agent_run(tracer, model, price_table, goal):
trace_id = uuid.uuid4().hex
total_cost = 0.0
total_in, total_out = 0, 0
with tracer.span("invoke_agent", gen_ai_operation_name="invoke_agent",
gen_ai_request_model=model,
gen_ai_agent_id="billing-refund-v2",
prompt_fp=fingerprint("sys-prompt-v3", "tool-schema-v7", model)):
with tracer.span("chat", gen_ai_operation_name="chat",
gen_ai_request_model=model) as s:
# In real code these come from the provider's usage payload.
in_tok, out_tok = 1420, 280
cost = in_tok * price_table["input"] + out_tok * price_table["output"]
s["attrs"].update({"gen_ai.usage.input_tokens": in_tok,
"gen_ai.usage.output_tokens": out_tok,
"cost_usd": round(cost, 6)})
total_cost += cost
total_in += in_tok
total_out += out_tok
with tracer.span("execute_tool get_weather",
gen_ai_operation_name="execute_tool",
gen_ai_tool_name="get_weather"):
pass # Tool runs here. Redact args and results at write time.
return {"trace_id": trace_id, "goal": goal,
"total_cost_usd": round(total_cost, 6),
"total_input_tokens": total_in, "total_output_tokens": total_out,
"spans": tracer.spans}
if __name__ == "__main__":
# Placeholder rates. Load real ones from a versioned price table.
price = {"input": 3.0 / 1_000_000, "output": 15.0 / 1_000_000}
t = Tracer()
trace = agent_run(t, "claude-3-7-sonnet-20250219", price, "check refund status")
with open("/tmp/agent_trace.json", "w") as f:
json.dump(trace, f, indent=2)
print("trace_id", trace["trace_id"], "cost_usd", trace["total_cost_usd"])
publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.