packxel-utils 0.3.1

A bunch of packaging utilities
Documentation
//! Logging module for Packxel
//!
//! ## Default Logger
//! ```
//! use packxel_utils::initializer::Initializer;
//! let initializer = Initializer::init("package-name", "0.0.1");
//! PackxelLogger::init(initializer);
//! // Run with RUST_LOG=debug cargo run
//! ```
//!
//! ## With Builder
//!
//! ```
//! use packxel_utils::initializer::Initializer;
//! use packxel_utils::logging::PackxelLoggerBuilder;
//!
//! let initializer = Initializer::init("package-name", "0.0.1");
//! let builder = PackxelLoggerBuilder::new(initializer)
//!     .with_default_log_level()
//!     .with_file_writer(false);
//!     .build()
//! ```

use std::{env::temp_dir, fs::File, io::Write, str::FromStr};

use colored::{ColoredString, Colorize};
use log::{warn, Level};

use crate::initializer::PackxelInitializer;

/// FileWriter. Will be used for log rotation.
/// Once flush is added.
#[derive(Debug)]
struct FileWriter {
    file: File,
}

#[derive(Debug, Default)]
/// Base logger for Packxel
pub struct PackxelLogger {
    log_level: Option<Level>,
    print_lines: bool,
    file_writer: Option<FileWriter>,
    log_file_path: String,
}

#[derive(Debug)]
/// Builder mechanism for PackxelLogger
/// ## Examples
///
/// ```
/// let logger = PackxelLoggerBuilder::new(initializer)
///     .with_default_log_level()
///     .build()
/// ```
pub struct PackxelLoggerBuilder {
    log_level: Option<Level>,
    print_lines: bool,
    initializer: Option<PackxelInitializer>,
    file_writer: bool,
}

impl PackxelLoggerBuilder {
    fn new(initializer: Option<PackxelInitializer>) -> Self {
        Self {
            log_level: None,
            print_lines: false,
            initializer,
            file_writer: true,
        }
    }

    fn with_default_log_level(mut self) -> PackxelLoggerBuilder {
        let log_level = get_level_from_env();
        self.log_level = log_level;
        self
    }

    pub fn with_log_level(mut self, log_level: Level) -> PackxelLoggerBuilder {
        self.log_level = Some(log_level);
        self
    }

    pub fn with_print_lines(mut self, print_lines: bool) -> PackxelLoggerBuilder {
        self.print_lines = print_lines;
        self
    }

    pub fn with_file_writer(mut self, file_writer: bool) -> PackxelLoggerBuilder {
        self.file_writer = file_writer;
        self
    }

    fn try_build(self) -> std::io::Result<PackxelLogger> {
        let file_writer;
        let mut log_file_path;
        if self.initializer.is_some() {
            let initializer = self.initializer.unwrap();
            log_file_path = initializer.get_log_file();
        } else {
            log_file_path = temp_dir();
            log_file_path.push("data.log");
        }

        let file = File::options()
            .append(true)
            .create(true)
            .open(&log_file_path)
            .expect("Unable to open file");

        let log_file_path = log_file_path
            .to_str()
            .expect("Failed to get log file path from path buffer")
            .to_string();

        if self.file_writer {
            file_writer = Some(FileWriter { file })
        } else {
            file_writer = None
        }

        Ok(PackxelLogger {
            log_level: self.log_level,
            print_lines: self.print_lines,
            file_writer,
            log_file_path,
        })
    }

    fn build(self) -> PackxelLogger {
        self.try_build().expect("Failed to build logger.")
    }
}

impl PackxelLogger {
    /// Logger with builder capacity.
    ///
    /// Calls `log::set_boxed_logger` which an be only called once.
    /// ```
    /// use packxel_utils::initializer::Initializer;
    /// use packxel_utils::logging::PackxelLoggerBuilder;
    ///
    /// let initializer = Initializer::init("package-name", "0.0.1");
    /// let builder = PackxelLoggerBuilder::new(initializer)
    ///     .with_default_log_level()
    ///     .with_file_writer(false);
    ///     .build();
    ///
    /// PackxelLogger::init_with_builder(builder);
    ///
    /// log::info!("Hello World");
    /// ```
    pub fn init_with_builder(builder: PackxelLoggerBuilder) -> Result<(), log::SetLoggerError> {
        log::set_boxed_logger(Box::new(builder.build()))
            .map(|()| log::set_max_level(log::LevelFilter::Trace))
    }

