CubeAPM
CubeAPM CubeAPM

How to Instrument Django Applications with OpenTelemetry 

How to Instrument Django Applications with OpenTelemetry 

Table of Contents

Django applications can be instrumented with OpenTelemetry in two ways: automatically using the opentelemetry-instrument command with zero code changes, or programmatically by calling DjangoInstrumentor().instrument() inside your application. Both approaches produce the same spans. 

The programmatic approach is the right choice for most production deployments because it gives you explicit control over SDK initialization order, exporter configuration, and signal setup. This guide covers both paths end-to-end, including database query tracing, ASGI setup, log correlation, and manual spans.

Key Takeaways

  • Latest versions as of May 2026: opentelemetry-api==1.42.1, opentelemetry-sdk==1.42.1, opentelemetry-instrumentation-django==0.62b1. The Django instrumentation package requires Python 3.9 or above.
  • Django 6.0.5 is the latest release. Django 5.2.x is the current LTS (security support until April 2028). Django 4.2 LTS reached the end of support in April 2026.
  • DjangoInstrumentor().instrument() must be called after DJANGO_SETTINGS_MODULE is set and before Django processes any requests. The correct place in most deployments is manage.py or the WSGI/ASGI entry point.
  • Database query spans are not captured by the Django instrumentation package alone. Install the appropriate database driver instrumentation separately: opentelemetry-instrumentation-psycopg2 for PostgreSQL via psycopg2, opentelemetry-instrumentation-psycopg for psycopg3, or opentelemetry-instrumentation-sqlite3 for SQLite.
  • The default OTLP export protocol is HTTP/protobuf (port 4318). Use opentelemetry-exporter-otlp-proto-http unless your environment specifically requires gRPC.
  • Use OTEL_PYTHON_DJANGO_EXCLUDED_URLS to exclude health check and readiness endpoints from tracing.

What Gets Instrumented Automatically

When you instrument a Django application with OpenTelemetry, the following are captured without additional code:

SignalWhat is captured
TracesOne span per HTTP request: method, URL template (not filled path), status code, duration
Context propagationIncoming W3C TraceContext headers are read and used to continue parent traces from upstream services
Request attributesHTTP method, URL, status code, user agent attached as span attributes by default

Database query spans, outgoing HTTP call spans, and Celery task spans require separate instrumentation packages installed alongside the Django package.

Step 1: Install the Required Packages

pip install opentelemetry-api==1.42.1 \

            opentelemetry-sdk==1.42.1 \

            opentelemetry-instrumentation-django==0.62b1 \

            opentelemetry-exporter-otlp-proto-http==1.42.1

Install database driver instrumentation matching your backend:

# PostgreSQL via psycopg2 (Python 3.10+ required for 0.63b0)

pip install opentelemetry-instrumentation-psycopg2==0.63b0

# PostgreSQL via psycopg3

pip install opentelemetry-instrumentation-psycopg==0.62b1

# MySQL via mysqlclient

pip install opentelemetry-instrumentation-dbapi==0.62b1

# SQLite

pip install opentelemetry-instrumentation-sqlite3==0.62b1

Note: opentelemetry-instrumentation-psycopg2==0.63b0 requires Python 3.10 or above. If you are running Python 3.9, pin to 0.62b1 instead, which supports Python 3.9.

For outgoing HTTP calls via the requests library:

pip install opentelemetry-instrumentation-requests==0.63b0

Pin all packages in requirements.txt to keep builds reproducible:

opentelemetry-api==1.42.1

opentelemetry-sdk==1.42.1

opentelemetry-instrumentation-django==0.62b1

opentelemetry-instrumentation-psycopg2==0.63b0

opentelemetry-exporter-otlp-proto-http==1.42.1

Step 2A: Programmatic Instrumentation (Recommended)

Initialize the OTel SDK and call DjangoInstrumentor().instrument() before Django starts serving requests. The correct place is manage.py for development and the WSGI or ASGI entry point for production.

manage.py

#!/usr/bin/env python

import os

import sys

