Spans, attributes, and hierarchies
Auto-instrumentation alone gives you a flat list of LLM spans. A routing agent has structure: a turn contains a router call, a tool call, a synthesis call. If you do not add that structure, the Phoenix timeline looks like an unsorted log. A parent span per agent step is the fix.
Flat spans vs a readable hierarchy
The left side is what you get without parent spans. The right side is what a good trace looks like.
from opentelemetry import trace
tracer = trace.get_tracer("agent.session")
class EcommerceSession:
def ask(self, question: str) -> str:
self.turn_number += 1
with tracer.start_as_current_span(
f"turn_{self.turn_number}",
attributes={
"session.id": self.session_id,
"session.turn_number": self.turn_number,
},
):
final_state = self.graph.invoke({"user_message": question, "conversation_history": self.conversation_history})
return final_state["final_answer"]A single start_as_current_span at the top of the turn. Every downstream auto-span becomes a child, so the trace tree mirrors your agent graph.
Matching exercise: OpenInference span kinds
Loading practice…
Attributes are where the insight lives. Use stable names (session.id, agent.route, llm.model) so you can filter and aggregate across traces. Prefix your app-specific attributes with a namespace like agent. so they do not collide with OTel semantic conventions.
AI prompt: Try it: draft your span map
Loading practice…