| Crates.io | tracing-subscriber-reload-arcswap |
| lib.rs | tracing-subscriber-reload-arcswap |
| version | 0.1.1 |
| created_at | 2025-12-18 14:20:34.172711+00 |
| updated_at | 2025-12-18 14:30:53.099041+00 |
| description | Lock-free reload layer for tracing-subscriber using ArcSwap. |
| homepage | |
| repository | https://github.com/mikasd/tracing-subscriber-reload-arcswap |
| max_upload_size | |
| id | 1992485 |
| size | 52,305 |
This crate exists because the tracing-subscriber maintainers asked that an arc-swap-based
reload layer be split out into a separate crate rather than adding a new feature to
tracing-subscriber.
TL;DR: a functionally-equivalent alternative to
tracing_subscriber::reload::Layer
that is typically comparable and can be far faster under high OS-thread parallelism (e.g.
tokio::spawn_blocking, Rayon, or other thread pools) (in certain cases up to ~280x faster!);
see Benchmarks.
Context:
tracing-subscriber PR/discussion: https://github.com/tokio-rs/tracing/pull/3438tracing_subscriber_reload_arcswap::ArcSwapLayer is intended as a pragmatic replacement for
tracing_subscriber::reload::Layer.
It provides the same core behavior:
Layer or per-layer Filterreload/modify)The primary difference is implementation strategy:
tracing_subscriber::reload::Layer uses an RwLock (every span/event hits the lock on the read path)arc-swap for a lock-free read path (reload/modify are serialized; they’re expected to be rare)In practice, it is a drop-in replacement for reloadable filters and for layers that are Clone.
L: Clone caveatArcSwapLayer implements Layer only when L: Clone. This is because Layer::on_layer requires
mutable access, and with ArcSwap the safe way to update is to clone the current value, mutate it,
and swap it back in.
That clone happens only on reload/modify (when you actively change the layer), not on every
span/event. So the clone cost is not in the hot path, and it’s usually insignificant compared to
the benefit of removing the RwLock from the read path. For most use cases the cloned value is
small (filters or lightweight layers) and reloads are infrequent.
If cloning L is expensive or you expect frequent reloads, tracing_subscriber::reload::Layer may
be a better fit. If you only need reloadable filtering, prefer wrapping the filter itself rather
than a Filtered layer.
use tracing::info;
use tracing_subscriber::{filter, fmt, prelude::*};
use tracing_subscriber_reload_arcswap::ArcSwapLayer;
let (filter, handle) = ArcSwapLayer::new(filter::LevelFilter::WARN);
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer())
.init();
info!("this is ignored");
handle.reload(filter::LevelFilter::INFO).unwrap();
info!("this is logged");
For per-layer filtering, prefer wrapping the filter directly:
use tracing_subscriber::{filter, fmt, prelude::*};
use tracing_subscriber_reload_arcswap::ArcSwapLayer;
let (filter, handle) =
ArcSwapLayer::<_, tracing_subscriber::Registry>::new(filter::LevelFilter::WARN);
let layer = fmt::layer().with_filter(filter);
tracing_subscriber::registry().with(layer).init();
handle.reload(filter::LevelFilter::INFO).unwrap();
cargo bench
The multi-threaded benchmarks intentionally construct OS-thread parallelism (via std::thread,
tokio::spawn_blocking, and a Rayon pool) to exacerbate read-side synchronization contention. This
is not representative of typical Tokio async request-handling on a small number of runtime worker
threads.
On an Apple M4 Pro (14 cores, 48GB; macOS 26.2; rustc 1.92.0), Criterion point estimates for the
benchmarks that originally motivated this crate were:
| Benchmark | Baseline (no reload) | reload::Layer (RwLock) |
ArcSwapLayer (ArcSwap) |
ArcSwapLayer vs reload::Layer |
|---|---|---|---|---|
single_threaded |
4.88 ns | 8.90 ns (1.82x) | 9.58 ns (1.96x) | 0.93x (slower) |
multithreaded_16x1000 (std::thread) |
67.2 µs | 11.9 ms (177x) | 71.7 µs (1.07x) | 166x (faster) |
tokio_spawn_blocking_16x1000 |
57.1 µs | 12.8 ms (223x) | 62.8 µs (1.10x) | 204x (faster) |
rayon_16x1000 |
39.4 µs | 15.0 ms (380x) | 51.9 µs (1.32x) | 289x (faster) |
These results show why the crate exists:
ArcSwapLayer is in the same ballpark as reload::Layerspawn_blocking, Rayon, or other thread pools),
the RwLock read-side overhead can dominate even when you never reload — ArcSwapLayer avoids that contentiontracing-log: updates log's max-level after reload/modify.tokio-rs/tracing issue: https://github.com/tokio-rs/tracing/issues/2658Michael Ingley michael.ingley@gmail.com