Trace a function or script

Kolo traces any Python code. There are three ways to turn it on for a script, a test run or a single function. For per-request traces in Django see Trace Django requests, and for a process that is already running see Attach to a running process.

KOLO=1

Set the KOLO environment variable to 1 and every Python process that starts with it is traced from start to finish:

KOLO=1 python example.py
KOLO=1 python -m mymodule
KOLO=1 pytest tests/test_checkout.py

There is nothing to import or configure: pip install kolo installs a .pth file that activates Kolo at interpreter startup when KOLO is 1 or true.

When the process exits, Kolo saves the trace and prints the path to .kolo/kolo.txt:

Trace captured using KOLO=1
/path/to/project/.kolo/kolo.txt

The text files in .kolo/traces/ and kolo.txt are written by a background process, so they can appear a moment after the program exits. kolo cat works straight away.

Each process gets its own trace, so a program that starts Python subprocesses produces one trace per process. Threads are recorded in the same trace as the process that started them.

KOLO=1 records one trace for the whole process. For a long-running process such as a worker or server, use trace points to save a separate trace each time a chosen function returns (Python 3.12 or newer).

Set KOLO_DISABLE=1 to turn tracing off without unsetting KOLO.

kolo run

kolo run traces a single command without setting an environment variable:

kolo run python example.py
kolo run example.py
kolo run python -m mymodule

Pass --inline to print the call tree to stderr when the program exits, and --returns to include arguments and return values in it.

Unlike KOLO=1, kolo run does not trace Python subprocesses.

Tests

For tests, pass --one-trace-per-test to get one small trace per test instead of one large trace for the whole run:

kolo run --one-trace-per-test pytest
kolo run --one-trace-per-test python manage.py test
kolo run --one-trace-per-test unittest

kolo.enable

To trace one block of code, use kolo.enable as a context manager:

import kolo


def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


if __name__ == "__main__":
    with kolo.enable():
        print(fibonacci(6))

Or as a decorator, to trace a function every time it is called:

import kolo


@kolo.enable
def checkout(cart):
    total = sum(item.price for item in cart)
    return charge(total)

@kolo.enable also works on async def functions and on generator and async generator functions: a coroutine is traced until it returns, and a generator from its first item until it is exhausted, closed or garbage-collected, which includes the code that runs between items.

Kolo records function calls and returns, so a with kolo.enable(): block records the functions called inside the block.

Note

kolo.enable can be nested. A nested kolo.enable joins the trace that is already running: the outermost one produces the trace, and the config and name of nested ones are ignored.

Pass name to label the trace in kolo trace list, and a config dictionary to override config for this trace:

config = {
    "filters": {
        "include_frames": ["django/db"],
        "ignore_frames": ["/migrations/"],
    },
}

with kolo.enable(config, name="checkout"):
    ...


@kolo.enable(config=config)
def example(): ...

Read the trace

All three write to the same place. Grep .kolo/traces/, read .kolo/kolo.txt, or run:

kolo cat                         # call tree of the most recent trace
kolo cat --returns               # the same, with arguments and return values
kolo trace list                  # stored traces, newest first
kolo trace node TRACE_ID INDEX   # full details of one call, including locals

See the CLI reference for every command.