def main():

    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

    # Initialize OTel before Django imports

    from opentelemetry import trace, metrics

    from opentelemetry.sdk.trace import TracerProvider

    from opentelemetry.sdk.trace.export import BatchSpanProcessor

    from opentelemetry.sdk.metrics import MeterProvider

    from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

    from opentelemetry.sdk.resources import Resource

    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

    from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter

    from opentelemetry.instrumentation.django import DjangoInstrumentor

    resource = Resource.create({

        "service.name": "order-service",

        "service.version": "1.0.0",

        "deployment.environment": "production",

    })

    tracer_provider = TracerProvider(resource=resource)

    tracer_provider.add_span_processor(

        BatchSpanProcessor(

            OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")

        )

    )

    trace.set_tracer_provider(tracer_provider)

    metric_reader = PeriodicExportingMetricReader(

        OTLPMetricExporter(endpoint="http://otel-collector:4318/v1/metrics"),

        export_interval_millis=60000,

    )

    meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])

    metrics.set_meter_provider(meter_provider)

    # Instrument Django after DJANGO_SETTINGS_MODULE is set

    DjangoInstrumentor().instrument()

    try:

        from django.core.management import execute_from_command_line

    except ImportError as exc:

        raise ImportError(

            "Couldn't import Django. Are you sure it's installed and "

            "available on your PYTHONPATH environment variable? Did you "

            "forget to activate a virtual environment?"

        ) from exc

    execute_from_command_line(sys.argv)

if __name__ == '__main__':

    main()

wsgi.py (production WSGI deployments)

import os

from opentelemetry import trace

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.sdk.resources import Resource

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

from opentelemetry.instrumentation.django import DjangoInstrumentor

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

resource = Resource.create({"service.name": "order-service"})

tracer_provider = TracerProvider(resource=resource)

tracer_provider.add_span_processor(

    BatchSpanProcessor(

        OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")

    )

)

trace.set_tracer_provider(tracer_provider)

DjangoInstrumentor().instrument()

from django.core.wsgi import get_wsgi_application

application = get_wsgi_application()

asgi.py (ASGI deployments with Daphne or Uvicorn)

For ASGI deployments, Django instrumentation wraps the ASGI application via the opentelemetry-instrumentation-asgi package, which is installed as a dependency of the Django instrumentation package. The setup is identical to WSGI:

import os

from opentelemetry import trace

from opentelemetry.sdk.trace import TracerProvider

from opentelemetry.sdk.trace.export import BatchSpanProcessor

from opentelemetry.sdk.resources import Resource

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

from opentelemetry.instrumentation.django import DjangoInstrumentor

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

resource = Resource.create({"service.name": "order-service"})

tracer_provider = TracerProvider(resource=resource)

tracer_provider.add_span_processor(

    BatchSpanProcessor(

        OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")

    )

)

trace.set_tracer_provider(tracer_provider)

DjangoInstrumentor().instrument()

from django.core.asgi import get_asgi_application

application = get_asgi_application()

BatchSpanProcessor buffers spans and exports them in batches. Do not use SimpleSpanProcessor in production. It exports each span synchronously on the request thread, which adds latency to every response.

Step 2B: Zero-Code Auto-Instrumentation

For deployments where you cannot modify application code, use the opentelemetry-instrument command. Install opentelemetry-distro to get the command:

pip install opentelemetry-distro==0.63b1

Run the bootstrap command to auto-detect and install instrumentation for packages in your environment:

opentelemetry-bootstrap -a install

Then start Django with environment variables:

OTEL_SERVICE_NAME=order-service \

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \

OTEL_PYTHON_DJANGO_EXCLUDED_URLS="health,readyz,metrics" \

opentelemetry-instrument python manage.py runserver

In Docker:

services:

  order-service:

    image: order-service:latest

    environment:

      OTEL_SERVICE_NAME: order-service

      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318

      OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf

      OTEL_PYTHON_DJANGO_EXCLUDED_URLS: "health,readyz,metrics"

    command: >

      opentelemetry-instrument

      gunicorn myproject.wsgi:application --bind 0.0.0.0:8000

