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}