1use lockfree::channel::spsc::{create, Sender};
2use std::cell::RefCell;
3use std::fs::File;
4use std::io::Write;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7use std::thread::{self};
8
9#[derive(Clone)]
10pub enum LogTo {
11 Ephemeral,
12 File,
13}
14
15struct LogEntry {
16 closure: Box<dyn FnOnce() -> String + Send>,
17 log_to: LogTo,
18}
19
20pub struct Logger {
21 sx: RefCell<Sender<LogEntry>>,
22 file: Option<File>,
23 log_to: LogTo,
24 with_time: bool,
25 shutdown: Arc<AtomicBool>,
26}
27
28#[derive(Clone, Copy)]
29pub struct LoggerFileOptions {
30 pub path: &'static str,
31 pub append_mode: bool,
32}
33
34impl Logger {
35 pub fn builder(log_op: Option<LoggerFileOptions>) -> Self {
36 let (sx, mut rx) = create::<LogEntry>();
37
38 let shutdown_flag = Arc::new(AtomicBool::new(false));
39 let shutdown_flag_clone = shutdown_flag.clone();
40 thread::spawn(move || {
41 let mut file = None;
42 if let Some(op) = log_op {
43 file = Some(Logger::open_log_file(op));
44 }
45
46 loop {
47 match rx.recv() {
48 Err(_) => {
49 if shutdown_flag_clone.load(Ordering::Acquire) {
50 break;
51 }
52 }
53 Ok(entry) => {
54 let mut message = (entry.closure)();
55
56 match entry.log_to {
57 LogTo::File => {
58 message.push('\n');
59 let f = file.as_mut().unwrap();
60 f.write_all(message.as_bytes()).unwrap();
61 f.flush().unwrap();
62 }
63 LogTo::Ephemeral => println!("{}", message),
64 };
65 }
66 }
67 }
68 });
69
70 let file = log_op.map(Logger::open_log_file);
71
72 Logger {
73 sx: RefCell::new(sx),
74 file,
75 log_to: log_op.map_or(LogTo::Ephemeral, |_| LogTo::File),
76 with_time: false,
77 shutdown: shutdown_flag,
78 }
79 }
80
81 fn open_log_file(op: LoggerFileOptions) -> File {
82 File::options()
83 .write(true)
84 .append(op.append_mode)
85 .create(true)
86 .open(op.path)
87 .unwrap()
88 }
89
90 #[track_caller]
91 fn log<F, T>(&self, level: &'static str, f: F)
92 where
93 F: FnOnce() -> T + Send + 'static,
94 T: AsRef<str>,
95 {
96 let tt = self.with_time;
97 let location = std::panic::Location::caller();
98 let entry = LogEntry {
99 closure: Box::new(move || {
100 let file_line = format!("{}:{}", location.file(), location.line());
101 let time = match tt {
102 true => format!(
103 "{}",
104 chrono::offset::Local::now().format("%Y-%m-%d %H:%M:%S ")
105 ),
106 false => String::new(),
107 };
108 let message = f();
109 format!("{}{} {} {}", time, file_line, level, message.as_ref())
110 }),
111 log_to: self.log_to.clone(),
112 };
113
114 match self.sx.borrow_mut().send(entry) {
115 Ok(_) => (),
116 Err(_) => panic!("Logger thread died :("),
117 }
118 }
119
120 pub fn with_time(mut self, time: bool) -> Self {
121 self.with_time = time;
122 self
123 }
124
125 pub fn shutdown(&self) {
127 self.shutdown.store(true, Ordering::Release);
128 while self.sx.borrow().is_connected() {
129 thread::yield_now();
130 }
131
132 if let Some(ref file) = self.file {
133 file.sync_all().unwrap();
134 }
135 }
136
137 #[track_caller]
138 pub fn info<F, T>(&self, f: F)
139 where
140 F: FnOnce() -> T + Send + 'static,
141 T: AsRef<str>,
142 {
143 self.log("\x1b[32m[INFO]\x1b[0m", f);
144 }
145
146 #[track_caller]
147 pub fn error<F, T>(&self, f: F)
148 where
149 F: FnOnce() -> T + Send + 'static,
150 T: AsRef<str>,
151 {
152 self.log("\x1b[31m[ERROR]\x1b[0m", f);
153 }
154
155 #[track_caller]
156 pub fn debug<F, T>(&self, f: F)
157 where
158 F: FnOnce() -> T + Send + 'static,
159 T: AsRef<str>,
160 {
161 self.log("\x1b[36m[DEBUG]\x1b[0m", f);
162 }
163
164 #[track_caller]
165 pub fn warning<F, T>(&self, f: F)
166 where
167 F: FnOnce() -> T + Send + 'static,
168 T: AsRef<str>,
169 {
170 self.log("\x1b[33m[WARNING]\x1b[0m", f);
171 }
172}
173
174#[cfg(test)]
175mod tests {
176
177 use super::*;
178 use std::fs;
179
180 fn setup() {
181 fs::File::options()
182 .read(true)
183 .write(true)
184 .create(true)
185 .truncate(true)
186 .open("log.txt")
187 .unwrap();
188 }
189
190 fn teardown() {
191 fs::remove_file("log.txt").unwrap();
192 }
193
194 #[test]
195 fn run_test_sequentially() {
196 simple_to_file();
197 correct_ord();
198 tt();
199 }
200
201 fn tt() {
202 setup();
203 let logger = Logger::builder(None).with_time(true);
204 logger.info(String::new);
205 logger.info(|| String::from("hello"));
206 logger.debug(|| "foo");
207 let logger = logger.with_time(false);
208 logger.error(|| "bar");
209 logger.warning(|| "world");
210
211 logger.shutdown();
212 teardown();
213 }
214
215 fn simple_to_file() {
216 setup();
217 let o = LoggerFileOptions {
218 path: "log.txt",
219 append_mode: false,
220 };
221 let logger = Logger::builder(Some(o)).with_time(false);
222 logger.info(|| "to file".to_owned());
223 logger.shutdown();
224 let bytes = fs::read(o.path).unwrap();
225
226 teardown();
227
228 assert_eq!(
229 String::from_utf8(bytes).unwrap(),
230 "src/lib.rs:222 \u{1b}[32m[INFO]\u{1b}[0m to file\n".to_owned()
231 );
232 }
233
234 fn correct_ord() {
235 setup();
236 let o = LoggerFileOptions {
237 path: "log.txt",
238 append_mode: false,
239 };
240 let logger = Logger::builder(Some(o));
241 for i in 0..1000 {
242 logger.debug(move || format!("{}", i));
243 }
244
245 logger.shutdown();
246
247 for (i, line) in fs::read_to_string("log.txt").unwrap().lines().enumerate() {
248 assert_eq!(
249 line,
250 format!("src/lib.rs:242 \u{1b}[36m[DEBUG]\u{1b}[0m {}", i)
251 );
252 }
253 teardown();
254 }
255}