Step 3: Instrument Database Queries

Django ORM queries are not captured by the Django instrumentation package alone. After installing the appropriate database driver instrumentation, call its instrumentor explicitly alongside DjangoInstrumentor:

from opentelemetry.instrumentation.django import DjangoInstrumentor

from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor

DjangoInstrumentor().instrument()

Psycopg2Instrumentor().instrument()

Each database query then produces a child span under the request span, with the SQL statement, database name, and duration as attributes.

Enabling sqlcommenter for database log correlation:

sqlcommenter appends trace context as SQL comments to every query, enabling correlation between database logs and application traces. Enable it on DjangoInstrumentor:

DjangoInstrumentor().instrument(is_sql_commentor_enabled=True)

A query like User.objects.all() then executes as:

SELECT * FROM auth_user /*traceparent='00-abc123-def456-01'*/

Step 4: Add Manual Spans for Business Logic

Auto-instrumentation captures the HTTP request and database lifecycle. Add manual spans for business logic inside your views:

from opentelemetry import trace

tracer = trace.get_tracer("order-service", "1.0.0")

def process_order(request):

    with tracer.start_as_current_span("order.validate") as span:

        span.set_attribute("order.customer_id", str(request.user.id))

        span.set_attribute("order.item_count", len(request.POST.getlist("items")))

        validate_order(request)

    with tracer.start_as_current_span("order.charge") as span:

        span.set_attribute("payment.provider", "stripe")

        try:

            result = charge_customer(request)

            span.set_attribute("payment.transaction_id", result.transaction_id)

        except PaymentError as e:

            span.record_exception(e)

            span.set_status(trace.StatusCode.ERROR, str(e))

            raise

    return result

Adding attributes to the current active span without creating a child span:

from opentelemetry import trace

def order_detail(request, order_id):

    trace.get_current_span().set_attribute("order.id", str(order_id))

    return render(request, "order_detail.html", {"order": get_order(order_id)})

Step 5: Correlate Logs with Traces

The OTel Python SDK can inject the active trace ID and span ID into Django’s log records automatically, enabling log-to-trace correlation in your backend.

import logging

from opentelemetry._logs import set_logger_provider

from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler

from opentelemetry.sdk._logs.export import BatchLogRecordProcessor

from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter

# Add to your SDK initialization block alongside TracerProvider setup

logger_provider = LoggerProvider(resource=resource)

logger_provider.add_log_record_processor(

    BatchLogRecordProcessor(

        OTLPLogExporter(endpoint="http://otel-collector:4318/v1/logs")

    )

)

set_logger_provider(logger_provider)

# Attach OTel handler to the root logger

handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)

logging.getLogger().addHandler(handler)

After this setup, every log record emitted during a traced request automatically carries the trace_id and span_id of the active span.

Excluding URLs from Tracing

Health check, readiness, and liveness endpoints should be excluded to prevent noise in your traces.

Via environment variable:

OTEL_PYTHON_DJANGO_EXCLUDED_URLS="health,readyz,livez,metrics"

Via code:

DjangoInstrumentor().instrument(excluded_urls="health,readyz,livez,metrics")

The value is a comma-separated list of regex patterns matched against the full request URL path. health matches /health, /healthz, and /api/health.

Configure the OTel Collector

receivers:

  otlp:

    protocols:

      grpc:

        endpoint: 0.0.0.0:4317

      http:

        endpoint: 0.0.0.0:4318

processors:

  batch:

    timeout: 5s

    send_batch_size: 512

exporters:

  otlp:

    endpoint: your-backend:4317

service:

  pipelines:

    traces:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

    metrics:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

    logs:

      receivers: [otlp]

      processors: [batch]

      exporters: [otlp]

Common Setup Problems

