Exceptions
opentelemetry-instrumentation-django records unhandled exceptions automatically. When a view raises and Django doesn't catch it before the response is sent, the middleware calls span.record_exception(exc) and span.set_status(StatusCode.ERROR) on the request span. So:
- Any exception thrown from a view function / class-based view is captured on the request span. Django returns a 500, the endpoint row in TracePath shows status
500, and the error appears under Issues with the full Python traceback. - Any exception thrown from a Celery task is captured on the consumer span by
opentelemetry-instrumentation-celery(see Tasks). - Any exception thrown inside a span you created with
tracer.start_as_current_span(...)is captured by the context manager's__exit__whenset_status_on_exception=True(the default). - Any exception raised inside a
requests/httpx/ DB driver call is captured by that library's instrumentation on its own client span.
You only need the patterns on this page when:
- You catch an exception and don't re-raise it, so the middleware never sees it.
- You want to attach extra context (user id, request id, business attributes) to the auto-captured exception event.
- You want a custom 500 handler to still report what failed.
Recording an Exception on the Current Span
When you catch an error but want it reported to TracePath, record it as an event on the active span:
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
def checkout(request):
span = trace.get_current_span()
try:
payment_gateway.charge(request.user, request.POST["amount"])
except Exception as exc:
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR, str(exc)))
raiserecord_exception adds an exception event to the span with the type, message, and traceback. TracePath extracts these events and creates Issues from them.
Adding Attributes to Exceptions
Add context by setting span attributes before or after recording the exception:
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
def process_order(request, order_id):
span = trace.get_current_span()
span.set_attribute("user.id", request.user.pk)
span.set_attribute("order.id", order_id)
try:
_process_order(order_id)
except Exception as exc:
span.record_exception(exc, attributes={
"order.status": "failed",
"retry.count": int(request.headers.get("x-retry", 0)),
})
span.set_status(Status(StatusCode.ERROR, str(exc)))
raiseThe attributes={...} payload passed to record_exception is attached to the exception event itself, not the span.
Adding Context to the Auto-Capture (Middleware)
To attach request-level diagnostic fields to every span, including spans that ended with an exception event, set them from your own middleware. Your middleware runs inside the OTel span, so trace.get_current_span() is the request span:
# myproject/tracepath_context.py
from opentelemetry import trace
class TracePathContextMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
span = trace.get_current_span()
if hasattr(request, "user") and request.user.is_authenticated:
span.set_attribute("enduser.id", request.user.pk)
rid = request.headers.get("X-Request-Id")
if rid:
span.set_attribute("request.id", rid)
return responseAdd "myproject.tracepath_context.TracePathContextMiddleware" to MIDDLEWARE in settings.py, the same way you added the route middleware in the Quick Start. You can put both jobs in one middleware if you prefer.
What about the instrumentation's
response_hook? It works, but only if you uninstrument Django first. Underopentelemetry-instrumentthe agent has already instrumented Django, and a second.instrument(response_hook=...)is a no-op: the hook never fires. All you get is one easily missed startup line on stderr,WARNING ... Attempting to instrument while already instrumented. See Request & Response Hooks for the working version. A middleware needs none of that.
Capturing Exceptions in Services
For exceptions in services that run outside a request (or are several call frames away from one), get the current span from context. It will be whatever span is active, including the Celery consumer span or a custom span you created:
# myapp/services/payments.py
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
class PaymentService:
def process_payment(self, customer_id: str, amount: float) -> None:
span = trace.get_current_span()
try:
self.gateway.charge(customer_id, amount)
except Exception as exc:
span.record_exception(exc, attributes={
"customer.id": customer_id,
"payment.amount": amount,
})
span.set_status(Status(StatusCode.ERROR, str(exc)))
raiseIf no span is currently active (you're in a management command without a wrapping span, for example), trace.get_current_span() returns a no-op span, so record_exception is silently dropped. Wrap the entry point in a CONSUMER-kind span first; see Spans and Tasks.
Custom Exception Types
Custom exception classes work the same way. Their type name appears in the TracePath dashboard:
class InsufficientFundsError(RuntimeError):
def __init__(self, account_id: str, requested: float, available: float) -> None:
super().__init__(f"Insufficient funds: requested {requested}, available {available}")
self.account_id = account_id
self.requested = requested
self.available = available
def withdraw(request):
span = trace.get_current_span()
try:
_withdraw(request)
except InsufficientFundsError as exc:
span.record_exception(exc, attributes={
"account.id": exc.account_id,
"amount.requested": exc.requested,
"amount.available": exc.available,
})
span.set_status(Status(StatusCode.ERROR, str(exc)))
raiseCustom 500 Handler
If you've configured a handler500 in urls.py, exceptions are already handled by Django's BaseHandler before your handler runs. The middleware has caught the exception, recorded it, and set the span status. Your handler500 just renders the response. No extra instrumentation work is needed.
If you replace the default exception handling with custom middleware that swallows exceptions, record them on the span manually before returning a response:
# myapp/middleware.py
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
class SwallowExceptionsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
return self.get_response(request)
except Exception as exc:
span = trace.get_current_span()
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR, str(exc)))
from django.http import JsonResponse
return JsonResponse({"error": "internal"}, status=500)This keeps the Issue in TracePath even though Django's response is a clean 500.