rslogger/
lib.rs

1mod writer;
2use std::{path::PathBuf, sync::RwLock};
3
4use crate::writer::BufferedWriter;
5
6use log::{Log, SetLoggerError, LevelFilter};
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}
37
38impl Logger {
39
40    /// Initializes the global logger with an RLogger instance with
41    /// default log level set to `Level::Trace`.
42    ///
43    /// ```no_run
44    /// use rslogger::Logger;
45    /// Logger::new().init().unwrap();
46    /// log::warn!("This is an example message.");
47    /// ```
48    ///
49    /// [`init`]: #method.init
50    #[must_use = "You must call init() to initialize the logger"]
51    pub fn new() -> Logger {
52        Logger { 
53            log_level: LevelFilter::Trace , 
54            timestamps: Timestamps::Local, 
55            target: false,
56            thread: false, 
57            writers: Vec::new() }
58    }
59
60    /// Sets the global log level of the logger. 
61    #[must_use = "You must call init() to initialize the logger"]
62    pub fn with_level(mut self, level: LevelFilter) -> Logger {
63        self.log_level = level;
64        self
65    }
66
67    /// Display timestamps in UTC time
68    #[must_use = "You must call init() to initialize the logger"]
69    pub fn with_utc_timestamps(mut self) -> Logger {
70        self.timestamps = Timestamps::Utc;
71        self
72    }
73
74    /// Display timestamps in Local time
75    #[must_use = "You must call init() to initialize the logger"]
76    pub fn with_local_timestamps(mut self) -> Logger {
77        self.timestamps = Timestamps::Local;
78        self
79    }
80
81    /// Don't display timestamps
82    #[must_use = "You must call init() to initialize the logger"]
83    pub fn without_timestamps(mut self) -> Logger {
84        self.timestamps = Timestamps::None;
85        self
86    }
87
88    /// Display thread Id
89    #[must_use = "You must call init() to initialize the logger"]
90    pub fn with_thread(mut self) -> Logger {
91        self.thread = true;
92        self
93    }
94
95    /// Displays the module name that is logging
96    #[must_use = "You must call init() to initialize the logger"]
97    pub fn with_target(mut self) -> Logger {
98        self.target = true;
99        self
100    }
101
102    /// Hides the module name that is logging
103    #[must_use = "You must call init() to initialize the logger"]
104    pub fn without_target(mut self) -> Logger {
105        self.target = false;
106        self
107    }
108
109    ///
110    /// Adds a stdout writer. 
111    /// # Param
112    /// * `multi_thread` - If set to true, the writer will be multi thread, otherwise single thread
113    /// * `capacity` - If Some(capacity), specified the buffer capacity of the writer. If None, initializes it with the default capacity.
114    /// 
115    #[must_use = "You must call init() to initialize the logger"]
116    pub fn add_writer_stdout(mut self, multi_thread: bool, capacity: Option<usize>) -> Logger {
117        let mut writer = BufferedWriter::new().on_stdout();
118
119        if multi_thread { writer = writer.with_separate_thread(); }
120        if let Some(buf_cap) = capacity { writer = writer.with_buffer_capacity(buf_cap) }
121
122        match writer.init() {
123            Ok(initialized_writer) => self.writers.push(RwLock::new(initialized_writer)),
124            Err(error) => println!("Error while initializing writer. Details: {}", error),
125        }
126
127        self
128    }
129
130    ///
131    /// Adds a file writer. 
132    /// # Param
133    /// * `file_path` - The path of the file to write on.
134    /// * `multi_thread` - If set to true, the writer will be multi thread, otherwise single thread
135    /// * `capacity` - If Some(capacity), specified the buffer capacity of the writer. If None, initializes it with the default capacity.
136    /// 
137    #[must_use = "You must call init() to initialize the logger"]
138    pub fn add_writer_file(mut self, file_path: PathBuf, multi_thread: bool, capacity: Option<usize>) -> Logger {
139        let mut writer = BufferedWriter::new().on_file(file_path);
140        
141        if multi_thread { writer = writer.with_separate_thread(); }
142        if let Some(buf_cap) = capacity { writer = writer.with_buffer_capacity(buf_cap) }
143
144        match writer.init() {
145            Ok(initialized_writer) => self.writers.push(RwLock::new(initialized_writer)),
146            Err(error) => println!("Error while initializing writer. Details: {}", error),
147        }
148
149        self
150    }
151
152    pub fn init(self) -> Result<(), SetLoggerError> {
153        log::set_max_level(self.log_level);
154        log::set_boxed_logger(Box::new(self))
155    }
156
157    pub fn log_level(&self) -> LevelFilter {
158        self.log_level
159    }
160}
161
162impl Default for Logger {
163    fn default() -> Self {
164        Logger::new()
165    }
166}
167
168impl Log for Logger {
169    fn enabled(&self, metadata: &log::Metadata) -> bool {
170        metadata.level().to_level_filter() <= self.log_level
171    }
172
173    fn log(&self, record: &log::Record) {
174        if !self.enabled(record.metadata()) {
175            return;
176        }
177
178        let mut target = "";
179
180        if self.target {
181            target = if !record.target().is_empty() {
182                    record.target()
183                } else {
184                    record.module_path().unwrap_or_default()
185                };
186        }
187
188
189        let thread = if self.thread {
190            format!("{}", std::thread::current().name().unwrap_or("?"))
191        } else {
192            "".to_string()
193        };
194        
195        let timestamp = match self.timestamps {
196            Timestamps::None => "".to_string(),
197            Timestamps::Local => format!(
198                "{}",
199                OffsetDateTime::now_local()
200                    .expect(concat!(
201                        "Could not determine the UTC offset on this system. ",
202                        "Consider displaying UTC time instead. ",
203                        "Possible causes are that the time crate does not implement \"local_offset_at\" ",
204                        "on your system, or that you are running in a multi-threaded environment and ",
205                        "the time crate is returning \"None\" from \"local_offset_at\" to avoid unsafe ",
206                        "behaviour. See the time crate's documentation for more information. ",
207                        "(https://time-rs.github.io/internal-api/time/index.html#feature-flags)"
208                    ))
209                    .format(TIMESTMAMP_FORMAT)
210                    .unwrap()
211            ),
212            Timestamps::Utc => format!( "{}", UtcDateTime::now().format(TIMESTMAMP_FORMAT).unwrap()),
213        };
214
215        let message = format!("{timestamp}-[{target}][{thread}] -> {{{}}} {}", record.level().to_string(), record.args());
216
217        for writer in &self.writers {
218            if let Ok(writer_mut) = writer.write() {
219                writer_mut.write(message.as_str());
220            } else {
221                panic!("Cannot get writer as mutable. RWLock is poisoned!");
222            }
223        }
224    }
225
226    ///
227    /// Flushes to ensure that all possible buffered data are logged. 
228    /// This method should be called right before closing the program or when you don't need to 
229    /// log anything else. 
230    /// This is because in case of separate thread, the logger thread will be stopped.
231    /// # Example
232    /// ```
233    /// use rslogger::Logger;
234    /// use log::{info, warn, error};
235    /// Logger::new()
236    ///     .with_level(log::LevelFilter::Trace)
237    ///     .with_local_timestamps()
238    ///     .add_writer_stdout(true, Some(1000))
239    ///     .init().unwrap();
240    /// info!("This is a info test");
241    /// warn!("This is a warn test");
242    /// error!("This is an error test");
243    /// log::logger().flush();
244    /// let result = std::panic::catch_unwind(|| info!("Test"));
245    /// assert!(result.is_err())
246    /// ```
247    /// 
248    fn flush(&self) {
249        for writer in &self.writers {
250            if let Ok(mut writer_mut) = writer.write() {
251                writer_mut.flush_and_cleanup();
252            } else {
253                panic!("Cannot get writer as mutable. RWLock is poisoned!");
254            }
255        }
256    }
257
258}
259
260#[cfg(test)]
261mod tests {
262    use log::{Metadata, Level};
263
264    use super::*;
265
266    #[test]
267    fn test_default_level() {
268        let builder = Logger::new();
269        assert_eq!(builder.log_level(), LevelFilter::Trace);
270    }
271
272    #[test]
273    fn test_creation_level() {
274        let builder = Logger::new().with_level(LevelFilter::Debug);
275        assert_eq!(builder.log_level(), LevelFilter::Debug);
276    }
277
278    #[test]
279    fn test_logger_enabled() {
280        let logger = Logger::new().with_level(LevelFilter::Debug);
281        assert_eq!(logger.log_level(), LevelFilter::Debug);
282        assert!(logger.enabled(&create_log("test_enabled", Level::Debug)));
283    }
284
285    #[test]
286    fn test_timestamp_default() {
287        let builder = Logger::new();
288        assert!(builder.timestamps == Timestamps::Local);
289    }
290
291    #[test]
292    fn test_utc_timestamp() {
293        let builder = Logger::new().with_utc_timestamps();
294        assert!(builder.timestamps == Timestamps::Utc);
295    }
296
297
298    fn create_log(name: &str, level: Level) -> Metadata {
299        let mut builder = Metadata::builder();
300        builder.level(level);
301        builder.target(name);
302        builder.build()
303    }
304}