पाठ 4 / 31

Traces, Spans and Observations

यह पाठ अभी हिंदी में अनुवादित नहीं है, इसलिए अंग्रेज़ी में दिखाया जा रहा है।

Describe a trace as a tree of timed steps and read parent-child relationships in a trace view.

लेखक: · 15 साल से वेब डेवलपर

A trace is a tree of steps

A trace represents one end-to-end unit of work, usually one user request such as "answer this chat message". Inside it are spans: timed steps with a name, start and end time, input, output and metadata. Spans nest. The top-level span (the root) might be chat-request; under it sit children such as retrieve-docs and llm-call, and an agent step can have its own children for each tool call. Every span except the root records its parent, which is how the tool draws the tree, and each span's start and end times show how long that step took. Naming differs between tools: Langfuse calls the steps observations (with types such as span, generation and event), while LangSmith calls each step a run and the tree a run tree. OpenTelemetry simply calls them spans. The idea is the same everywhere: one tree per request, so you can open a slow or wrong answer and see exactly which step misbehaved.

From request to trace tree

A request enters the app, the root span opens, child spans record retrieval and generation, and the finished tree is sent to the observability backend in the background.

Four connected stages: request arrives, root span opens, child spans record steps, trace is exported.
Figure 2.1: request -> root span -> child spans -> export to backend

Tracing nesting with a toy tracer

This tiny tracer keeps a stack of open spans, which is roughly how SDKs track the current parent. Follow how each span finds its parent.

from contextlib import contextmanager

stack = []

@contextmanager
def span(name):
    parent = stack[-1] if stack else None
    stack.append(name)
    print(f"start {name} (parent={parent})")
    try:
        yield
    finally:
        stack.pop()
        print(f"end   {name}")

with span("chat-request"):
    with span("retrieve-docs"):
        pass
    with span("llm-call"):
        pass
  1. Line 16: Entering the outer `with` opens the root span. The stack is empty, so it has no parent.
  2. Line 17: The nested `with` opens a child. The top of the stack is `chat-request`, so that becomes the parent.
  3. Line 18: The block finishes, the `finally` pops the span and records its end.
  4. Line 19: The second child opens. The stack again holds only the root, so it is a sibling of the retrieval span, not its child.
  5. Line 20: The LLM span closes and pops.
  6. Line 16: Leaving the outer block closes the root last: a parent always ends after its children.

Running the toy tracer

The real output of the flow above. Real SDKs use context variables instead of a global list so async tasks and threads keep separate stacks.

from contextlib import contextmanager

stack = []

@contextmanager
def span(name):
    parent = stack[-1] if stack else None
    stack.append(name)
    print(f"start {name} (parent={parent})")
    try:
        yield
    finally:
        stack.pop()
        print(f"end   {name}")

with span("chat-request"):
    with span("retrieve-docs"):
        pass
    with span("llm-call"):
        pass

Output:

start chat-request (parent=None)
start retrieve-docs (parent=chat-request)
end   retrieve-docs
start llm-call (parent=chat-request)
end   llm-call
end   chat-request

त्वरित जाँच: In a trace tree, what is the relationship between a retrieval span and an LLM span that both run directly inside the root span?

  • The LLM span is the parent of the retrieval span
  • They belong to two different traces
  • They are siblings: both are children of the root span
  • The retrieval span is the root
Answer

They are siblings: both are children of the root span — Both were opened while the root was the current span, so both record the root as their parent, making them siblings.