Skip to main content
Decorator-First: Use @trace decorators as your primary pattern. Context managers are for special cases like loops or conditional tracing.

Overview

Custom spans let you trace specific business logic, workflow steps, and application components beyond just LLM calls. Use Cases:
  • Business process tracking
  • Performance bottleneck identification
  • Complex workflow visualization
  • Custom error tracking

The @trace Decorator

The recommended approach for function-level tracing:
import os
from honeyhive import HoneyHiveTracer, trace, enrich_span

tracer = HoneyHiveTracer.init(
    api_key=os.getenv("HH_API_KEY"),
    project=os.getenv("HH_PROJECT")
)

@trace
def process_request(user_id: str, data: dict) -> dict:
    """Automatically traced with inputs/outputs captured."""
    
    # Add custom context (see Enriching Traces for patterns)
    enrich_span({"user_id": user_id})
    
    result = do_processing(data)
    return {"status": "success", "data": result}

@trace
def nested_workflow(request: dict) -> dict:
    """Nested calls create trace hierarchy automatically."""
    validated = validate(request)      # Child span
    processed = process(validated)     # Child span
    return save(processed)             # Child span
Benefits:
  • ✅ Automatic inputs/outputs capture
  • ✅ Nested calls create proper trace hierarchy
  • ✅ Clean code without span management clutter
For details on adding metadata with enrich_span(), see Enriching Traces.

Async Functions

The @trace decorator works with both sync and async functions automatically:
@trace
async def fetch_data(url: str) -> dict:
    """Async function - @trace works automatically."""
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

# Call with await
result = await fetch_data("https://api.example.com/data")
No separate @atrace needed. The decorator detects async functions automatically.

Context Managers

Use context managers for scenarios where decorators don’t fit:

When to Use

  • Loop iterations - Tracing individual items in batch processing
  • Conditional spans - Dynamic span creation based on runtime conditions
  • Non-function blocks - Setup, cleanup, or configuration phases
  • Regular functions - Use @trace instead
Creates spans with automatic HoneyHive namespacing:
from honeyhive.tracer.processing.context import enrich_span_context

@trace
def process_batch(items: list) -> list:
    results = []
    
    for i, item in enumerate(items):
        with enrich_span_context(
            event_name=f"process_item_{i}",
            inputs={"item": item},
            metadata={"batch_size": len(items)}
        ):
            result = transform_item(item)
            tracer.enrich_span(outputs={"result": result})
            results.append(result)
    
    return results

tracer.start_span() (Low-Level)

For raw OpenTelemetry-style control:
with tracer.start_span("process_item") as span:
    span.set_attribute("item.index", i)
    result = do_processing()
    span.set_attribute("success", True)

Comparison

Featureenrich_span_context()tracer.start_span()
Auto namespacing✅ Automatic❌ Manual
HoneyHive enrichment✅ Built-in❌ Manual attributes
Best forBusiness logicLow-level control

Conditional Spans

Create spans only when conditions are met:
import os

DEBUG_MODE = os.getenv("DEBUG", "false").lower() == "true"

@trace
def operation_with_debug(data: dict):
    if DEBUG_MODE:
        with enrich_span_context(
            event_name="debug_inspection",
            inputs={"data": data}
        ):
            inspect_data(data)
    
    return process(data)

Best Practices

Span Naming

# ✅ Good: Descriptive, hierarchical
@trace(event_name="user_authentication")
@trace(event_name="payment_processing_stripe")

# ❌ Bad: Generic
@trace(event_name="process")
@trace(event_name="api_call")

Avoid Over-Instrumentation

# ❌ Bad: Span per item in hot path
for item in million_items:
    with enrich_span_context(event_name="process_item"):
        process(item)

# ✅ Good: Batch-level span only
@trace
def process_batch(items: list):
    for item in items:
        process(item)