rust-utils 0.16.0

Various utility routines used in the rust programs I have written
Documentation
//! Rotating logging API
//! This can be used to create a configurable log
//! that saves to a file and optionally
//! prints to stdout as well.

use std::{
    env, fs, thread, process,
    path::PathBuf,
    panic::{self, PanicHookInfo},
    sync::{Arc, Mutex, RwLock, LazyLock},
    fmt::{
        Display,
        Formatter,
        Result as FmtResult
    },
};
use backtrace::Backtrace;
use chrono::{Timelike, Datelike, Local};
use colored::{Colorize, ColoredString};

static PANIC_LOG: LazyLock<RwLock<Option<Log>>> = LazyLock::new(|| RwLock::new(None));

/// An automatic rotating log based on the current date
///
/// It logs output like the following
///
/// `[hh:mm:ss] [LEVEL]: message`
#[derive(Clone)]
pub struct Log {
    name: String,
    folder: String,
    path: PathBuf,
    main_log_path: Option<PathBuf>,
    runlock: Arc<Mutex<()>>
}

impl Log {
    /// Creates a new log handle
    ///
    /// Logs will be stored under `$HOME/.local/share/<folder>` and
    /// named `<log_name>-<year>-<month>-<day>.log`
    ///
    /// Panics: You don't seem to have a home folder. Make sure $HOME is set.
    pub fn new(log_name: &str, folder: &str) -> Log {
        let path = PathBuf::from(format!("{}/.local/share/{folder}", env::var("HOME").expect("Where the hell is your home folder?!")));
        fs::create_dir_all(&path).unwrap_or(());

        Log {
            name: log_name.to_string(),
            folder: folder.to_string(),
            path,
            main_log_path: None,
            runlock: Arc::default()
        }
    }

    /// If this is true, there will be a main application log under
    /// `/tmp/<folder name>-<username>/app.log`
    ///
    /// This log will be available for viewing until the system is rebooted
    pub fn main_log(mut self, main_log: bool) -> Log {
        if main_log {
            let user = env::var("USER").unwrap();
            let main_log_path = PathBuf::from(format!("/tmp/{}-{user}", self.folder));
            fs::create_dir_all(&main_log_path).unwrap_or(());
            self.main_log_path = Some(main_log_path);
        }

        self
    }

    /// Print a line to the log
    ///
    /// This will print any object that implements `Display`
    pub fn line<T: Display>(&self, level: LogLevel, text: T, print_stdout: bool) {
        let run = self.runlock.lock().unwrap();
        if let LogLevel::Debug(false) = level {
            return;
        }

        let log_path = self.log_path();
        let mut log = fs::read_to_string(&log_path).unwrap_or_default();
        let text_str = text.to_string();

        for line in text_str.lines() {
            let now = Local::now();
            let msg = format!("[{}:{:02}:{:02}] [{level}]: {line}\n", now.hour(), now.minute(), now.second());
            if print_stdout { print!("{}", level.colorize(&msg)); }
            log.push_str(&msg);
        }

        fs::write(log_path, &log).expect("Unable to write to log file!");

        if let Some(main_log_path) = self.main_log_path() {
            let mut main_log = fs::read_to_string(&main_log_path).unwrap_or_default();
            for line in text_str.lines() {
                let now = Local::now();
                let msg = format!("[{}:{:02}:{:02}] [{level}]: {line}\n", now.hour(), now.minute(), now.second());
                main_log.push_str(&msg);
            }

            fs::write(main_log_path, main_log).expect("Unable to write to log file!");
        }

        drop(run);
    }
    
    /// Print a line to the log (basic info)
    pub fn line_basic<T: Display>(&self, text: T, print_stdout: bool) { self.line(LogLevel::Info, text, print_stdout); }

    /// Should this log handle be used to report application panics? This 
    /// creates a panic handler that logs the thread panicked, where the panic occurred in the source
    /// and the backtrace.
    ///
    /// This could be useful in conjunction with libraries that block stdout/stderr like cursive
    pub fn report_panics(&self, report: bool) {
        if report {
            *(PANIC_LOG.write().unwrap()) = Some(self.clone());
            panic::set_hook(Box::new(panic_handler));
        }
        else {
            *(PANIC_LOG.write().unwrap()) = None;
            drop(panic::take_hook());
        }
    }

    /// Returns the path of the main log for viewing
    ///
    /// Returns `None` if the main log is not enabled
    pub fn main_log_path(&self) -> Option<PathBuf> {
        self.main_log_path.as_ref()
            .map(|path| path.join("app.log"))
    }

    /// Returns the path of the log file that is currently being written to
    pub fn log_path(&self) -> PathBuf {
        let now = Local::now();
        let path = format!("{}-{}-{}-{}.log", self.name, now.year(), now.month(), now.day());
        self.path.join(path)
    }
}

/// Severity level for a log entry
#[derive(Copy, Clone)]
pub enum LogLevel {
    /// Possibly useful information
    Info,

    /// Debug information, can optionally be hidden
    Debug(bool),

    /// This might cause trouble
    Warn,

    /// Oops...
    ///
    /// Indicates an error has occurred
    Error,

    /// The Application has panicked
    Fatal
}

impl LogLevel {
    fn colorize(&self, input: &str) -> ColoredString {
        match self {
            Self::Debug(_) => input.cyan(),
            Self::Info => input.green(),
            Self::Warn => input.bright_yellow(),
            Self::Error => input.bright_red(),
            Self::Fatal => input.bright_red().on_black()
        }
    }
}

impl Display for LogLevel {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        match *self {
            LogLevel::Info => write!(f, "INFO"),
            LogLevel::Debug(_) => write!(f, "DEBUG"),
            LogLevel::Warn => write!(f, "WARN"),
            LogLevel::Error => write!(f, "ERROR"),
            LogLevel::Fatal => write!(f, "FATAL")
        }
    }
}

fn panic_handler(info: &PanicHookInfo) {
    let backtrace = format!("{:?}", Backtrace::new());
    let maybe_log = PANIC_LOG.read().unwrap();
    let panic_log = if let Some(ref log) = &*maybe_log {
        log
    }
    else {
        eprintln!("Internal Error");
        process::exit(101);
    };

    let cur_thread = thread::current();
    let thread_disp = if let Some(name) = cur_thread.name() {
        name.to_string()
    }
    else {
        format!("{:?}", cur_thread.id())
    };

    if let Some(loc) = info.location() {
        panic_log.line(LogLevel::Fatal, format!("Thread '{thread_disp}' panicked at {loc}"), true);
    }
    else {
        panic_log.line(LogLevel::Fatal, format!("Thread '{thread_disp}' panicked"), true);
    }

    if let Some(payload) = info.payload().downcast_ref::<String>() {
        panic_log.line(LogLevel::Fatal, format!("Error: {payload}"), true);
    }

    panic_log.line(LogLevel::Fatal, format!("Backtrace:\n{backtrace}"), true);
}