Django’s request tracing with OpenTelemetry is surprisingly less about capturing data and more about making the right decisions about what data to capture and how to structure it.

Let’s see it in action. Imagine a Django view that queries a database and then makes an external HTTP request.

# views.py
from django.http import HttpResponse
import requests
from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def my_view(request):
    with tracer.start_as_current_span("my_view"):
        # Simulate database query
        with tracer.start_as_current_span("db_query"):
            # In a real app, this would be ORM calls, etc.
            import time
            time.sleep(0.05) 
            db_result = "data from db"

        # Simulate external API call
        with tracer.start_as_current_span("external_api_call"):
            response = requests.get("https://httpbin.org/delay/1") # Simulates a 1-second delay
            external_data = response.json()

        return HttpResponse(f"Data: {db_result}, External: {external_data['url']}")

To make this work, you’d need to set up OpenTelemetry with a Django integration. A common way is using the opentelemetry-django package.

# settings.py (simplified)
INSTALLED_APPS = [
    # ...
    'opentelemetry.instrumentation.django',
    # ...
]

# Configure the tracer provider and exporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
span_processor = BatchSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(span_processor)

# Set the global tracer provider
from opentelemetry import trace
trace.set_tracer_provider(provider)

# You'd also configure the Django instrumentation itself,
# often via environment variables or specific settings.
# For example, to enable HTTP requests instrumentation:
# OTEL_INSTRUMENTATION_HTTP_CLIENT_ENABLED=true

This setup will automatically create spans for incoming HTTP requests to your Django app, and our manual with tracer.start_as_current_span(...) blocks will create nested spans for the database query and external API call. The result, when viewed in a tracing backend like Jaeger or Honeycomb, would show a tree of operations: a root span for the incoming request, with child spans for db_query and external_api_call, and potentially further children for the underlying HTTP client calls made by requests.

The core problem OpenTelemetry tracing solves for web applications is visibility into distributed systems. When a request traverses multiple services (e.g., your Django app, a microservice, a database, a third-party API), a simple log on one service doesn’t tell you where the bottleneck is or what caused the failure. Tracing links these disparate operations together into a single, end-to-end view. You can see not just that a request took 5 seconds, but which specific part of that 5 seconds was spent in your Django view, in the database, or waiting for an external service.

The fundamental levers you control are the granularity and context of your spans. Granularity is how many spans you create. Too few, and you don’t have enough detail. Too many, and the overhead becomes significant, and the trace becomes noisy. Context is the metadata attached to spans: attributes (key-value pairs) and events. For instance, you might add attributes to the db_query span like db.system: postgres, db.name: myapp_db, and db.statement: SELECT .... For the external API call, you’d add http.method: GET, http.url: https://httpbin.org/delay/1, and http.status_code: 200. This rich context is what allows for powerful filtering and analysis in your tracing backend.

When you instrument external HTTP requests using libraries like requests with OpenTelemetry, the instrumentation automatically propagates the trace context (a set of headers like traceparent and tracestate) from the incoming request. This means that the child span created for the outgoing HTTP request to httpbin.org will be correctly linked to the parent span (external_api_call) in your Django app, and importantly, will also be linked to the trace initiated by the original request. This context propagation is the magic that stitches together calls across service boundaries.

The most surprising thing is that the default instrumentation for Django and many HTTP clients will automatically propagate trace context. You don’t need to manually add traceparent headers to outgoing requests if you’re using standard libraries and the OpenTelemetry SDK is initialized. The instrumentation handles it.

The next concept you’ll likely grapple with is sampling: deciding which traces to send to your backend when dealing with high traffic.

Want structured learning?

Take the full Django course →