August 29, 2019·4 min read·engineeringpythonobservabilitysecurity

Python 3.8 beta adds runtime audit events

The last planned beta exposes hooks for named runtime events. Their ordering, exception behavior and event schemas define a new integration contract.

Runtime monitoring needs a defined point at which an operation reports what it is doing. Python 3.8's audit hooks introduce named events with argument schemas, giving consumers a documented interface for those notifications. The last planned beta makes the interface available before the release-candidate phase.

Python 3.8.0b4 was released on August 29, 2019. The announcement includes runtime audit hooks among the developing release's major changes and asks maintainers to report compatibility problems. It remains a preview. The relevant achievement is an implementation that integration authors can inspect and exercise, with no need to assume how the final release will behave.

The beta's sys documentation describes two Python functions. sys.addaudithook() registers a callable, and sys.audit() raises an event. The interface connects a named operation to a series of consumers without requiring the operation itself to know how each consumer will use the information.

I see value in this kind of explicit protocol. Python libraries are often most useful when they participate through a defined language or runtime interface. A stable event name and argument contract give an integration something more precise to depend on than incidental details of an implementation.

Events carry a schema

An audit event consists of a string name and associated arguments. The documentation treats the number and types of those arguments as a public, stable API. Consumers therefore need to interpret the event according to its schema, rather than treating every event as an arbitrary diagnostic string.

A small application-defined event illustrates the call convention.

import sys

def observe(event, args):
    if event == "example.export":
        destination, = args
        print("Export requested for", destination)

sys.addaudithook(observe)
sys.audit("example.export", "report.csv")

The hook receives the event name separately from the tuple of arguments. The sys.audit() call supplies one positional argument after the name, so the hook unpacks a one-element tuple. This example reports an application event. It does not claim that CPython supplies an event with this example-specific name.

The stable-schema requirement is useful because consumers can be written independently of the code that raises an event. Changing the order of arguments or their types would change the meaning of that contract. Human-readable log text can vary in ways that make it unsuitable for that role, while an event schema states what a consumer can expect.

The interface also leaves the destination of the information to the hook. Printing in an example shows that the hook ran, but an actual integration may record information elsewhere. That separation avoids requiring every audited operation to depend directly on a particular logging backend.

Hook ordering is part of the contract

The beta documentation specifies that native hooks registered with PySys_AddAuditHook run first. Python hooks registered in the current interpreter follow in the order in which they were added. This is a defined sequence, which matters whenever one hook can affect whether later work proceeds.

The distinction also separates interpreter-level integration from registration in Python code. A Python hook belongs to the current interpreter's collection. Native registration has its own API, and the documented ordering gives it precedence. Consumers should not assume that all hooks have the same registration context simply because they receive auditing events.

Registering a hook is itself an audited operation. Existing hooks receive an event when sys.addaudithook() is called. The beta documentation says an exception derived from Exception raised by an existing hook can cause the new registration to be silently ignored. A caller cannot assume registration succeeded unless it controls the existing hooks.

That behavior is especially relevant to a demonstration or plugin that installs its own observer. Reaching the next statement does not prove that the new callback has joined the collection. The source's stated limitation belongs in the integration's assumptions, because otherwise missing observations could be mistaken for missing runtime events.

Exceptions can stop an operation

The sys.audit() contract says it raises the first exception raised by a hook. It also recommends that those errors generally terminate the process promptly. Consequently, an audit call is not equivalent to a best-effort message that an operation can always ignore.

This gives a hook a way to make an event consequential. It also means that adding an observer can affect control flow if that observer fails. A hook's own processing is part of the execution path in which the event is raised. Its exception behavior should therefore be deliberate.

The registration case and the event case have different documented behavior. Failure while adding a hook can silently prevent registration under the specified condition. Failure while raising an event propagates the first hook exception. Conflating those cases would lead to an incorrect expectation about what the caller will observe.

I prefer an integration contract that spells out those differences. The useful question is which failure a caller can observe and at what point it occurs. The beta documentation supplies that information without requiring a consumer to infer it from a successful example.

Runtime coverage remains implementation-specific

The beta's audit-events documentation says its table derives from CPython's documentation. It explicitly cautions that another implementation may raise different events. The Python-facing registration functions do not, by themselves, establish identical event coverage across runtimes.

The last planned beta thus exposes both a mechanism and its boundaries. Consumers get named events with stable argument schemas, defined callback ordering and specified exception behavior. An integration can use those contracts while documenting which runtime's events it expects and which registration context it controls.