trapy 0.1.0

Python logging facade that emits real Rust `tracing` events with structured kwargs.
docs.rs failed to build trapy-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

trapy

Python logging facade that emits real Rust tracing events with structured kwargs.

In a process that mixes Python and Rust (PyO3 cdylibs), trapy gives both sides one tracing pipeline: trapy.info("msg", cat="planning", x=1) produces an event indistinguishable from tracing::info!(cat = "planning", x = 1, "msg") to any subscriber — including category routing, level filtering, and per-field structured access. Caller file/line/module are pulled from the Python frame and woven into the event metadata (target = "py:<module>"), so subscribers report Python locations and can route by Python module.

Events

import trapy

trapy.info("seam planned", cat="planning", part="bracket", qs=[0.1, 0.2])
trapy.warn("ik fallback", attempts=3)
trapy.error("collision", link="wrist", depth_mm=1.7)

Each kwarg becomes a real tracing field. Primitives (bool/int/float/str/None) flow through natively; lists, tuples, dicts, dataclasses, and anything implementing __array__ (numpy arrays, tensors) flow through valuable as structured values. Anything else is stringified rather than raising.

Spans

with trapy.info_span("plan_part", part="bracket"):
    trapy.debug("inside the span")


@trapy.instrument("plan_path", level="debug", category="planning")
def plan(path): ...


timer = trapy.LocalTimer()
timer.epoch("load")  # closes the previous epoch span, opens the next
timer.epoch("solve")
timer.close_last("write")

Every span emits cat="timing" events on enter and exit (with elapsed_ms), so <dir>/timing.log doubles as a wall-clock record. For aggregate statistics install the timing layer (below) and call trapy.dump_timing().

Subscriber backend

The backend is compiled behind the subscribers cargo feature (the wheel enables it). Configure any number of layers, then install once:

trapy.add_file_router("/tmp/trace")  # tracing-catty: events route to <dir>/<cat>.log
trapy.add_env_filter("info,planning=trace")
trapy.add_fmt_layer("stderr", format="compact", span_events=["close"])
trapy.add_json_file("/tmp/trace/events.json")
trapy.add_timing_layer()  # tracing-timing HDR histograms
trapy.add_tracy_layer()  # stream to a Tracy profiler
trapy.add_opentelemetry_layer(service_name="composer")  # OTLP span export
trapy.init_subscriber()  # one-shot; add_* calls fail afterwards

Or the one-call convenience for the file-router-only setup: trapy.init_tracing(dir).

Available layers:

call layer
add_file_router(dir, ...) tracing-catty per-category file router. Defaults: cat="progress" mirrors to stdout, cat="console" is stdout-only, ERROR events fan out to errors.log; override via console_cats= / console_only_cats= / split_errors= / split_warnings=
add_fmt_layer(...) tracing_subscriber::fmt with the full builder surface: format= full/compact/pretty/json, stdout/stderr or file=, span_events=, thread/file/line knobs
add_env_filter(directive) / add_env_filter_from_env(var) EnvFilter with RUST_LOG syntax; rejected events never reach downstream layers
add_json_file(path) JSON-per-line file, for Loki/Promtail-style shipping
add_timing_layer(...) + dump_timing() tracing-timing inter-event HDR histograms keyed by (span, event)
add_tracy_layer() tracing-tracy; starts the Tracy client eagerly so the profiler can attach before the first span
add_opentelemetry_layer(endpoint, service_name) tracing-opentelemetry OTLP span export over HTTP + binary protobuf (no embedded tokio runtime)

Flushing: flush_tracing() (async), flush_tracing_sync() (blocks until the writer acks — call before process exit), flush_opentelemetry() / shutdown_opentelemetry() for the OTLP batch exporter.

Multi-cdylib forwarding

Every PyO3 cdylib statically links its own copy of tracing-core, so a subscriber installed in trapy._trapy.so is invisible to events emitted from another cdylib. trapy bridges this with a C-ABI capsule (trapy._capsule): the host cdylib links trapy as an rlib without default features and installs a forwarding subscriber at module init —

trapy = { version = "0.1.0", default-features = false }
#[pymodule]
fn _my_ext(m: &Bound<'_, PyModule>) -> PyResult<()> {
    let _ = trapy::import_trapy_subscriber(m.py()); // best-effort, never raises
    ...
}

Events from the host's tracing::*! macros then flow through trapy's full layer chain. The handshake is fail-safe: if trapy isn't importable, the host still loads and its events are dropped. set_log_level() / get_log_level() gate the FFI boundary coarsely so foreign cdylibs can drop events before paying for formatting; in-process filtering is EnvFilter's job.

Building

pip install trapy            # or: maturin develop --release

Cargo features:

feature meaning
subscribers the whole backend: layer builders, the file router, the capsule owner side. The wheel enables it via pyproject.toml
extension-module pyo3/extension-module, for standalone wheel builds only

Crates that consume trapy as an rlib (capsule forwarding) must enable neither — they stay consumer-only and skip compiling every subscriber crate.

The valuable field support rides tracing's unstable API: builds need --cfg tracing_unstable (the workspace .cargo/config.toml sets it).