ProblemLikely causeFix
No spans appearing in the backendDjangoInstrumentor().instrument() called after Django starts handling requestsMove the call to manage.py or wsgi.py before get_wsgi_application()
Database queries have no spansDatabase driver instrumentation not installed or its instrumentor not calledInstall the matching package and call its instrumentor explicitly after DjangoInstrumentor().instrument()
opentelemetry-instrument command not foundopentelemetry-distro not installedRun pip install opentelemetry-distro==0.63b1
Health check endpoints flooding tracesNot excluded from instrumentationSet OTEL_PYTHON_DJANGO_EXCLUDED_URLS=health,readyz,livez
Spans appear but have no custom request attributesOTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS not configuredSet it to a comma-separated list of Django request attribute names, e.g. OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS=path_info,content_type
psycopg2 instrumentation install fails on Python 3.9opentelemetry-instrumentation-psycopg2==0.63b0 requires Python 3.10+Pin to opentelemetry-instrumentation-psycopg2==0.62b1 if running Python 3.9
Logs not correlated with tracesLoggingHandler not attached or attached after the first log callAttach the handler before Django’s logging system emits any records
Metrics not appearingPeriodicExportingMetricReader export interval not elapsedDefault export interval is 60 seconds. Data may not appear immediately after startup

From Traces to Root Cause: Where CubeAPM Fits

Once OpenTelemetry tracing is flowing from your Django application, the value it delivers depends on what your backend does with the data. A span showing a 900ms request is visible in any trace viewer. Understanding whether that 900ms came from a slow Django ORM query, a downstream API call, or a Redis cache miss requires a backend that correlates spans with infrastructure metrics and logs from the same request.

CubeAPM accepts OTLP directly from your existing Django instrumentation with no additional configuration. It correlates distributed request traces with database query spans, outgoing HTTP call durations, and infrastructure metrics using the shared context that OpenTelemetry provides. When a slow request trace appears, CubeAPM links it to the specific query or downstream call responsible. It runs self-hosted inside your own infrastructure at $0.15/GB ingestion with no per-user fees.

Summary

Instrumenting a Django application with OpenTelemetry takes three steps: installing the packages, initializing the SDK, and calling DjangoInstrumentor().instrument() before requests are served. Database query spans require separate driver instrumentation packages called alongside the Django instrumentor. Use BatchSpanProcessor in production, never SimpleSpanProcessor. Add manual spans inside view functions for business logic that auto-instrumentation cannot see.

StepWhat to doKey detail
Install packagesopentelemetry-api, opentelemetry-sdk, opentelemetry-instrumentation-django, database driver instrumentationSDK 1.42.1, Django instrumentation 0.62b1. Python 3.9 or above
Initialize SDKCreate TracerProvider, set globally before Django startsUse BatchSpanProcessor. Initialize in manage.py or wsgi.py
Instrument DjangoDjangoInstrumentor().instrument() after DJANGO_SETTINGS_MODULE is setCall database instrumentors immediately after
Export to CollectorHTTP exporter pointing at port 4318. gRPC exporter at port 4317HTTP/protobuf is the recommended default
Instrument databaseCall Psycopg2Instrumentor().instrument() or equivalentDatabase spans appear as children of the request span
Add manual spanstracer.start_as_current_span(“name”) inside view functionsCall span.record_exception() and set status on failure
Correlate logsAdd LoggingHandler to root loggerInjects trace ID and span ID into every log record
Exclude noisy URLsOTEL_PYTHON_DJANGO_EXCLUDED_URLS=health,readyz,livezComma-separated regex patterns matched against the URL path

Disclaimer : Package versions are verified directly from PyPI: opentelemetry-api 1.42.1 and opentelemetry-sdk 1.42.1 (May 21, 2026), opentelemetry-instrumentation-django 0.62b1 (April 24, 2026), opentelemetry-instrumentation-psycopg2 0.63b0 (May 19, 2026), opentelemetry-instrumentation-requests 0.63b0 (May 19, 2026), opentelemetry-distro 0.63b1 (May 21, 2026). Django version information verified from djangoproject.com/download (Django 6.0.5 latest, Django 5.2.x current LTS) as of May 2026.

Also read:

How to Add OpenTelemetry Tracing to a Spring Boot Application 

What is the Difference Between Synthetic monitoring and Real User Monitoring (RUM)?

What Are the Best Synthetic Monitoring Tools in 2026? 

×
×