    /// Default logger. Can be only called once.
    ///
    /// Calls `log::set_boxed_logger` which an be only called once.
    /// 
    /// ```
    /// use packxel_utils::initializer::Initializer;
    /// use packxel_utils::logging::PackxelLoggerBuilder;
    ///
    /// let initializer = Initializer::init("package-name", "0.0.1");
    /// 
    /// PackxelLogger::init(initializer);
    ///
    /// log::info!("Hello World");
    /// ```
    pub fn init(initializer: PackxelInitializer) -> Result<(), log::SetLoggerError> {
        log::set_boxed_logger(Box::new(
            PackxelLoggerBuilder::new(Some(initializer))
                .with_default_log_level()
                .build(),
        ))
        .map(|()| log::set_max_level(log::LevelFilter::Trace))
    }

    fn get_colored_string(level: Level) -> ColoredString {
        match level {
            Level::Error => format!("{} ", level).as_str().red(),
            Level::Warn => format!("{}  ", level).as_str().yellow(),
            Level::Info => format!("{}  ", level).as_str().green(),
            Level::Debug => format!("{} ", level).as_str().blue(),
            Level::Trace => format!("{} ", level).as_str().clear(),
        }
    }
}

fn get_level_from_env() -> Option<Level> {
    let log_level = std::env::var("RUST_LOG").unwrap_or("".to_string());
    let log_level = Level::from_str(&log_level);
    let log_level = match log_level {
        Ok(log_level) => Some(log_level),
        Err(_) => None,
    };
    log_level
}

fn rotate_file<S: AsRef<str>>(buffer: &mut &File, log_file_path: S) -> std::io::Result<bool> {
    let file_metadata = buffer.metadata()?;

    let file_size = file_metadata.len();

    let one_mb = 1000 * 1000;

    if file_size > 9 * one_mb {
        warn!("Log file is getting large. Will be rotating file.")
    }

    if file_size > 10 * one_mb {
        let new_file_name = format!(
            "{}-{}.old",
            chrono::Local::now().timestamp(),
            log_file_path.as_ref()
        );
        std::fs::copy(log_file_path.as_ref(), new_file_name)?;

        {
            todo!("Seek to start of file")
            // buffer.set_len(0)?;
            // buffer.seek(std::io::SeekFrom::Start(0))?;
        }
    }

    Ok(false)
}

impl log::Log for PackxelLogger {
    /// _Check if enabled. Used inside crate `log` macros._
    fn enabled(&self, metadata: &log::Metadata) -> bool {
        if self.log_level.is_none() {
            return false;
        }
        return metadata.level() <= self.log_level.unwrap();
    }

    /// Push to buffer and print to stdout.
    fn log(&self, record: &log::Record) {
        let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
        let level = Self::get_colored_string(record.level());

        if self.enabled(record.metadata()) {
            if self.file_writer.is_some() {
                let file_writer = self.file_writer.as_ref().unwrap();

                let output = format!(
                    "{} {} {}\n",
                    level.clone().clear(),
                    timestamp,
                    record.args()
                );

                let mut file = &file_writer.file;

                file.write(output.as_bytes())
                    .expect("Unable to write to file");

                match rotate_file(&mut &file_writer.file, &self.log_file_path) {
                    Ok(rotated) => {
                        if rotated {
                            println!("Rotated");
                        }
                    }
                    Err(_) => {
                        log::error!("Failed to rotate log file");
                    }
                };
            }

            print!("{}", level);
            print!("{}\t", timestamp);

            if record.level() == Level::Trace || self.print_lines {
                println!(
                    "{}:{} - {}",
                    record.file().unwrap_or("unknown"),
                    record.line().unwrap_or(0),
                    record.args(),
                );
            } else {
                println!("{}", record.args());
            }
        }
    }

    fn flush(&self) {}
}