Skip to content

Spans

Spans are the fundamental building blocks of distributed tracing in Prela. A span represents a unit of work in your AI agent system, such as an LLM call, tool invocation, or custom operation.

Overview

Each span captures:

  • Identity: Unique span ID and trace ID for correlation
  • Timing: Start and end timestamps with nanosecond precision
  • Metadata: Attributes, events, and status information
  • Hierarchy: Parent-child relationships for nested operations
graph TD
    A[Root Span: Agent Request] --> B[Span: LLM Call]
    A --> C[Span: Tool Execution]
    C --> D[Span: Database Query]
    B --> E[Span: Token Generation]

Span Structure

from prela import Span, SpanType, SpanStatus

span = Span(
    span_id="550e8400-e29b-41d4-a716-446655440000",
    trace_id="123e4567-e89b-12d3-a456-426614174000",
    parent_span_id=None,  # Root span
    name="agent.process_request",
    span_type=SpanType.AGENT,
    status=SpanStatus.SUCCESS,
    attributes={
        "user_id": "user123",
        "request_type": "chat"
    }
)

Span Types

Prela provides semantic span types for different operations:

SpanType.AGENT

Represents agent-level operations (reasoning, planning, orchestration).

from prela import get_tracer, SpanType

with get_tracer().span("process_query", SpanType.AGENT) as span:
    span.set_attribute("query", "What's the weather?")
    # Agent logic here

SpanType.LLM

Represents LLM API calls (automatically created by instrumentation).

# Auto-instrumented LLM calls create LLM spans
from anthropic import Anthropic

client = Anthropic()
response = client.messages.create(...)  # Creates SpanType.LLM

SpanType.TOOL

Represents tool or function calls made by the agent.

with get_tracer().span("search_database", SpanType.TOOL) as span:
    span.set_attribute("tool.name", "database_search")
    span.set_attribute("tool.input", query)
    result = search(query)
    span.set_attribute("tool.output", result)

SpanType.RETRIEVAL

Represents retrieval operations (vector search, RAG, knowledge base queries).

with get_tracer().span("vector_search", SpanType.RETRIEVAL) as span:
    span.set_attribute("retrieval.query", text)
    span.set_attribute("retrieval.top_k", 10)
    docs = vector_db.search(text, k=10)
    span.set_attribute("retrieval.num_results", len(docs))

SpanType.EMBEDDING

Represents embedding generation operations.

with get_tracer().span("generate_embedding", SpanType.EMBEDDING) as span:
    span.set_attribute("embedding.model", "text-embedding-ada-002")
    span.set_attribute("embedding.input", text)
    embedding = model.embed(text)
    span.set_attribute("embedding.dimensions", len(embedding))

SpanType.CUSTOM

Represents custom operations specific to your application.

with get_tracer().span("custom_operation", SpanType.CUSTOM) as span:
    span.set_attribute("operation.type", "data_processing")
    # Custom logic

Span Lifecycle

Spans follow a strict lifecycle:

stateDiagram-v2
    [*] --> Created
    Created --> Running: start()
    Running --> Ended: end()
    Ended --> [*]

    note right of Running
        Can add attributes,
        events, update status
    end note

    note right of Ended
        Immutable - no changes
        allowed after end()
    end note

1. Creation

from prela import Span, SpanType
from datetime import datetime, timezone

span = Span(
    name="my_operation",
    span_type=SpanType.CUSTOM,
    started_at=datetime.now(timezone.utc)
)

2. Running (Mutable)

During execution, you can add attributes and events:

# Add attributes
span.set_attribute("user_id", "user123")
span.set_attribute("model", "claude-sonnet-4")

# Add events
span.add_event("checkpoint_reached", {"step": 1})
span.add_event("cache_hit", {"key": "abc123"})

3. Completion (Immutable)

Once ended, spans become immutable:

span.end()  # Status defaults to SUCCESS

# After end(), modifications raise RuntimeError
try:
    span.set_attribute("foo", "bar")
except RuntimeError as e:
    print(e)  # "Span has ended and is immutable"

Error Handling

try:
    # Operation that might fail
    result = risky_operation()
    span.set_attribute("result", result)
except Exception as e:
    span.set_status(SpanStatus.ERROR, str(e))
    raise
finally:
    span.end()

Span Status

Spans have one of three statuses:

Status Description Use Case
PENDING Operation in progress Default when span is created
SUCCESS Operation completed successfully Default when end() is called
ERROR Operation failed Set manually when exceptions occur
from prela import SpanStatus

# Manual status control
span.set_status(SpanStatus.SUCCESS)
span.set_status(SpanStatus.ERROR, "Connection timeout after 30s")

