ring_log/
lib.rs

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    /// Waits until all messages are logged
126    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}