packxel_utils/logging/
mod.rs

1//! Logging module for Packxel
2//!
3//! ## Default Logger
4//! ```
5//! use packxel_utils::initializer::Initializer;
6//! let initializer = Initializer::init("package-name", "0.0.1");
7//! PackxelLogger::init(initializer);
8//! // Run with RUST_LOG=debug cargo run
9//! ```
10//!
11//! ## With Builder
12//!
13//! ```
14//! use packxel_utils::initializer::Initializer;
15//! use packxel_utils::logging::PackxelLoggerBuilder;
16//!
17//! let initializer = Initializer::init("package-name", "0.0.1");
18//! let builder = PackxelLoggerBuilder::new(initializer)
19//!     .with_default_log_level()
20//!     .with_file_writer(false);
21//!     .build()
22//! ```
23
24use std::{env::temp_dir, fs::File, io::Write, str::FromStr};
25
26use colored::{ColoredString, Colorize};
27use log::{warn, Level};
28
29use crate::initializer::PackxelInitializer;
30
31/// FileWriter. Will be used for log rotation.
32/// Once flush is added.
33#[derive(Debug)]
34struct FileWriter {
35    file: File,
36}
37
38#[derive(Debug, Default)]
39/// Base logger for Packxel
40pub struct PackxelLogger {
41    log_level: Option<Level>,
42    print_lines: bool,
43    file_writer: Option<FileWriter>,
44    log_file_path: String,
45}
46
47#[derive(Debug)]
48/// Builder mechanism for PackxelLogger
49/// ## Examples
50///
51/// ```
52/// let logger = PackxelLoggerBuilder::new(initializer)
53///     .with_default_log_level()
54///     .build()
55/// ```
56pub struct PackxelLoggerBuilder {
57    log_level: Option<Level>,
58    print_lines: bool,
59    initializer: Option<PackxelInitializer>,
60    file_writer: bool,
61}
62
63impl PackxelLoggerBuilder {
64    fn new(initializer: Option<PackxelInitializer>) -> Self {
65        Self {
66            log_level: None,
67            print_lines: false,
68            initializer,
69            file_writer: true,
70        }
71    }
72
73    fn with_default_log_level(mut self) -> PackxelLoggerBuilder {
74        let log_level = get_level_from_env();
75        self.log_level = log_level;
76        self
77    }
78
79    pub fn with_log_level(mut self, log_level: Level) -> PackxelLoggerBuilder {
80        self.log_level = Some(log_level);
81        self
82    }
83
84    pub fn with_print_lines(mut self, print_lines: bool) -> PackxelLoggerBuilder {
85        self.print_lines = print_lines;
86        self
87    }
88
89    pub fn with_file_writer(mut self, file_writer: bool) -> PackxelLoggerBuilder {
90        self.file_writer = file_writer;
91        self
92    }
93
94    fn try_build(self) -> std::io::Result<PackxelLogger> {
95        let file_writer;
96        let mut log_file_path;
97        if self.initializer.is_some() {
98            let initializer = self.initializer.unwrap();
99            log_file_path = initializer.get_log_file();
100        } else {
101            log_file_path = temp_dir();
102            log_file_path.push("data.log");
103        }
104
105        let file = File::options()
106            .append(true)
107            .create(true)
108            .open(&log_file_path)
109            .expect("Unable to open file");
110
111        let log_file_path = log_file_path
112            .to_str()
113            .expect("Failed to get log file path from path buffer")
114            .to_string();
115
116        if self.file_writer {
117            file_writer = Some(FileWriter { file })
118        } else {
119            file_writer = None
120        }
121
122        Ok(PackxelLogger {
123            log_level: self.log_level,
124            print_lines: self.print_lines,
125            file_writer,
126            log_file_path,
127        })
128    }
129
130    fn build(self) -> PackxelLogger {
131        self.try_build().expect("Failed to build logger.")
132    }
133}
134
135impl PackxelLogger {
136    /// Logger with builder capacity.
137    ///
138    /// Calls `log::set_boxed_logger` which an be only called once.
139    /// ```
140    /// use packxel_utils::initializer::Initializer;
141    /// use packxel_utils::logging::PackxelLoggerBuilder;
142    ///
143    /// let initializer = Initializer::init("package-name", "0.0.1");
144    /// let builder = PackxelLoggerBuilder::new(initializer)
145    ///     .with_default_log_level()
146    ///     .with_file_writer(false);
147    ///     .build();
148    ///
149    /// PackxelLogger::init_with_builder(builder);
150    ///
151    /// log::info!("Hello World");
152    /// ```
153    pub fn init_with_builder(builder: PackxelLoggerBuilder) -> Result<(), log::SetLoggerError> {
154        log::set_boxed_logger(Box::new(builder.build()))
155            .map(|()| log::set_max_level(log::LevelFilter::Trace))
156    }
157
158    /// Default logger. Can be only called once.
159    ///
160    /// Calls `log::set_boxed_logger` which an be only called once.
161    /// 
162    /// ```
163    /// use packxel_utils::initializer::Initializer;
164    /// use packxel_utils::logging::PackxelLoggerBuilder;
165    ///
166    /// let initializer = Initializer::init("package-name", "0.0.1");
167    /// 
168    /// PackxelLogger::init(initializer);
169    ///
170    /// log::info!("Hello World");
171    /// ```
172    pub fn init(initializer: PackxelInitializer) -> Result<(), log::SetLoggerError> {
173        log::set_boxed_logger(Box::new(
174            PackxelLoggerBuilder::new(Some(initializer))
175                .with_default_log_level()
176                .build(),
177        ))
178        .map(|()| log::set_max_level(log::LevelFilter::Trace))
179    }
180
181    fn get_colored_string(level: Level) -> ColoredString {
182        match level {
183            Level::Error => format!("{} ", level).as_str().red(),
184            Level::Warn => format!("{}  ", level).as_str().yellow(),
185            Level::Info => format!("{}  ", level).as_str().green(),
186            Level::Debug => format!("{} ", level).as_str().blue(),
187            Level::Trace => format!("{} ", level).as_str().clear(),
188        }
189    }
190}
191
192fn get_level_from_env() -> Option<Level> {
193    let log_level = std::env::var("RUST_LOG").unwrap_or("".to_string());
194    let log_level = Level::from_str(&log_level);
195    let log_level = match log_level {
196        Ok(log_level) => Some(log_level),
197        Err(_) => None,
198    };
199    log_level
200}
201
202fn rotate_file<S: AsRef<str>>(buffer: &mut &File, log_file_path: S) -> std::io::Result<bool> {
203    let file_metadata = buffer.metadata()?;
204
205    let file_size = file_metadata.len();
206
207    let one_mb = 1000 * 1000;
208
209    if file_size > 9 * one_mb {
210        warn!("Log file is getting large. Will be rotating file.")
211    }
212
213    if file_size > 10 * one_mb {
214        let new_file_name = format!(
215            "{}-{}.old",
216            chrono::Local::now().timestamp(),
217            log_file_path.as_ref()
218        );
219        std::fs::copy(log_file_path.as_ref(), new_file_name)?;
220
221        {
222            todo!("Seek to start of file")
223            // buffer.set_len(0)?;
224            // buffer.seek(std::io::SeekFrom::Start(0))?;
225        }
226    }
227
228    Ok(false)
229}
230
231impl log::Log for PackxelLogger {
232    /// _Check if enabled. Used inside crate `log` macros._
233    fn enabled(&self, metadata: &log::Metadata) -> bool {
234        if self.log_level.is_none() {
235            return false;
236        }
237        return metadata.level() <= self.log_level.unwrap();
238    }
239
240    /// Push to buffer and print to stdout.
241    fn log(&self, record: &log::Record) {
242        let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
243        let level = Self::get_colored_string(record.level());
244
245        if self.enabled(record.metadata()) {
246            if self.file_writer.is_some() {
247                let file_writer = self.file_writer.as_ref().unwrap();
248
249                let output = format!(
250                    "{} {} {}\n",
251                    level.clone().clear(),
252                    timestamp,
253                    record.args()
254                );
255
256                let mut file = &file_writer.file;
257
258                file.write(output.as_bytes())
259                    .expect("Unable to write to file");
260
261                match rotate_file(&mut &file_writer.file, &self.log_file_path) {
262                    Ok(rotated) => {
263                        if rotated {
264                            println!("Rotated");
265                        }
266                    }
267                    Err(_) => {
268                        log::error!("Failed to rotate log file");
269                    }
270                };
271            }
272
273            print!("{}", level);
274            print!("{}\t", timestamp);
275
276            if record.level() == Level::Trace || self.print_lines {
277                println!(
278                    "{}:{} - {}",
279                    record.file().unwrap_or("unknown"),
280                    record.line().unwrap_or(0),
281                    record.args(),
282                );
283            } else {
284                println!("{}", record.args());
285            }
286        }
287    }
288
289    fn flush(&self) {}
290}