Log vs tracing for a C library
⚓ Rust 📅 2026-04-05 👤 surdeus 👁️ 6I'm building a Rust SDK with three layers:
core-lib — core logic (crypto, HTTP, storage)
sdk-lib — public Rust API, builds on core-lib
c-bindings — C FFI layer, produces cdylib + staticlib
The library supports multiple concurrent instances in a single process. Each instance carries context that should appear on every log event: an instance ID and few other identifiers.
Why tracing over log
I need that context to propagate automatically into deep internal calls without threading it through every function signature. tracing spans solve this — one span per instance at the C boundary, all downstream events inherit the context. With log I'd repeat those fields manually everywhere.
core-lib and sdk-lib depend on tracing only. The c-bindings crate owns the subscriber.
Logging in my case is behind a feature flag. The library is not always compiled with it — only specific debug build enable it. In production builds, all tracing macros compile away to nothing.
Current approach
When the logging feature is enabled, I auto-initialize the subscriber inside the instance constructor (new) using OnceLock and try_init() — so the first instance creation triggers initialization, no explicit init call required from the user.
The tension
The general Rust guidance is: libraries use tracing (emit events), applications use tracing-subscriber (own the subscriber). My case sits in between — c-bindings is a library by Rust's definition, but for C, Python, and Java users it effectively is the application. There is no Rust runtime above it. Someone has to own the subscriber, and it has to be me.
For cdylib this works cleanly — bundled Rust runtime, isolated tracing global, no conflict possible.
For staticlib linked into a Rust application that also uses tracing-subscriber, the single shared binary means one tracing global. If the user initializes their subscriber before the first new call, try_init() silently fails and their subscriber receives our events — acceptable. If our new runs first, their subscriber setup fails — not acceptable.
Question
Given that logging is a debug-only, on-demand feature and the subscriber is initialized in the constructor rather than a separate init function:
-
Should I still use tracing instead of log?
-
Is there any pattern that allows the library to own the subscriber cleanly even in the staticlib case, or is the answer simply "document the constraint and move on"?
1 post - 1 participant
🏷️ Rust_feed