How to use thread local storage for tracing events?

1k Views Asked by At

This code uses tracing events:

# Cargo.toml
[dependencies]
tracing = "0.1.3"
tracing-subscriber = { version = "0.2.9", features = ["chrono", "env-filter", "fmt"] }
tracing-appender = "0.1.1"
use tracing::{Level, event, };
use tracing::dispatcher::{with_default, Dispatch};
use std::thread;
use tracing_appender::rolling::{RollingFileAppender};
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::fmt::SubscriberBuilder;

pub static file_appender:RollingFileAppender = tracing_appender::rolling::never("/ws/sarvi-sjc/", "prefix.log");
pub static (non_blocking, _guard:WorkerGuard):(NonBlocking:WorkerGuard) = tracing_appender::non_blocking(file_appender);
pub static subscriber:SubscriberBuilder = tracing_subscriber::FmtSubscriber::builder()
    .with_max_level(Level::TRACE)
    .with_writer(non_blocking)
    .finish();
pub static my_dispatch = Dispatch::new(subscriber);

with_default(&my_dispatch, || {
    event!(Level::INFO, "chmod(...)");
});

I want the first global static lines to be initialized and stored in a thread_local!() so that it is initialized only once for each thread.

I should then be able to use that thread-specific Dispatch instance to scope event subscribers. The above code is taken from inside a function and were let statements. As static variables, one them doesn't work, and had the same problem within thread_local!() as well.

pub static (non_blocking, _guard:WorkerGuard):(NonBlocking:WorkerGuard) = tracing_appender::non_blocking(file_appender);
error: expected identifier, found `(`
  --> src/lib.rs:13:12
   |
13 | pub static (non_blocking, _guard:WorkerGuard):(NonBlocking:WorkerGuard) = tracing_appender::non_blocking(file_appender);
   |            ^ expected identifier

The second problem was understanding how they are initialized in a thread local fashion.

1

There are 1 best solutions below

2
On BEST ANSWER

Wrap your static declarations with the thread_local! macro, then you can access each value using the with method, which will return a value unique to the thread.

use tracing::{
    dispatcher::{with_default, Dispatch},
    event, Level,
};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::FmtSubscriber;

fn make_dispatch() -> (Dispatch, WorkerGuard) {
    let file_appender = tracing_appender::rolling::never("ws/sarvi-sjc/", "prefix.log");
    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
    let subscriber = FmtSubscriber::builder()
        .with_max_level(Level::TRACE)
        .with_writer(non_blocking)
        .finish();
    (Dispatch::new(subscriber), guard)
}

thread_local!(static MY_DISPATCH: (Dispatch, WorkerGuard) = make_dispatch());

fn main() {
    // Main thread:
    let (my_dispatch, _guard) = make_dispatch();
    with_default(&my_dispatch, || {
        event!(Level::INFO, "main thread");
    });

    // Other thread:
    std::thread::spawn(|| {
        MY_DISPATCH.with(|(my_dispatch, _guard)| {
            with_default(&my_dispatch, || {
                event!(Level::INFO, "other thread");
            });
        });
    })
    .join()
    .unwrap();
}

I made sure to store the WorkerGuard in thread local storage too, so that it does not go out of scope after MY_DISPATCH is initialized. This is because the documentation for tracing_appender::non_blocking states:

This function returns a tuple of NonBlocking and WorkerGuard. NonBlocking implements MakeWriter which integrates with tracing_subscriber. WorkerGuard is a drop guard that is responsible for flushing any remaining logs when the program terminates.

Note that the WorkerGuard returned by non_blocking must be assigned to a binding that is not _, as _ will result in the WorkerGuard being dropped immediately. Unintentional drops of WorkerGuard remove the guarantee that logs will be flushed during a program's termination, in a panic or otherwise.

This way, the guard will be dropped when the thread exits. However, keep in mind that Rust's built-in thread local storage has some weird quirks about initialization and destruction. See the documentation for std::thread::LocalKey. Notably:

On Unix systems when pthread-based TLS is being used, destructors will not be run for TLS values on the main thread when it exits. Note that the application will exit immediately after the main thread exits as well.

Therefore, on the main thread, you should call make_dispatch() directly rather than MY_DISPATCH, so that it is dropped when the program exits (but note that in general, things are not guaranteed to be dropped, especially during a panic or std::process::exit, etc.; therefore, there is still a chance that some logs could be lost, as is the nature of most non-blocking I/O).