Implementing a logger that can log from any thread

I have a program which is basically a game, i.e. it renders at 60fps and draws UI. I'd like log messages to be logged to my UI rather than to stdout.

I was thinking of using mpsc channels - the main thread keeps an mpsc::Receiver and prints anything from it to the UI, and I could have an mpsc::Sender which I could pass around with function calls, and clone it and send a copy whenever I spawn a new thread.

While this would work, I'd kind of like the convenience of using the log::Logger trait instead, rather than having to pass the logger around all over the place. I was thinking of something like implementing Logger for a struct MyLogger { rx: Option<mpsc::Receiver>, tx: mpsc::Sender }, which will be stored as a global static mut, and using thread_local! to get each thread lazily to clone the Sender and store it as a thread local static, which MyLogger::log can access to add a message to the queue.

How does this approach sound? Any advice?

You do not need a thread-local, and you especially do not need static mut.

(In general, never use static mut; ordinary statics combined with suitable interior mutability primitives are much safer. But in this case, you don't even need a static, since log has already got the static variable you need.)

Just put the channel in the log::Log implementation. Here's a simple demo:

use std::sync::mpsc;

struct MyLogger {
    sender: mpsc::Sender<String>,
}

impl log::Log for MyLogger {
    fn enabled(&self, _: &log::Metadata<'_>) -> bool {
        true
    }

    fn log(&self, record: &log::Record<'_>)  {
        self.sender.send(record.args().to_string()).unwrap();
    }

    fn flush(&self) {}
}

fn main() {
    let (sender, receiver) = mpsc::channel();
    
    log::set_boxed_logger(Box::new(MyLogger { sender })).unwrap();
    log::set_max_level(log::LevelFilter::Trace);
    
    std::thread::spawn(move || {
        while let Ok(msg) = receiver.recv() {
            eprintln!("[{msg}]");
        }
        eprintln!("[log sender dropped]");
    });
    
    log::info!("hello world");
    std::thread::sleep(std::time::Duration::from_secs(1));
}

This program will print "[hello world]" by way of the logger implementation.

5 Likes

Thanks. I was way overthinking it.

I assumed you needed mutable access to a Sender to send(), but of course they use interior mutability so no &mut required.

I believe there are some channel-sender-like things that do require &mut or aren't Sync (though I don't recall what it was I encountered once) — so my general advice would be to remember to check the properties of whatever you're considering using, and consider that there might be alternatives if it doesn't suit.