|
| 1 | +# Tracing |
| 2 | + |
| 3 | +Python projects that write extension modules for performance reasons may want to |
| 4 | +tap into [Rust's `tracing` ecosystem] to gain insight into the performance of |
| 5 | +their extension module. |
| 6 | + |
| 7 | +This section of the guide describes a few crates that provide ways to do that. |
| 8 | +They build on [`tracing_subscriber`][tracing-subscriber] and require code |
| 9 | +changes in both Python and Rust to integrate. Note that each extension module |
| 10 | +must configure its own `tracing` integration; one extension module will not see |
| 11 | +`tracing` data from a different module. |
| 12 | + |
| 13 | +## `pyo3-tracing-subscriber` ([documentation][pyo3-tracing-subscriber-docs]) |
| 14 | + |
| 15 | +[`pyo3-tracing-subscriber`][pyo3-tracing-subscriber] provides a way for Python |
| 16 | +projects to configure `tracing_subscriber`. It exposes a few |
| 17 | +`tracing_subscriber` layers: |
| 18 | +- `tracing_subscriber::fmt` for writing human-readable output to file or stdout |
| 19 | +- `opentelemetry-stdout` for writing OTLP output to file or stdout |
| 20 | +- `opentelemetry-otlp` for writing OTLP output to an OTLP endpoint |
| 21 | + |
| 22 | +The extension module must call [`pyo3_tracing_subscriber::add_submodule`][add-submodule] |
| 23 | +to export the Python classes needed to configure and initialize `tracing`. |
| 24 | + |
| 25 | +On the Python side, use the `Tracing` context manager to initialize tracing and |
| 26 | +run Rust code inside the context manager's block. `Tracing` takes a |
| 27 | +`GlobalTracingConfig` instance describing the layers to be used. |
| 28 | + |
| 29 | +See [the README on crates.io][pyo3-tracing-subscriber] |
| 30 | +for example code. |
| 31 | + |
| 32 | +## `pyo3-python-tracing-subscriber` ([documentation][pyo3-python-tracing-subscriber-docs]) |
| 33 | + |
| 34 | +The similarly-named [`pyo3-python-tracing-subscriber`][pyo3-python-tracing-subscriber] |
| 35 | +implements a shim in Rust that forwards `tracing` data to a `Layer` |
| 36 | +implementation defined in and passed in from Python. |
| 37 | + |
| 38 | +There are many ways an extension module could integrate `pyo3-python-tracing-subscriber` |
| 39 | +but a simple one may look something like this: |
| 40 | +```rust |
| 41 | +#[tracing::instrument] |
| 42 | +#[pyfunction] |
| 43 | +fn fibonacci(index: usize, use_memoized: bool) -> PyResult<usize> { |
| 44 | + // ... |
| 45 | +} |
| 46 | + |
| 47 | +#[pyfunction] |
| 48 | +pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { |
| 49 | + tracing_subscriber::registry() |
| 50 | + .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) |
| 51 | + .init(); |
| 52 | +} |
| 53 | +``` |
| 54 | +The extension module must provide some way for Python to pass in one or more |
| 55 | +Python objects that implement [the `Layer` interface]. Then it should construct |
| 56 | +[`pyo3_python_tracing_subscriber::PythonCallbackLayerBridge`][PythonCallbackLayerBridge] |
| 57 | +instances with each of those Python objects and initialize `tracing_subscriber` |
| 58 | +as shown above. |
| 59 | + |
| 60 | +The Python objects implement a modified version of the `Layer` interface: |
| 61 | +- `on_new_span()` may return some state that will stored inside the Rust span |
| 62 | +- other callbacks will be given that state as an additional positional argument |
| 63 | + |
| 64 | +A dummy `Layer` implementation may look like this: |
| 65 | +```python |
| 66 | +import rust_extension |
| 67 | + |
| 68 | +class MyPythonLayer: |
| 69 | + def __init__(self): |
| 70 | + pass |
| 71 | + |
| 72 | + # `on_new_span` can return some state |
| 73 | + def on_new_span(self, span_attrs: str, span_id: str) -> int: |
| 74 | + print(f"[on_new_span]: {span_attrs} | {span_id}") |
| 75 | + return random.randint(1, 1000) |
| 76 | + |
| 77 | + # The state from `on_new_span` is passed back into other trait methods |
| 78 | + def on_event(self, event: str, state: int): |
| 79 | + print(f"[on_event]: {event} | {state}") |
| 80 | + |
| 81 | + def on_close(self, span_id: str, state: int): |
| 82 | + print(f"[on_close]: {span_id} | {state}") |
| 83 | + |
| 84 | + def on_record(self, span_id: str, values: str, state: int): |
| 85 | + print(f"[on_record]: {span_id} | {values} | {state}") |
| 86 | + |
| 87 | +def main(): |
| 88 | + rust_extension.initialize_tracing(MyPythonLayer()) |
| 89 | + |
| 90 | + print("10th fibonacci number: ", rust_extension.fibonacci(10, True)) |
| 91 | +``` |
| 92 | + |
| 93 | +`pyo3-python-tracing-subscriber` has [working examples] |
| 94 | +showing both the Rust side and the Python side of an integration. |
| 95 | + |
| 96 | +[pyo3-tracing-subscriber]: https://crates.io/crates/pyo3-tracing-subscriber |
| 97 | +[pyo3-tracing-subscriber-docs]: https://docs.rs/pyo3-tracing-subscriber |
| 98 | +[add-submodule]: https://docs.rs/pyo3-tracing-subscriber/*/pyo3_tracing_subscriber/fn.add_submodule.html |
| 99 | + |
| 100 | +[pyo3-python-tracing-subscriber]: https://crates.io/crates/pyo3-python-tracing-subscriber |
| 101 | +[pyo3-python-tracing-subscriber-docs]: https://docs.rs/pyo3-python-tracing-subscriber |
| 102 | +[PythonCallbackLayerBridge]: https://docs.rs/pyo3-python-tracing-subscriber/*/pyo3_python_tracing_subscriber/struct.PythonCallbackLayerBridge.html |
| 103 | +[working examples]: https://github.com/getsentry/pyo3-python-tracing-subscriber/tree/main/demo |
| 104 | + |
| 105 | +[Rust's `tracing` ecosystem]: https://crates.io/crates/tracing |
| 106 | +[tracing-subscriber]: https://docs.rs/tracing-subscriber/*/tracing_subscriber/ |
| 107 | +[the `Layer` interface]: https://docs.rs/tracing-subscriber/*/tracing_subscriber/layer/trait.Layer.html |
0 commit comments