elastic / apm-agent-python

https://www.elastic.co/guide/en/apm/agent/python/current/index.html
BSD 3-Clause "New" or "Revised" License
408 stars 213 forks source link

Fix transaction tracing when running Django under ASGI #1953

Open jsma opened 7 months ago

jsma commented 7 months ago

(Moved from original discussion)

When running Django under ASGI, the middleware that Elastic APM injects is not tracking standard Django request transactions.

Here's my asgi.py:

django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
    }
)

My settings.ELASTIC_APM:

{'SERVICE_NAME': 'myapp',
 'SERVER_URL': 'http://fleet-server:8200',
 'SECRET_TOKEN': '*************',
 'ENVIRONMENT': 'development',
 'DEBUG': True}

I've browsed several pages and waited for Fleet to flush but all I see are Celery tasks:

Screenshot 2024-01-02 at 3 52 41 PM

Nothing is logged as a request transaction type by the standard Django middleware when running under Daphne.

If I switch the project back to standard WSGI/Django runserver, it logs request transactions:

image
MWedl commented 5 months ago

We encountered this bug after updating to Django 5.0. We also run django in ASGI mode. In Django 4.x, transaction tracing worked fine, even in ASGI mode.

benclark158 commented 3 months ago

I have encountered this as well in Django 5.0. When I call the following within my view class

import elasticapm
elasticapm.label(obj_id='id')

I get this message: Ignored labels obj_id. No transaction currently active. But when I downgrade to django 4.2.13 it works as expected

xrmx commented 3 months ago

Created a new django 5.0.6 project.

Added in settings.py:

ELASTIC_APM = {
    "SERVICE_NAME": "djangoasgi",
    "SERVER_URL": "",
    "API_KEY": "",
    "DEBUG": True,
}

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'elasticapm.contrib.django',
]

Added in views:

from django.http import HttpResponse
import elasticapm

async def hello(request):
    elasticapm.set_transaction_name("djangoasgi.hello")
    elasticapm.label(is_asgi=True)
    return HttpResponse("Hello")

In urls.py:

from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('hello', views.hello),
]

in asgi.py:

import os

from django.core.asgi import get_asgi_application

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

from elasticapm.contrib.asgi import ASGITracingMiddleware

application = ASGITracingMiddleware(get_asgi_application())

run it via gunicorn djangoasgi.asgi:application -w 1 -k uvicorn.workers.UvicornWorker

And got

Schermata del 2024-05-10 11-24-51

benclark158 commented 3 months ago

Thank you @xrmx doing that and extending your code fixed the issues I was having. I adapted your solution to use a custom middleware as I had a lot of URLs to monitor.

I have found that the config option TRANSACTION_IGNORE_URLS didn't work with your solution though. I did some customisation to get it working with the middleware and ASGI middleware:

middleware.py

import elasticapm
from elasticapm.contrib.django.middleware import TracingMiddleware

class CustomTracingMiddleware(TracingMiddleware):

    def process_request(self, request: HttpRequest):

        if elasticapm.get_transaction_id():
            transaction_name: str = "{} {}".format(request.method.upper(), request.path.lower()) #type: ignore
            elasticapm.set_transaction_name(transaction_name) #type: ignore

        return None

asgi.py

from elasticapm.contrib.asgi import ASGITracingMiddleware

class CustomASGITracingMiddleware(ASGITracingMiddleware):
    def get_url(self, scope, host: str | None = None) -> Tuple[str, dict[str, str]]:
        _, url_dict = super().get_url(scope, host)
        path = scope.get("root_path", "") + scope.get("path", "")
        return path, url_dict

application = CustomASGITracingMiddleware(get_asgi_application())

settings.py

MIDDLEWARE = [
    'ehs_api.middleware.CustomTracingMiddleware',
    # add others here
]

ELASTIC_APM = {
    'SERVICE_NAME': 'djangoapm',
    'SERVER_URL': '',
    'DEBUG': True, #allows it to run in test mode
    'TRANSACTION_IGNORE_URLS': [
        '/__debug__/*',
    ],
    'DJANGO_TRANSACTION_NAME_FROM_ROUTE': True,
    'DJANGO_AUTOINSERT_MIDDLEWARE': False
}