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.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.