mlog 0.3.1

A logging implementation in Rust.
Documentation
//! A logging implementation in Rust.
//!
//! # Example
//!
//! ```rust
//! #[macro_use]
//! extern crate log;
//!
//! extern crate chrono;
//! extern crate mlog;
//!
//! use mlog::Logger;
//!
//! fn main() {
//!     Logger::new()
//!         .for_module("mlog_example::*", log::LevelFilter::Off)
//!         .format(|record| {
//!             format!(
//!                 "[{}] {}: {}",
//!                 chrono::Local::now()
//!                     .format("%H:%M:%S"),
//!                  record.level(),
//!                  record.args()
//!             )
//!         })
//!         .set_default_level(log::LevelFilter::Info)
//!         .ready()
//!         .unwrap();
//!
//!     info!("hello, world!");
//! }
//! ```

extern crate hashbrown;
extern crate log;

use hashbrown::{HashMap, hash_map::Entry};
use log::{LevelFilter, Log, Metadata, Record};

use std::borrow::Cow;
use std::fs::{File, OpenOptions};
use std::io::Write;

type Formatter = Fn(&log::Record) -> String + Sync + Send + 'static;

/// Returns the name of the parent module of `target`.
fn get_parent_module(target: &str) -> &str {
    let index = target.rfind("::")
        .unwrap_or(target.len());

    &target[..index]
}

/// Prepares the log file.
fn prepare_log_file(path: String) -> File {
    let log_file = OpenOptions::new()
        .append(true)
        .create(true)
        .open(path);

    match log_file {
        Ok(f) => f,
        Err(e) => panic!("Failed to open log file: {}", e)
    }
}

/// The stream that log messages will be sent to.
pub enum Output {
    Stdout,
    Stderr,
    File(String)
}

pub struct Logger {
    default_level: LevelFilter,
    filters: HashMap<Cow<'static, str>, LevelFilter>,
    formatter: Option<Box<Formatter>>,
    output_dst: Output
}

impl Logger {
    /// Initializes the `Logger`.
    pub fn new() -> Self {
        Logger {
            default_level: LevelFilter::Info,
            filters: HashMap::new(),
            formatter: None,
            output_dst: Output::Stdout
        }
    }

    /// Sets the logging level for `module`.
    pub fn for_module<T: Into<Cow<'static, str>>>(mut self, module: T, level: LevelFilter) -> Self {
        let module = module.into();

        match self.filters.entry(module) {
            Entry::Occupied(mut o) => {
                o.insert(level);
            },
            Entry::Vacant(v) => {
                v.insert(level);
            }
        };

        self
    }

    /// Sets the log formatter for this logger.
    pub fn format<F>(mut self, formatter: F) -> Self where
        F: Fn(&log::Record) -> String + Sync + Send + 'static  {
        self.formatter = Some(Box::new(formatter));

        self
    }

    /// Returns the level filter that corresponds to the maximum log level of this logger.
    pub fn get_max_level(&self) -> LevelFilter {
        self.filters.values()
            .fold(self.default_level, |acc, e| {
                acc.max(*e)
            })
    }

    /// Sets the global logger to this logger.
     pub fn ready(self) -> Result<(), log::SetLoggerError> {
        let max_level = self.get_max_level();
        let log = Box::new(self);

        log::set_boxed_logger(log)?;
        log::set_max_level(max_level);

        Ok(())
    }

    /// Returns the reference to the level filter of the module `target`.
    fn resolve_module_level(&self, target: &str) -> &LevelFilter {
        if self.filters.contains_key(target) {
            self.filters.get(target)
                .unwrap_or(&self.default_level)
        } else {
            let key = format!("{}::*", get_parent_module(target));

            // check if `target` is not the root module
            if target.contains("::") && self.filters.contains_key(&key[..]) {
                self.filters.get(&key[..])
                    .unwrap_or(&self.default_level)
            } else {
                &self.default_level
            }
        }
    }

    /// Sets the default log level for this logger.
    pub fn set_default_level(mut self, level: LevelFilter) -> Self {
        self.default_level = level;

        self
    }
}

impl Log for Logger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        let metadata_filter = metadata.level()
            .to_level_filter();
        let target_filter = self.resolve_module_level(metadata.target());
        &metadata_filter <= target_filter
    }

    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            // default log format
            let mut message = format!("[{}] {}", record.level(), record.args());

            if let Some(formatter) = &self.formatter {
                message = format!("{}", formatter(record));
            }

            match &self.output_dst {
                Output::Stderr => {
                    eprintln!("{}", message);
                },
                Output::Stdout => {
                    println!("{}", message);
                },
                Output::File(path) => {
                    let mut file = prepare_log_file(path.to_owned());

                    if let Err(e) = writeln!(file, "{}", message) {
                        panic!("Failed to write to log file: {}", e);
                    }

                    self.flush();
                }
            }


        }
    }

    fn flush(&self) {
        // "Note that stdout is frequently line-buffered by default ..."
        if let Output::File(path) = &self.output_dst {
            let mut file = prepare_log_file(path.to_owned());

            if let Err(e) = file.flush() {
                panic!("Failed to flush the log file: {}", e);
            }
        }
    }
}