sys.audit in CPython

October 27, 2024

Apart from sys.monitoring, PEP 578 proposes a trace mechanism named sys.audit in CPython, providing more flexible interfaces for users to invoke a callback. For instance, you can raise a audit event each time a function gets called to count its frequency:

import sys

count = 0


def fib_hook(event, args):
    global count
    if event == "fib.audit":
        count += 1
        print(f"event: {event}, args: {args}, count: {count}")


def fib(x: int) -> int:
    sys.audit("fib.audit")
    if x == 0 or x == 1:
        return 1

    return fib(x - 1) + fib(x - 2)


sys.addaudithook(fib_hook)

print(fib(4))

We can see fib is called 9 times when calculating fib(4).

In this post, we'll briefly go through the underlying implementation of sys.audit.

sys.addaudithook

It does nothing but appends the received hook function into a per-interpreter list, see the details in function sys_addaudithook_impl.

It should be mentioned that if the hook list has been allocated in the IS, it will trigger a event named sys.addaudithook first. If there is any previous raised error, the newly added hook will NOT be appended into the IS list.

sys.audit

Intuitively, sys.audit just iterates the IS hook list and use vector calling convention to invoke each iterated hook. You can trigger the audit via Python level interface sys.audit or C API PySys_Audit.

Note there are some system audit raised by some internal system routines:

Modules/posixmodule.c
static PyObject *
os_chdir_impl(PyObject *module, path_t *path)
/*[clinic end generated code: output=3be6400eee26eaae input=1a4a15b4d12cb15d]*/
{
    int result;

    if (PySys_Audit("os.chdir", "(O)", path->object) < 0) {
        return NULL;
    }
    /*
    ...
    */
}

We can see whenever the os.chdir gets called, it will raise a audit event with the path passed through. To check all the audit events, see the audit event table.