OpenTelemetry
Django
Spans

Spans

The instrumentation creates spans for HTTP requests, DB queries, Celery tasks, cache calls, and outbound HTTP automatically. To measure sub-operations like business logic, third-party API calls, or batched work, create spans using the OTel Tracer API.

Getting a Tracer

Call trace.get_tracer(__name__) once per module. Tracers are cheap, cached by name, and safe to keep at module scope:

from opentelemetry import trace
 
tracer = trace.get_tracer(__name__)

The recommended pattern is start_as_current_span as a context manager. It activates the span, runs the block, ends the span on exit, and records any exception that propagates out:

class OrderService:
    def process_order(self, order_id: int) -> None:
        with tracer.start_as_current_span("order.validate"):
            self._validate_order(order_id)
 
        with tracer.start_as_current_span("order.charge"):
            self._charge_payment(order_id)

Exception handling inside start_as_current_span: by default, if the block raises, the span calls record_exception(exc) and set_status(ERROR) before re-raising. Both happen automatically. This is different from some other OTel SDKs (e.g. PHP's keepsuit/laravel-opentelemetry::measure) which only call record_exception and leave the status unset. If you want to suppress the auto-status (for example you're treating a Http404 as a normal outcome, not an error), pass set_status_on_exception=False:

with tracer.start_as_current_span("lookup-user", set_status_on_exception=False) as span:
    try:
        return User.objects.get(pk=user_id)
    except User.DoesNotExist:
        span.set_attribute("user.found", False)
        return None

set_status_on_exception=False only suppresses the span status. The exception event is still recorded if the exception escapes the block, and TracePath still opens an Issue from it. Catch the exception inside the block, as above, if you don't want that.

With Attributes

Set attributes on the span via the context manager's binding, or via trace.get_current_span() from anywhere inside the block:

with tracer.start_as_current_span("stripe.charge") as span:
    span.set_attribute("payment.amount", amount)
    span.set_attribute("payment.currency", "usd")
    return stripe.charge(amount)

To set initial attributes when the span is created (useful when sampling decisions depend on them), pass them through attributes=:

with tracer.start_as_current_span(
    "stripe.charge",
    attributes={"payment.amount": amount, "payment.currency": "usd"},
):
    return stripe.charge(amount)

Nested Spans

Child spans opened with start_as_current_span are automatically activated, so any further spans nest under them:

with tracer.start_as_current_span("order.fulfill"):
    with tracer.start_as_current_span("inventory.reserve"):
        reserve()
    with tracer.start_as_current_span("payment.charge"):
        charge()
    with tracer.start_as_current_span("email.send"):
        notify()

Manual Span Management

If you need finer control (e.g., spans that cross function boundaries or that need to outlive a with block), open the span manually and activate it with trace.use_span for the lifetime of the work:

from opentelemetry import trace
 
tracer = trace.get_tracer(__name__)
 
span = tracer.start_span("long-running-job")
with trace.use_span(span, end_on_exit=True):
    do_work()

Setting end_on_exit=True makes use_span close the span when the block exits. Always pair start_span with either use_span(..., end_on_exit=True) or an explicit span.end() in a finally block. Otherwise the span leaks and is never exported.

Root spans need a kind. Everything above assumes there is already a parent span, which is the case inside a request or a Celery task. A span you open at the top of a script, a cron entry, or a management command is a root span, and a root span with the default INTERNAL kind is dropped by TracePath with no error. Pass kind=SpanKind.CONSUMER so it becomes a Task. See Tasks.

Other Tracer Utilities

A handful of helpers on the OTel API surface come in handy when working with custom spans:

from opentelemetry import trace
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
 
trace.get_current_span()                        # the currently active Span
trace.get_current_span().get_span_context()     # SpanContext (trace_id, span_id, flags)
format(trace.get_current_span().get_span_context().trace_id, "032x")  # trace id as hex string
 
# Inject the active trace context into outbound headers. Most users don't need this,
# because requests/httpx/urllib3 instrumentation already injects W3C headers automatically.
carrier: dict[str, str] = {}
TraceContextTextMapPropagator().inject(carrier)
# carrier now contains "traceparent" (and "tracestate" if set)

Adding Attributes

Attach metadata to spans for filtering and debugging:

with tracer.start_as_current_span("db-query") as span:
    span.set_attribute("db.system", "postgresql")
    span.set_attribute("db.statement", "SELECT * FROM users WHERE id = $1")
    rows = run_query()
    span.set_attribute("db.row_count", len(rows))

For DB queries you go through Django's ORM, you don't need to do this. opentelemetry-instrumentation-psycopg2 (or your DB driver's matching instrumentation) sets db.system / db.statement automatically.

Recording Errors on Spans

When a span's operation fails and you don't want to rely on the auto-record behavior (for example you catch the error, downgrade it to a warning, and don't re-raise), record the exception and set the status explicitly:

import requests
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
 
tracer = trace.get_tracer(__name__)
 
with tracer.start_as_current_span("external-api-call") as span:
    try:
        response = requests.get("https://api.example.com/data", timeout=5)
        response.raise_for_status()
    except requests.RequestException as exc:
        span.record_exception(exc)
        span.set_status(Status(StatusCode.ERROR, str(exc)))
        raise

Span Naming Conventions

Use descriptive names that indicate the operation type:

GoodBad
db.users.findquery
cache.sessions.getcache
stripe.chargeapi
s3.upload-imageupload
email.send-welcomesend