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.