file_per_thread_logger/
lib.rs

1use std::cell::{RefCell, RefMut};
2use std::env;
3use std::fs::File;
4use std::io::{self, Write};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::thread;
7
8use env_logger::filter::{Builder, Filter};
9use log::{LevelFilter, Metadata, Record};
10
11thread_local! {
12    static WRITER: RefCell<Option<io::BufWriter<File>>> = RefCell::new(None);
13}
14
15static ALLOW_UNINITIALIZED: AtomicBool = AtomicBool::new(false);
16
17/// Helper struct that can help retrieve a writer, from within a custom format function.
18///
19/// Use `GetWriter::get()` to retrieve an instance of the writer.
20pub struct GetWriter<'a> {
21    rc: &'a RefCell<Option<io::BufWriter<File>>>,
22}
23
24impl<'a> GetWriter<'a> {
25    /// Retrieves a mutable reference to the underlying buffer writer.
26    pub fn get(&self) -> RefMut<'a, io::BufWriter<File>> {
27        RefMut::map(self.rc.borrow_mut(), |maybe_buf_writer| {
28            maybe_buf_writer
29                .as_mut()
30                .expect("call the logger's initialize() function first")
31        })
32    }
33}
34
35/// Format function to print logs in a custom format.
36///
37/// Note: to allow for reentrant log invocations, `record.args()` must be reified before the writer
38/// has been taken with the `GetWriter` instance, otherwise double borrows runtime panics may
39/// occur.
40pub type FormatFn = fn(&GetWriter, &Record) -> io::Result<()>;
41
42/// Initializes the current process/thread with a logger, parsing the RUST_LOG environment
43/// variables to set the logging level filter and/or directives to set a filter by module name,
44/// following the usual env_logger conventions.
45///
46/// Must be called on every running thread, or else logging will panic the first time it's used.
47/// ```
48/// use file_per_thread_logger::initialize;
49///
50/// initialize("log-file-prefix");
51/// ```
52pub fn initialize(filename_prefix: &str) {
53    init_logging(filename_prefix, None)
54}
55
56/// Initializes the current process/thread with a logger, parsing the RUST_LOG environment
57/// variables to set the logging level filter and/or directives to set a filter by module name,
58/// following the usual env_logger conventions. The format function specifies the format in which
59/// the logs will be printed.
60///
61/// To allow for recursive log invocations (a log happening in an argument to log), the format
62/// function must take care of reifying the record's argument *before* taking the reference to the
63/// writer, at the risk of causing double-borrows runtime panics otherwise.
64///
65/// Must be called on every running thread, or else logging will panic the first time it's used.
66/// ```
67/// use file_per_thread_logger::{initialize_with_formatter, FormatFn};
68/// use std::io::Write;
69///
70/// let formatter: FormatFn = |writer, record| {
71///     // Reify arguments first, to allow for recursive log invocations.
72///     let args = format!("{}", record.args());
73///     writeln!(
74///         writer,
75///         "{} [{}:{}] {}",
76///         record.level(),
77///         record.file().unwrap_or_default(),
78///         record.line().unwrap_or_default(),
79///         args,
80///     )
81/// };
82/// initialize_with_formatter("log-file-prefix", formatter);
83/// ```
84pub fn initialize_with_formatter(filename_prefix: &str, formatter: FormatFn) {
85    init_logging(filename_prefix, Some(formatter))
86}
87
88/// Allow logs files to be created from threads in which the logger is specifically uninitialized.
89/// It can be useful when you don't have control on threads spawned by a dependency, for instance.
90///
91/// Should be called before calling code that spawns the new threads.
92pub fn allow_uninitialized() {
93    ALLOW_UNINITIALIZED.store(true, Ordering::Relaxed);
94}
95
96fn init_logging(filename_prefix: &str, formatter: Option<FormatFn>) {
97    let env_var = env::var_os("RUST_LOG");
98    if env_var.is_none() {
99        return;
100    }
101
102    let level_filter = {
103        let mut builder = Builder::new();
104        builder.parse(env_var.unwrap().to_str().unwrap());
105        builder.build()
106    };
107
108    // Ensure the thread local state is always properly initialized.
109    WRITER.with(|rc| {
110        if rc.borrow().is_none() {
111            rc.replace(Some(open_file(filename_prefix)));
112        }
113    });
114
115    let logger = FilePerThreadLogger::new(level_filter, formatter);
116    let _ =
117        log::set_boxed_logger(Box::new(logger)).map(|()| log::set_max_level(LevelFilter::max()));
118
119    log::info!("Set up logging; filename prefix is {}", filename_prefix);
120}
121
122struct FilePerThreadLogger {
123    filter: Filter,
124    formatter: Option<FormatFn>,
125}
126
127impl FilePerThreadLogger {
128    pub fn new(filter: Filter, formatter: Option<FormatFn>) -> Self {
129        FilePerThreadLogger { filter, formatter }
130    }
131}
132
133impl log::Log for FilePerThreadLogger {
134    fn enabled(&self, metadata: &Metadata) -> bool {
135        self.filter.enabled(metadata)
136    }
137
138    fn log(&self, record: &Record) {
139        if !self.enabled(record.metadata()) {
140            return;
141        }
142
143        WRITER.with(|rc| {
144            if ALLOW_UNINITIALIZED.load(Ordering::Relaxed) {
145                // Initialize the logger with a default value, if it's not done yet.
146                let mut rc = rc.borrow_mut();
147                if rc.is_none() {
148                    *rc = Some(open_file(""));
149                }
150            }
151
152            if let Some(ref format_fn) = self.formatter {
153                let get_writer = GetWriter { rc };
154                let _ = format_fn(&get_writer, record);
155            } else {
156                // A note: we reify the argument first, before taking a hold on the mutable
157                // refcell, in case reifing args will cause a reentrant log invocation. Otherwise,
158                // we'd end up with a double borrow of the refcell.
159                let args = format!("{}", record.args());
160
161                let mut opt_writer = rc.borrow_mut();
162                let writer = opt_writer
163                    .as_mut()
164                    .expect("call the logger's initialize() function first");
165
166                let _ = writeln!(*writer, "{} - {}", record.level(), args);
167            }
168        })
169    }
170
171    fn flush(&self) {
172        WRITER.with(|rc| {
173            let mut opt_writer = rc.borrow_mut();
174            let writer = opt_writer
175                .as_mut()
176                .expect("call the logger's initialize() function first");
177            let _ = writer.flush();
178        });
179    }
180}
181
182/// Open the tracing file for the current thread.
183fn open_file(filename_prefix: &str) -> io::BufWriter<File> {
184    let curthread = thread::current();
185    let tmpstr;
186    let mut path = filename_prefix.to_owned();
187    path.extend(
188        match curthread.name() {
189            Some(name) => name.chars(),
190            // The thread is unnamed, so use the thread ID instead.
191            None => {
192                tmpstr = format!("{:?}", curthread.id());
193                tmpstr.chars()
194            }
195        }
196        .filter(|ch| ch.is_alphanumeric() || *ch == '-' || *ch == '_'),
197    );
198    let file = File::create(path).expect("Can't open tracing file");
199    io::BufWriter::new(file)
200}