logflume/
lib.rs

1extern crate core;
2
3use chrono::{Local, Utc};
4use core_affinity::CoreId;
5use std::fmt::{self};
6use std::fs::File;
7use std::io::{BufWriter, Write};
8use std::thread;
9use std::time::Duration;
10
11pub mod __private_api;
12pub mod macros;
13
14static mut LOGGER: Option<&Logger> = None;
15
16pub enum Level {
17    Debug,
18    Info,
19    Warn,
20    Error,
21}
22
23impl fmt::Display for Level {
24    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25        match *self {
26            Level::Debug => write!(f, "DEBUG"),
27            Level::Info => write!(f, "INFO"),
28            Level::Warn => write!(f, "WARN"),
29            Level::Error => write!(f, "ERROR"),
30        }
31    }
32}
33
34struct LogMetaData {
35    level: Level,
36    func: LoggingFunc,
37}
38
39#[derive(Debug)]
40pub enum LoggerError {
41    InitialisationError,
42}
43
44impl std::error::Error for LoggerError {}
45
46impl fmt::Display for LoggerError {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        match self {
49            LoggerError::InitialisationError => write!(f, "Error during initialisation"),
50        }
51    }
52}
53
54enum LogCommand {
55    Msg(LogMetaData),
56    Flush(crossbeam_channel::Sender<()>),
57}
58
59pub struct LoggingFunc {
60    data: Box<dyn Fn() -> String + Send + 'static>,
61}
62
63impl LoggingFunc {
64    #[allow(dead_code)]
65    pub fn new<T>(data: T) -> LoggingFunc
66    where
67        T: Fn() -> String + Send + 'static,
68    {
69        LoggingFunc {
70            data: Box::new(data),
71        }
72    }
73    fn invoke(self) -> String {
74        (self.data)()
75    }
76}
77
78impl fmt::Debug for LoggingFunc {
79    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        Ok(())
81    }
82}
83
84pub struct Logger {
85    cpu: Option<usize>,
86    buffer_size: usize,
87    file_path: Option<String>,
88    filter_level: Level,
89    utc_time: bool,
90    sleep_duration_millis: u64,
91    thread_name: String,
92    sender: Option<crossbeam_channel::Sender<LogCommand>>,
93}
94
95impl Logger {
96    pub fn new() -> Logger {
97        Logger {
98            cpu: None,
99            buffer_size: 0,
100            file_path: None,
101            filter_level: Level::Info,
102            utc_time: false,
103            sleep_duration_millis: 100,
104            thread_name: String::from("logflume - Rust logging library"),
105            sender: None,
106        }
107    }
108
109    pub fn level(mut self, filter: Level) -> Logger {
110        self.filter_level = filter;
111        self
112    }
113
114    pub fn cpu(mut self, cpu: usize) -> Logger {
115        self.cpu = Some(cpu);
116        self
117    }
118
119    pub fn buffer_size(mut self, buf_size: usize) -> Logger {
120        self.buffer_size = buf_size;
121        self
122    }
123
124    pub fn file(mut self, file: &str) -> Logger {
125        self.file_path = Some(file.to_string());
126        self
127    }
128
129    pub fn utc_time(mut self, b: bool) -> Logger {
130        self.utc_time = b;
131        self
132    }
133    
134    /// The logger will sleep if there are no messages to consume from the queue.
135    pub fn sleep_duration_millis(mut self, millis: u64) -> Logger {
136        self.sleep_duration_millis = millis;
137        self
138    }
139    
140    pub fn thread_name(mut self, name: &str) -> Logger {
141        self.thread_name = name.to_string();
142        self
143    }
144
145    pub fn init(mut self) -> Result<(), LoggerError> {
146        let (tx, rx) = match self.buffer_size {
147            0 => crossbeam_channel::unbounded(),
148            _ => crossbeam_channel::bounded(self.buffer_size),
149        };
150
151        self.sender = Some(tx);
152        let file_path = self.file_path.clone();
153        let file =
154            File::create(file_path.unwrap()).map_err(|_| LoggerError::InitialisationError)?;
155        let mut buffered_writer = BufWriter::new(file);
156        let time_func = if self.utc_time {
157            get_utc_time
158        } else {
159            get_local_time
160        };
161
162        let _a = thread::Builder::new().name(self.thread_name.to_string()).spawn(move || {
163            if let Some(core) = self.cpu {
164                core_affinity::set_for_current(CoreId { id: core });
165            }
166            loop {
167                match rx.try_recv() {
168                    Ok(cmd) => {
169                        Self::process_log_command(&mut buffered_writer, cmd, time_func);
170                    }
171                    Err(e) => match e {
172                        crossbeam_channel::TryRecvError::Empty => {
173                            let _ = buffered_writer.flush();
174                            thread::sleep(Duration::from_millis(self.sleep_duration_millis));
175                        }
176                        crossbeam_channel::TryRecvError::Disconnected => {
177                            let _ = buffered_writer
178                                .write_all("Logging channel disconnected".as_bytes());
179                        }
180                    },
181                }
182            }
183        });
184
185        unsafe {
186            let boxed_logger = Box::new(self);
187            LOGGER = Some(Box::leak(boxed_logger));
188        }
189        Ok(())
190    }
191
192    fn process_log_command(
193        buffered_file_writer: &mut BufWriter<File>,
194        cmd: LogCommand,
195        gettime: fn() -> String,
196    ) {
197        match cmd {
198            LogCommand::Msg(msg) => {
199                let log_msg = format!("{} {} {}\n", gettime(), msg.level, msg.func.invoke());
200                let _ = buffered_file_writer.write_all(log_msg.as_bytes());
201            }
202            LogCommand::Flush(tx) => {
203                let _ = buffered_file_writer.flush();
204                let _ = tx.send(());
205            }
206        }
207    }
208
209    pub fn log(&self, level: Level, func: LoggingFunc) {
210        match &self.sender {
211            Some(tx) => {
212                tx.send(LogCommand::Msg(LogMetaData { level, func }))
213                    .unwrap();
214            }
215            None => (),
216        }
217    }
218
219    /// Blocking
220    pub fn flush(&self) {
221        if let Some(tx) = &self.sender {
222            let (flush_tx, flush_rx) = crossbeam_channel::bounded(1);
223            tx.send(LogCommand::Flush(flush_tx)).ok();
224            let _ = flush_rx.recv();
225        }
226    }
227}
228
229impl Default for Logger {
230    fn default() -> Self {
231        Self::new()
232    } 
233}
234
235impl Drop for Logger {
236    fn drop(&mut self) {
237        self.flush();
238    }
239}
240
241pub fn logger() -> &'static Logger {
242    unsafe { LOGGER.unwrap() }
243}
244
245fn get_utc_time() -> String {
246    format!("{}", Utc::now())
247}
248
249fn get_local_time() -> String {
250    format!("{}", Local::now())
251}
252
253#[cfg(test)]
254mod tests {
255    use crate::{debug, error, info, warn};
256    use crate::{LogMetaData, Logger};
257
258    #[test]
259    pub fn test_log() {
260        Logger::new().file("test.log").init().unwrap();
261        info!("hello {} {}", "world", 123);
262        warn!("hello world");
263        debug!("debug log");
264        error!("Something went wrong!");
265        assert_eq!(std::mem::size_of::<LogMetaData>(), 24)
266    }
267}