← Python Code Deployment & Monitoring
Browse Python Concepts

OpenTelemetry Basics — Traces and Spans

Mental Model

Imagine OpenTelemetry spans forming a nested hierarchy, like folders on a computer. tracer.start_span() creates a new folder, but doesn't automatically "cd" into it. To ensure new files (sub-spans) are created inside the correct folder, you must explicitly cd into it using a context manager, making it the active working directory.

Rule: Always use tracer.start_as_current_span() as a context manager to maintain proper span relationships in Python.

The Setup

You are instrumenting an asynchronous API to trace concurrent database operations. You create spans for each task, but when looking at your APM dashboard, the traces are scrambled and nested incorrectly.

What Does This Print?

Broken code
Python
import asyncio
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

async def fetch_item(item_id):
    # Manually starting a span without setting/managing its context
    span = tracer.start_span(f"db_fetch_{item_id}")
    await asyncio.sleep(0.01)
    # Forget to close or use contextual scope managers
    print(f"Fetched {item_id} under Active Span: {trace.get_current_span().context.span_id}")

async def run_pipeline():
    await asyncio.gather(fetch_item(101), fetch_item(102))

asyncio.run(run_pipeline())
Predict what happens to the active span IDs when these two asynchronous tasks run concurrently.

The Output

What actually happens
Fetched 101 under Active Span: 0 Fetched 102 under Active Span: 0

Manually calling tracer.start_span() starts a trace element, but does not bind it as the active span context in the executing thread or async task scope. Consequently, any downstream spans or auto-instrumented database tasks will fail to establish parent-child relationships, resulting in disjointed, unassociated spans on your tracing backend.

Why Python Does This

OpenTelemetry traces rely on context propagation to track relationships. Under the hood, Python leverages standard library contextvars to manage scoped variables across asynchronous task yields (await). Direct activation of a span is missing when invoking start_span(). To bind a span dynamically to the task's async context, you must execute it inside a context manager using start_as_current_span(), which handles token activation and ensures cleanup upon execution block exits.

The Fix

Corrected pattern
Python
import asyncio
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

async def fetch_item(item_id):
    # Fix: Use context manager to bind current trace context to async execution
    with tracer.start_as_current_span(f"db_fetch_{item_id}") as span:
        await asyncio.sleep(0.01)
        current_span_id = trace.get_current_span().context.span_id
        print(f"Fetched {item_id} under Active Span: {current_span_id}")

async def run_pipeline():
    await asyncio.gather(fetch_item(101), fetch_item(102))

asyncio.run(run_pipeline())

Using tracer.start_as_current_span() as a context manager ensures that the created span is not only started but also bound as the "active" span in the current execution context (thread or async task). This allows child spans and auto-instrumentation to correctly link to the parent, maintaining the trace hierarchy.

How This Fails in Real Systems

A microservice backend showed high database latency, but the APM spans for PostgreSQL queries appeared detached from incoming HTTP request trace chains. This tracing bug hid query blockages, lengthening root cause analysis of database lockouts by 3 days.

Key Takeaway

Always use tracer.start_as_current_span() as a context manager to maintain proper span relationships in Python.
Common mistake: Developers creating OpenTelemetry spans manually use tracer.start_span(), assuming it automatically makes the new span the "active" one for subsequent operations, failing to realize that span context must be explicitly managed within the execution scope.