Trace Django requests

KoloMiddleware records one trace per Django request. It is one of several ways to capture a trace: to trace a management command, a test run or a script in a Django project, use KOLO=1 or kolo run instead.

Set up the middleware

  1. Install Kolo using pip install kolo.

  2. Add "kolo.middleware.KoloMiddleware" to the top of your MIDDLEWARE list in settings.py:

    # settings.py
    
    MIDDLEWARE = [
        "kolo.middleware.KoloMiddleware",
        "django.middleware.security.SecurityMiddleware",
        ...
    ]
    
  3. Start your server with python manage.py runserver. Kolo confirms it is active:

    View recent requests at http://127.0.0.1:8000/_kolo/
    
  4. Make a request to any page.

Read the trace

Each request is saved as its own trace and emitted to .kolo/traces/. A request trace contains the request and response, the view and the functions in your project that it called, and the SQL queries and outbound HTTP requests made along the way.

kolo trace list                    # one line per request: status, method, path, trace ID
kolo cat                           # call tree of the most recent request
grep -rn "auth_user" .kolo/traces/ # find a query, function or value across recent requests

Kolo also serves a web UI for browsing traces at /_kolo/ on your development server, for example http://localhost:8000/_kolo/.

Which SQL is recorded

Kolo records the statements the Django ORM compiles and runs: queryset reads, save(), create(), update(), delete(), bulk_create() and raw(). A query that raises is recorded too. When Kolo traces with sys.monitoring (Python 3.12 and later) it is marked with the exception ([raised IntegrityError]).

Some statements do not go through the ORM’s execute_sql and are not recorded, so a trace can show fewer statements than connection.queries or assertNumQueries count:

  • On Django 6.0 and later, the UPDATE ... RETURNING that save() runs when a field is assigned an expression (obj.count = F("count") + 1).

  • Transaction control: SAVEPOINT, RELEASE SAVEPOINT, BEGIN, COMMIT and ROLLBACK (for example from transaction.atomic()).

  • SQL your code or a library runs directly on a cursor (connection.cursor().execute(...), executemany, callproc), including migrations and schema changes.

To see every statement, add Django’s cursor wrapper to include_frames. Each CursorWrapper.execute call then appears as a function call with its sql and params:

[filters]
include_frames = ["django/db/backends/utils"]

When the middleware is active

KoloMiddleware is for local development. By default it only records requests when Django’s DEBUG setting is True, and it only ever serves the /_kolo/ web UI when DEBUG is True. With DEBUG = False, requests to /_kolo/ fall through to your own URL configuration.

Setting the KOLO_DISABLE environment variable (to anything other than 0 or false) turns off both recording and the web UI. The middleware also stops recording while another profiler is active in the process.

If you would rather not have the middleware installed outside development at all, add it conditionally:

# settings.py

if DEBUG:
    MIDDLEWARE.insert(0, "kolo.middleware.KoloMiddleware")

To skip some URLs, set ignore_request_paths in the filters config.