Search code examples
rustthread-safetyrust-tracing

How to wrap std::io::Write in Mutex for tracing-subscriber?


In the documentation for Mutex, it says that it implements Send and Sync -- which makes sense, because a Mutex is designed to be accessed from multiple threads that are locking, using the resource it protects, then unlocking.

However, in my code below, I get a compiler error that, as far as I can tell, complains that the Mutex doesn't implement Send/Sync:

use std::fs::File;
use std::io::Write;
use std::sync::Mutex;

use dotenvy::var;
use std::sync::Arc;
use tracing::Level;

struct MultiWriter {
    writers: Vec<Arc<dyn Write>>,
}

impl Write for MultiWriter {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        for writer in self.writers.iter_mut() {
            writer.write(buf)?;
        }
        Ok(buf.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        for writer in self.writers.iter_mut() {
            writer.flush()?;
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut writers: Vec<Arc<dyn Write>> = vec![(Arc::new(std::io::stderr()))];
    if let Some(log_file) = var("log_file").ok() {
        writers.push(Arc::new(File::create(log_file).unwrap()));
    }
    let mw = Mutex::new(MultiWriter { writers });

    let tsb = tracing_subscriber::FmtSubscriber::builder()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).with_ansi(false)
        .with_writer(mw);

    if let Ok(log_level) = var("log_level") {
        match log_level.to_uppercase().as_str() {
            "TRACE" => tsb.with_max_level(Level::TRACE),
            "DEBUG" => tsb.with_max_level(Level::DEBUG),
            "INFO" => tsb.with_max_level(Level::INFO),
            "WARN" => tsb.with_max_level(Level::WARN),
            "ERROR" => tsb.with_max_level(Level::ERROR),
            _ => tsb.with_max_level(Level::INFO)
        }
        .try_init().expect("setting default subscriber failed");
    }   
}
error[E0599]: the method `try_init` exists for struct `SubscriberBuilder<DefaultFields, Format, tracing::level_filters::LevelFilter, std::sync::Mutex<MultiWriter>>`, but its trait bounds were not satisfied
   --> src/main.rs:131:10
    |
131 |           .try_init().expect("setting default subscriber failed");
    |            ^^^^^^^^ method cannot be called on `SubscriberBuilder<DefaultFields, Format, tracing::level_filters::LevelFilter, std::sync::Mutex<MultiWriter>>` due to unsatisfied trait bounds
    |
   ::: /Users/sean/.cargo/registry/src/github.com-1ecc6299db9ec823/tracing-subscriber-0.3.16/src/fmt/fmt_layer.rs:62:1
    |
62  | / pub struct Layer<
63  | |     S,
64  | |     N = format::DefaultFields,
65  | |     E = format::Format<format::Full>,
66  | |     W = fn() -> io::Stdout,
67  | | > {
    | | -
    | | |
    | |_doesn't satisfy `_: std::marker::Send`
    |   doesn't satisfy `_: std::marker::Sync`
    |
    = note: the following trait bounds were not satisfied:
            `tracing_subscriber::fmt::Layer<Registry, DefaultFields, Format, std::sync::Mutex<MultiWriter>>: std::marker::Send`
            `tracing_subscriber::fmt::Layer<Registry, DefaultFields, Format, std::sync::Mutex<MultiWriter>>: std::marker::Sync`

If I remove the line .with_writer(mw) from my code below, the error goes away. Clearly the problem is related to the writer, but I'm not sure how to do this correctly.

The goal of the code is to write the logs from the tracing framework to both stderr and a file specified from dotenvy if a file name is specified (it's optional).

NB: I'm using the latest stable Rust and the released version of each crate used below, and compiling with std, libc, alloc, etc. (full Rust, not embedded) on MacOS, but the code is expected to work on the "multi-platform x86(_64) desktop" environment (Windows/MacOS/desktop Linux).


Solution

  • In the documentation for Mutex, it says that it implements Send and Sync

    That's not completely true:

    impl<T: ?Sized + Send> Send for Mutex<T>
    impl<T: ?Sized + Send> Sync for Mutex<T>
    

    This means that a Mutex is Send and Sync only if T is Send (the reason for this is described in this question.

    However, T isn't Send here:

    • T is a struct MultiWriter
    • struct MultiWriter contains a dyn Write
    • dyn Write is not Send (at least not always)
    • in turn, struct MultiWriter isn't either.

    To fix this, replace dyn Write by dyn Write + Send, and it should work.