rslogger/
lib.rs

1mod writer;
2use std::{path::PathBuf, sync::RwLock};
3
4use crate::writer::BufferedWriter;
5
6use log::{LevelFilter, Log, SetLoggerError};
7use time::{format_description::FormatItem, OffsetDateTime, UtcDateTime};
8
9const TIMESTMAMP_FORMAT: &[FormatItem] = time::macros::format_description!(
10    "[hour]:[minute]:[second]:[subsecond digits:6]"
11);
12
13
14#[derive(PartialEq)]
15enum Timestamps {
16    None, 
17    Local,
18    Utc,
19}
20
21pub struct Logger {
22    /// The default log level for all the logs.
23    log_level: LevelFilter,
24    timestamps: Timestamps,
25    thread: bool,
26    target: bool,
27    ///
28    /// The RwLock is needed to provide interior mutability. 
29    /// That bitch of the Log crate decided to declare flush method as flush(&self) and not 
30    /// flush(&mut self) and there is no way to call a method of the Logger struct that is not 
31    /// in the Log Trait (because when you set the boxed logger, they convert it into &'static).
32    /// So in order to ensure that the multi threaded BufferedWriter can flush and stop the thread
33    /// we need a mutable reference to it inside the flush method.
34    /// Also, it is an RwLock and not an Rc because this structure must be Sync + Send.
35    writers: Vec<RwLock<BufferedWriter>>,
36    writer_levels: Vec<LevelFilter>,
37}
38
39impl Logger {
40
41    /// Initializes the global logger with an RLogger instance with
42    /// default log level set to `Level::Trace`.
43    ///
44    /// ```no_run
45    /// use rslogger::Logger;
46    /// Logger::new().init().unwrap();
47    /// log::warn!("This is an example message.");
48    /// ```
49    ///
50    /// [`init`]: #method.init
51    #[must_use = "You must call init() to initialize the logger"]
52    pub fn new() -> Logger {
53        Logger { 
54            log_level: LevelFilter::Trace , 
55            timestamps: Timestamps::Local, 
56            target: false,
57            thread: false, 
58            writers: Vec::new(),
59            writer_levels: Vec::new()
60        }
61    }
62
63    /// Sets the global log level of the logger. 
64    #[must_use = "You must call init() to initialize the logger"]
65    pub fn with_level(mut self, level: LevelFilter) -> Logger {
66        self.log_level = level;
67        self
68    }
69
70    /// Display timestamps in UTC time
71    #[must_use = "You must call init() to initialize the logger"]
72    pub fn with_utc_timestamps(mut self) -> Logger {
73        self.timestamps = Timestamps::Utc;
74        self
75    }
76
77    /// Display timestamps in Local time
78    #[must_use = "You must call init() to initialize the logger"]
79    pub fn with_local_timestamps(mut self) -> Logger {
80        self.timestamps = Timestamps::Local;
81        self
82    }
83
84    /// Don't display timestamps
85    #[must_use = "You must call init() to initialize the logger"]
86    pub fn without_timestamps(mut self) -> Logger {
87        self.timestamps = Timestamps::None;
88        self
89    }
90
91    /// Display thread Id
92    #[must_use = "You must call init() to initialize the logger"]
93    pub fn with_thread(mut self) -> Logger {
94        self.thread = true;
95        self
96    }
97
98    /// Displays the module name that is logging
99    #[must_use = "You must call init() to initialize the logger"]
100    pub fn with_target(mut self) -> Logger {
101        self.target = true;
102        self
103    }
104
105    /// Hides the module name that is logging
106    #[must_use = "You must call init() to initialize the logger"]
107    pub fn without_target(mut self) -> Logger {
108        self.target = false;
109        self
110    }
111
112    ///
113    /// Adds a stdout writer. 
114    /// # Param
115    /// * `multi_thread` - If set to true, the writer will be multi thread, otherwise single thread
116    /// * `capacity` - If Some(capacity), specified the buffer capacity of the writer. If None, initializes it with the default capacity.
117    /// 
118    #[must_use = "You must call init() to initialize the logger"]
119    pub fn add_writer_stdout(mut self, multi_thread: bool, capacity: Option<usize>) -> Logger {
120        let default_level = self.log_level.clone();
121        self = self.add_writer_stdout_level(multi_thread, capacity, default_level);
122        self
123    }
124
125    ///
126    /// Adds a stdout writer. 
127    /// # Param
128    /// * `multi_thread` - If set to true, the writer will be multi thread, otherwise single thread
129    /// * `capacity` - If Some(capacity), specified the buffer capacity of the writer. If None, initializes it with the default capacity.
130    /// * `level` - Max level for this tracer.
131    /// 
132    #[must_use = "You must call init() to initialize the logger"]
133    pub fn add_writer_stdout_with_level(mut self, multi_thread: bool, capacity: Option<usize>, level: LevelFilter) -> Logger {
134        self = self.add_writer_stdout_level(multi_thread, capacity, level);
135        self
136    }
137
138    ///
139    /// Adds a file writer. 
140    /// # Param
141    /// * `file_path` - The path of the file to write on.
142    /// * `multi_thread` - If set to true, the writer will be multi thread, otherwise single thread
143    /// * `capacity` - If Some(capacity), specified the buffer capacity of the writer. If None, initializes it with the default capacity.
144    /// * `level` - Max level for this tracer.
145    /// 
146    #[must_use = "You must call init() to initialize the logger"]
147    pub fn add_writer_file(self, file_path: PathBuf, multi_thread: bool, capacity: Option<usize>) -> Logger {
148        let default_level = self.log_level.clone();
149        self.add_writer_file_level(file_path, multi_thread, capacity, default_level)
150    }
151
152    ///
153    /// Adds a file writer. 
154    /// # Param
155    /// * `file_path` - The path of the file to write on.
156    /// * `multi_thread` - If set to true, the writer will be multi thread, otherwise single thread
157    /// * `capacity` - If Some(capacity), specified the buffer capacity of the writer. If None, initializes it with the default capacity.
158    /// 
159    #[must_use = "You must call init() to initialize the logger"]
160    pub fn add_writer_file_with_level(self, file_path: PathBuf, multi_thread: bool, capacity: Option<usize>, level: LevelFilter) -> Logger {
161        self.add_writer_file_level(file_path, multi_thread, capacity, level)
162    }
163
164    pub fn init(self) -> Result<(), SetLoggerError> {
165        log::set_max_level(self.log_level);
166        log::set_boxed_logger(Box::new(self))
167    }
168
169    pub fn log_level(&self) -> LevelFilter {
170        self.log_level
171    }
172
173
174    fn add_writer_stdout_level(mut self, multi_thread: bool, capacity: Option<usize>, level: LevelFilter) -> Logger {
175        let mut writer = BufferedWriter::new().on_stdout();
176
177        if multi_thread { writer = writer.with_separate_thread(); }
178        if let Some(buf_cap) = capacity { writer = writer.with_buffer_capacity(buf_cap) }
179
180        match writer.init() {
181            Ok(initialized_writer) => {
182                self.writers.push(RwLock::new(initialized_writer));
183                self.writer_levels.push(level.to_owned());
184            },
185            Err(error) => println!("Error while initializing writer. Details: {}", error),
186        }
187
188        self
189    }
190
191
192    fn add_writer_file_level(mut self, file_path: PathBuf, multi_thread: bool, capacity: Option<usize>, level: LevelFilter) -> Logger {
193        let mut writer = BufferedWriter::new().on_file(file_path);
194        
195        if multi_thread { writer = writer.with_separate_thread(); }
196        if let Some(buf_cap) = capacity { writer = writer.with_buffer_capacity(buf_cap) }
197
198        match writer.init() {
199            Ok(initialized_writer) => {
200                self.writers.push(RwLock::new(initialized_writer));
201                self.writer_levels.push(level.to_owned());
202            },
203            Err(error) => println!("Error while initializing writer. Details: {}", error),
204        }
205
206        self
207    }
208}
209
210impl Default for Logger {
211    fn default() -> Self {
212        Logger::new()
213    }
214}
215
216impl Log for Logger {
217    fn enabled(&self, metadata: &log::Metadata) -> bool {
218        metadata.level().to_level_filter() <= self.log_level
219    }
220
221    fn log(&self, record: &log::Record) {
222        if !self.enabled(record.metadata()) {
223            return;
224        }
225
226        let mut target = "";
227
228        if self.target {
229            target = if !record.target().is_empty() {
230                    record.target()
231                } else {
232                    record.module_path().unwrap_or_default()
233                };
234        }
235
236
237        let thread = if self.thread {
238            if let Some(thread_name) = std::thread::current().name() {
239                format!("{}", thread_name)
240            } else {
241                format!("{:?}", std::thread::current().id())
242            }
243        } else {
244            "".to_string()
245        };
246        
247        let timestamp = match self.timestamps {
248            Timestamps::None => "".to_string(),
249            Timestamps::Local => format!(
250                "{}",
251                OffsetDateTime::now_local()
252                    .expect(concat!(
253                        "Could not determine the UTC offset on this system. ",
254                        "Consider displaying UTC time instead. ",
255                        "Possible causes are that the time crate does not implement \"local_offset_at\" ",
256                        "on your system, or that you are running in a multi-threaded environment and ",
257                        "the time crate is returning \"None\" from \"local_offset_at\" to avoid unsafe ",
258                        "behaviour. See the time crate's documentation for more information. ",
259                        "(https://time-rs.github.io/internal-api/time/index.html#feature-flags)"
260                    ))
261                    .format(TIMESTMAMP_FORMAT)
262                    .unwrap()
263            ),
264            Timestamps::Utc => format!( "{}", UtcDateTime::now().format(TIMESTMAMP_FORMAT).unwrap()),
265        };
266
267        let message = format!("{timestamp}-[{target}][{thread}] -> {{{}}} {}", record.level().to_string(), record.args());
268
269        for (index, writer) in self.writers.iter().enumerate() {
270            if index >= self.writer_levels.len() {
271                panic!("Level index out of range!");
272            }
273
274            // Skip as this trace should not be traced on this writer!
275            if record.metadata().level().to_level_filter() > self.writer_levels[index] {
276                continue;
277            }
278
279            if let Ok(writer_mut) = writer.write() {
280                writer_mut.write(message.as_str());
281            } else {
282                panic!("Cannot get writer as mutable. RWLock is poisoned!");
283            }
284        }
285    }
286
287    ///
288    /// Flushes to ensure that all possible buffered data are logged. 
289    /// This method should be called right before closing the program or when you don't need to 
290    /// log anything else. 
291    /// This is because in case of separate thread, the logger thread will be stopped.
292    /// # Example
293    /// ```
294    /// use rslogger::Logger;
295    /// use log::{info, warn, error};
296    /// Logger::new()
297    ///     .with_level(log::LevelFilter::Trace)
298    ///     .with_local_timestamps()
299    ///     .add_writer_stdout(true, Some(1000))
300    ///     .init().unwrap();
301    /// info!("This is a info test");
302    /// warn!("This is a warn test");
303    /// error!("This is an error test");
304    /// log::logger().flush();
305    /// let result = std::panic::catch_unwind(|| info!("Test"));
306    /// assert!(result.is_err())
307    /// ```
308    /// 
309    fn flush(&self) {
310        for writer in &self.writers {
311            if let Ok(mut writer_mut) = writer.write() {
312                writer_mut.flush_and_cleanup();
313            } else {
314                panic!("Cannot get writer as mutable. RWLock is poisoned!");
315            }
316        }
317    }
318
319}
320
321#[cfg(test)]
322mod tests {
323    use log::{Metadata, Level};
324
325    use super::*;
326
327    #[test]
328    fn test_default_level() {
329        let builder = Logger::new();
330        assert_eq!(builder.log_level(), LevelFilter::Trace);
331    }
332
333    #[test]
334    fn test_creation_level() {
335        let builder = Logger::new().with_level(LevelFilter::Debug);
336        assert_eq!(builder.log_level(), LevelFilter::Debug);
337    }
338
339    #[test]
340    fn test_logger_enabled() {
341        let logger = Logger::new().with_level(LevelFilter::Debug);
342        assert_eq!(logger.log_level(), LevelFilter::Debug);
343        assert!(logger.enabled(&create_log("test_enabled", Level::Debug)));
344    }
345
346    #[test]
347    fn test_timestamp_default() {
348        let builder = Logger::new();
349        assert!(builder.timestamps == Timestamps::Local);
350    }
351
352    #[test]
353    fn test_utc_timestamp() {
354        let builder = Logger::new().with_utc_timestamps();
355        assert!(builder.timestamps == Timestamps::Utc);
356    }
357
358
359    fn create_log(name: &str, level: Level) -> Metadata {
360        let mut builder = Metadata::builder();
361        builder.level(level);
362        builder.target(name);
363        builder.build()
364    }
365}