bin_common 0.2.0

A library for common things in binaries.
Documentation
use std::path::PathBuf;

use directories::{BaseDirs, ProjectDirs};
use fern::colors::{Color, ColoredLevelConfig};

use crate::CrateSetup;

#[cfg(feature = "log_rotation")]
mod rotation;

pub struct LoggingSetupBuilder<'a> {
    crate_setup: &'a CrateSetup,
    verbosity: Option<i8>,
    log_to_file: Option<bool>,
    log_filters: Vec<Box<fern::Filter>>,
    #[cfg(feature = "log_rotation")]
    log_rotation: Option<bool>,
    #[cfg(feature = "panic_handler")]
    log_panics: Option<bool>,
}

impl<'a> LoggingSetupBuilder<'a> {
    pub fn new(crate_setup: &'a CrateSetup) -> Self {
        Self {
            crate_setup,
            verbosity: None,
            log_to_file: None,
            log_filters: Vec::new(),
            log_rotation: None,
            log_panics: None,
        }
    }

    pub fn with_verbosity(mut self, verbosity: i8) -> Self {
        self.verbosity = Some(verbosity);
        self
    }

    pub fn with_log_to_file(mut self, log_to_file: bool) -> Self {
        self.log_to_file = Some(log_to_file);
        self
    }

    pub fn with_filter<F>(mut self, filter: F) -> Self
    where
        F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
    {
        self.log_filters.push(Box::new(filter));
        self
    }

    #[cfg(feature = "log_rotation")]
    pub fn with_log_rotation(mut self, log_rotation: bool) -> Self {
        self.log_rotation = Some(log_rotation);
        self
    }

    #[cfg(feature = "panic_handler")]
    pub fn with_log_panics(mut self, log_panics: bool) -> Self {
        self.log_panics = Some(log_panics);
        self
    }

    pub fn build(self) -> Result<(), fern::InitError> {
        // set defaults
        let verbosity = self
            .verbosity
            .unwrap_or_else(|| if cfg!(debug_assertions) { 2 } else { 1 });
        let log_to_file = self.log_to_file.unwrap_or(false);
        #[cfg(feature = "log_rotation")]
        let log_rotation = self.log_rotation.unwrap_or(false);
        #[cfg(not(feature = "log_rotation"))]
        let log_rotation = false;
        #[cfg(feature = "panic_handler")]
        let log_panics = self.log_panics.unwrap_or(false);

        // set up logger
        let colors = ColoredLevelConfig::new()
            .error(Color::Red)
            .warn(Color::Yellow)
            .info(Color::Green)
            .debug(Color::White)
            .trace(Color::BrightBlack);
        let mut logger = fern::Dispatch::new();
        logger = match verbosity {
            e if e < 0 => logger.level(log::LevelFilter::Error),
            0 => logger.level(log::LevelFilter::Warn),
            1 => logger.level(log::LevelFilter::Info),
            2 => logger.level(log::LevelFilter::Debug),
            _ => logger.level(log::LevelFilter::Trace),
        };
        for filter in self.log_filters {
            logger = logger.filter(filter);
        }

        // create stdout logger
        let stdout_logger = fern::Dispatch::new()
            .format(move |out, message, record| {
                out.finish(format_args!(
                    "{}[{}][{}][{}] {}\x1B[0m",
                    format_args!("\x1B[{}m", colors.get_color(&record.level()).to_fg_str()),
                    chrono::Local::now().format("%H:%M:%S"),
                    record.level(),
                    record.target(),
                    message
                ))
            })
            .chain(std::io::stdout());

        // set up file logger
        #[allow(unused_mut, unused_variables)]
        let mut old_logs = Vec::<PathBuf>::new();
        if log_to_file {
            let log_dir = get_log_path(
                self.crate_setup.base_dirs(),
                self.crate_setup.project_dirs(),
            );
            if log_rotation {
                // get old log files and log to two different log files
                #[cfg(feature = "log_rotation")]
                {
                    old_logs = rotation::get_old_log_files(&log_dir);
                    let log_file_latest = log_dir.join("latest.log");
                    let log_file_time = log_dir.join(
                        format_args!("{}.log", chrono::Local::now().format("%Y.%m.%d.%H.%M.%S"))
                            .to_string(),
                    );
                    logger = logger.chain(
                        fern::Dispatch::new()
                            .format(|out, message, record| {
                                out.finish(format_args!(
                                    "[{}][{}][{}] {}",
                                    chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                                    record.level(),
                                    record.target(),
                                    message
                                ))
                            })
                            .chain(
                                std::fs::OpenOptions::new()
                                    .truncate(true)
                                    .write(true)
                                    .create(true)
                                    .open(log_file_latest)?,
                            )
                            .chain(fern::log_file(log_file_time)?),
                    );
                }
            } else {
                // always append to a single log file
                let log_file = log_dir.join(format!("{}.log", self.crate_setup.application_name()));
                logger = logger.chain(
                    fern::Dispatch::new()
                        .format(|out, message, record| {
                            out.finish(format_args!(
                                "[{}][{}][{}] {}",
                                chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                                record.level(),
                                record.target(),
                                message
                            ))
                        })
                        .chain(
                            std::fs::OpenOptions::new()
                                .append(true)
                                .create(true)
                                .open(log_file)?,
                        ),
                );
            }
        }
        logger.chain(stdout_logger).apply()?;

        // do this after the logger has been applied so the logger can be used
        #[cfg(feature = "log_rotation")]
        {
            if !old_logs.is_empty() {
                rotation::clean_up_old_logs(old_logs);
            }
        }

        // hook panic handler if requested
        #[cfg(feature = "panic_handler")]
        {
            if log_panics {
                std::panic::set_hook(Box::new(logging_panic_handler));
            }
        }
        Ok(())
    }
}

fn get_log_path(base_dirs: &BaseDirs, project_dirs: &ProjectDirs) -> PathBuf {
    let dir = if cfg!(target_os = "macos") {
        base_dirs
            .home_dir()
            .join("Library")
            .join("Logs")
            .join(project_dirs.project_path())
    } else {
        project_dirs.data_local_dir().join("logs")
    };
    std::fs::create_dir_all(&dir).expect("Failed to create log dir");
    dir
}

#[cfg(feature = "panic_handler")]
pub fn logging_panic_handler(info: &std::panic::PanicInfo) {
    let location = info.location().unwrap(); // The current implementation always returns Some
    let msg = match info.payload().downcast_ref::<&'static str>() {
        Some(s) => *s,
        None => match info.payload().downcast_ref::<String>() {
            Some(s) => &s[..],
            None => "Box<Any>",
        },
    };
    let thread = std::thread::current();
    let name = thread.name().unwrap_or("<unnamed>");

    log::error!("thread '{}' panicked at '{}', {}", name, msg, location);

    let mut frame_counter = 0;

    backtrace::trace(|frame| {
        let ip = frame.ip();
        backtrace::resolve(ip, |symbol| {
            let name = symbol
                .name()
                .map(|name| name.to_string())
                .unwrap_or_else(|| "<unnamed>".into());
            let addr = symbol.addr().unwrap_or_else(std::ptr::null_mut);
            let filename = symbol.filename().map_or_else(
                || "<unknown source>".into(),
                |path| path.to_string_lossy().into_owned(),
            );
            let line_number = symbol.lineno().unwrap_or(0);

            log::error!(
                "#{}: {:?}:{} at {}:{}",
                frame_counter,
                addr,
                name,
                filename,
                line_number
            );
        });
        frame_counter += 1;
        true
    });
}