rust_utils/
logging.rs

1//! Rotating logging API
2//! This can be used to create a configurable log
3//! that saves to a file and optionally
4//! prints to stdout as well.
5
6use std::{
7    fs, env,
8    process, thread,
9    path::PathBuf,
10    fmt::{Display, Formatter, Result as FmtResult},
11    sync::{RwLock, Mutex, Arc},
12    panic::{self, PanicInfo}
13};
14use backtrace::Backtrace;
15use chrono::{Timelike, Datelike, Local};
16use colored::{Colorize, ColoredString};
17use lazy_static::lazy_static;
18
19lazy_static! {
20    static ref PANIC_LOG: RwLock<Option<Log>> = RwLock::new(None);
21}
22
23/// An automatic rotating log based on the current date
24///
25/// It logs output like the following
26///
27/// `[hh:mm:ss] [LEVEL]: message`
28#[derive(Clone)]
29pub struct Log {
30    name: String,
31    folder: String,
32    path: PathBuf,
33    main_log_path: Option<PathBuf>,
34    runlock: Arc<Mutex<()>>
35}
36
37impl Log {
38    /// Creates a new log handle
39    ///
40    /// Logs will be stored under `$HOME/.local/share/<folder>` and
41    /// named `<log_name>-<year>-<month>-<day>.log`
42    ///
43    /// Panics: You don't seem to have a home folder. Make sure $HOME is set.
44    pub fn new(log_name: &str, folder: &str) -> Log {
45        let path = PathBuf::from(format!("{}/.local/share/{folder}", env::var("HOME").expect("Where the hell is your home folder?!")));
46        fs::create_dir_all(&path).unwrap_or(());
47
48        Log {
49            name: log_name.to_string(),
50            folder: folder.to_string(),
51            path,
52            main_log_path: None,
53            runlock: Arc::default()
54        }
55    }
56
57    /// If this is true, there will be a main application log under
58    /// `/tmp/<folder name>-<username>/app.log`
59    ///
60    /// This log will be available for viewing until the system is rebooted
61    pub fn main_log(mut self, main_log: bool) -> Log {
62        if main_log {
63            let user = env::var("USER").unwrap();
64            let main_log_path = PathBuf::from(format!("/tmp/{}-{user}", self.folder));
65            fs::create_dir_all(&main_log_path).unwrap_or(());
66            self.main_log_path = Some(main_log_path);
67        }
68
69        self
70    }
71
72    /// Print a line to the log
73    ///
74    /// This will print any object that implements `Display`
75    pub fn line<T: Display>(&self, level: LogLevel, text: T, print_stdout: bool) {
76        let run = self.runlock.lock().unwrap();
77        if let LogLevel::Debug(false) = level {
78            return;
79        }
80
81        let log_path = self.log_path();
82        let mut log = fs::read_to_string(&log_path).unwrap_or_default();
83        let text_str = text.to_string();
84
85        for line in text_str.lines() {
86            let now = Local::now();
87            let msg = format!("[{}:{:02}:{:02}] [{level}]: {line}\n", now.hour(), now.minute(), now.second());
88            if print_stdout { print!("{}", level.colorize(&msg)); }
89            log.push_str(&msg);
90        }
91
92        fs::write(log_path, &log).expect("Unable to write to log file!");
93
94        if let Some(main_log_path) = self.main_log_path() {
95            let mut main_log = fs::read_to_string(&main_log_path).unwrap_or_default();
96            for line in text_str.lines() {
97                let now = Local::now();
98                let msg = format!("[{}:{:02}:{:02}] [{level}]: {line}\n", now.hour(), now.minute(), now.second());
99                main_log.push_str(&msg);
100            }
101
102            fs::write(main_log_path, main_log).expect("Unable to write to log file!");
103        }
104
105        drop(run);
106    }
107    
108    /// Print a line to the log (basic info)
109    pub fn line_basic<T: Display>(&self, text: T, print_stdout: bool) { self.line(LogLevel::Info, text, print_stdout); }
110
111    /// Should this log handle be used to report application panics? This 
112    /// creates a panic handler that logs the thread panicked, where the panic occurred in the source
113    /// and the backtrace.
114    ///
115    /// This could be useful in conjunction with libraries that block stdout/stderr like cursive
116    pub fn report_panics(&self, report: bool) {
117        if report {
118            *(PANIC_LOG.write().unwrap()) = Some(self.clone());
119            panic::set_hook(Box::new(panic_handler));
120        }
121        else {
122            *(PANIC_LOG.write().unwrap()) = None;
123            drop(panic::take_hook());
124        }
125    }
126
127    /// Returns the path of the main log for viewing
128    ///
129    /// Returns `None` if the main log is not enabled
130    pub fn main_log_path(&self) -> Option<PathBuf> {
131        self.main_log_path.as_ref()
132            .map(|path| path.join("app.log"))
133    }
134
135    /// Returns the path of the log file that is currently being written to
136    pub fn log_path(&self) -> PathBuf {
137        let now = Local::now();
138        let path = format!("{}-{}-{}-{}.log", self.name, now.year(), now.month(), now.day());
139        self.path.join(path)
140    }
141}
142
143/// Severity level for a log entry
144#[derive(Copy, Clone)]
145pub enum LogLevel {
146    /// Possibly useful information
147    Info,
148
149    /// Debug information, can optionally be hidden
150    Debug(bool),
151
152    /// This might cause trouble
153    Warn,
154
155    /// Oops...
156    ///
157    /// Indicates an error has occurred
158    Error,
159
160    /// The Application has panicked
161    Fatal
162}
163
164impl LogLevel {
165    fn colorize(&self, input: &str) -> ColoredString {
166        match self {
167            Self::Debug(_) => input.cyan(),
168            Self::Info => input.green(),
169            Self::Warn => input.bright_yellow(),
170            Self::Error => input.bright_red(),
171            Self::Fatal => input.bright_red().on_black()
172        }
173    }
174}
175
176impl Display for LogLevel {
177    fn fmt(&self, f: &mut Formatter) -> FmtResult {
178        match *self {
179            LogLevel::Info => write!(f, "INFO"),
180            LogLevel::Debug(_) => write!(f, "DEBUG"),
181            LogLevel::Warn => write!(f, "WARN"),
182            LogLevel::Error => write!(f, "ERROR"),
183            LogLevel::Fatal => write!(f, "FATAL")
184        }
185    }
186}
187
188fn panic_handler(info: &PanicInfo) {
189    let backtrace = format!("{:?}", Backtrace::new());
190    let maybe_log = PANIC_LOG.read().unwrap();
191    let panic_log = if let Some(ref log) = &*maybe_log {
192        log
193    }
194    else {
195        eprintln!("Internal Error");
196        process::exit(101);
197    };
198
199    let cur_thread = thread::current();
200    let thread_disp = if let Some(name) = cur_thread.name() {
201        name.to_string()
202    }
203    else {
204        format!("{:?}", cur_thread.id())
205    };
206
207    if let Some(loc) = info.location() {
208        panic_log.line(LogLevel::Fatal, format!("Thread '{thread_disp}' panicked at {loc}"), true);
209    }
210    else {
211        panic_log.line(LogLevel::Fatal, format!("Thread '{thread_disp}' panicked"), true);
212    }
213
214    if let Some(payload) = info.payload().downcast_ref::<String>() {
215        panic_log.line(LogLevel::Fatal, format!("Error: {payload}"), true);
216    }
217
218    panic_log.line(LogLevel::Fatal, format!("Backtrace:\n{backtrace}"), true);
219}