# Check current status
if span.status == SpanStatus.ERROR:
    print(f"Error: {span.status_message}")

Attributes

Attributes are key-value metadata attached to spans. They provide context about the operation.

Setting Attributes

# Single attribute
span.set_attribute("user_id", "user123")

# Multiple attributes
span.set_attribute("model", "gpt-4")
span.set_attribute("temperature", 0.7)
span.set_attribute("max_tokens", 1000)

# Complex values (automatically serialized)
span.set_attribute("metadata", {"key": "value"})

For LLM operations:

span.set_attribute("llm.vendor", "anthropic")
span.set_attribute("llm.model", "claude-sonnet-4-20250514")
span.set_attribute("llm.temperature", 1.0)
span.set_attribute("llm.input_tokens", 100)
span.set_attribute("llm.output_tokens", 50)

For tool operations:

span.set_attribute("tool.name", "web_search")
span.set_attribute("tool.input", query)
span.set_attribute("tool.output", results)

For retrieval operations:

span.set_attribute("retrieval.query", text)
span.set_attribute("retrieval.top_k", 5)
span.set_attribute("retrieval.num_results", 3)

Events

Events are timestamped occurrences during span execution.

from prela import SpanEvent
from datetime import datetime, timezone

# Add event with automatic timestamp
span.add_event("cache_miss", {"cache_key": "abc123"})

# Add event with explicit timestamp
span.add_event(
    "retry_attempted",
    {"attempt": 2, "delay_ms": 1000}
)

# Access events
for event in span.events:
    print(f"{event.timestamp}: {event.name}")
    print(f"  Attributes: {event.attributes}")

Common event types: - llm.request: LLM request details - llm.response: LLM response details - tool.call: Tool invocation - cache.hit / cache.miss: Cache access - retry.attempted: Retry logic

Context Manager Usage

The recommended way to use spans is with context managers:

from prela import get_tracer, SpanType

# Basic usage
with get_tracer().span("my_operation") as span:
    span.set_attribute("key", "value")
    # Operation here
    # span.end() called automatically

# With span type
with get_tracer().span("llm_call", SpanType.LLM) as span:
    # LLM operation
    pass

# Nested spans
with get_tracer().span("parent_op", SpanType.AGENT) as parent:
    parent.set_attribute("stage", "planning")

    with get_tracer().span("child_op", SpanType.TOOL) as child:
        child.set_attribute("tool", "calculator")
        # Child operation

Serialization

Spans can be serialized to/from dictionaries for storage or transmission:

# Serialize to dict
span_dict = span.to_dict()
# {
#     "span_id": "...",
#     "trace_id": "...",
#     "name": "my_operation",
#     "span_type": "custom",
#     "status": "success",
#     ...
# }

# Deserialize from dict
from prela import Span
reconstructed = Span.from_dict(span_dict)

Performance Considerations

Prela spans are designed for high performance:

  • Memory Efficient: Uses __slots__ to reduce memory overhead
  • Lazy Serialization: Only serializes when exported
  • Immutability: Ended spans can be safely shared across threads
  • Minimal Overhead: Typical span creation adds <100 microseconds
# Good: Minimal attributes for high-volume operations
with get_tracer().span("fast_op") as span:
    span.set_attribute("id", request_id)

# Avoid: Too many attributes in tight loops
# for i in range(10000):
#     with get_tracer().span(f"item_{i}") as span:
#         span.set_attribute("index", i)
#         span.set_attribute("data", large_dict)  # Expensive

Best Practices

1. Use Semantic Span Types

# Good
with get_tracer().span("query_db", SpanType.RETRIEVAL) as span:
    ...

# Less useful
with get_tracer().span("query_db", SpanType.CUSTOM) as span:
    ...

2. Add Relevant Attributes

# Good: Useful for debugging and analysis
span.set_attribute("user_id", user_id)
span.set_attribute("model", model_name)
span.set_attribute("latency_ms", duration)

# Bad: Too much detail
span.set_attribute("entire_request_body", massive_json)

3. Use Events for Milestones

span.add_event("validation_started")
# Validation logic
span.add_event("validation_completed", {"errors": 0})

4. Always Use Context Managers

# Good: Automatic cleanup
with get_tracer().span("operation") as span:
    do_work()

# Bad: Manual lifecycle management
span = get_tracer().start_span("operation")
try:
    do_work()
finally:
    span.end()  # Easy to forget!

5. Set Error Status on Exceptions

with get_tracer().span("risky_op") as span:
    try:
        risky_operation()
    except Exception as e:
        span.set_status(SpanStatus.ERROR, str(e))
        raise

Next Steps