print_logger 0.2.2

Logger that print messages to stdout or stderr
Documentation
// SPDX-FileCopyrightText: 2024 Michael Picht <mipi@fsfe.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

#![doc(html_root_url = "https://docs.rs/print_logger/")]

//! A simple logger that prints messages either to stdout or stderr (depending on
//! the message type). A fixed color scheme is applied.
//!
//! ## Simple example
//!
//! ```rust
//! use log::*;
//! use regex::Regex;
//!
//! // ...
//!
//! print_logger::new()
//!     .targets_by_regex(&[Regex::new(&format!("^{}[::.+]*", module_path!())).unwrap()])
//!     .level_filter(LevelFilter::Info)
//!     .init()
//!     .unwrap();
//!
//! error!("some failure");
//!
//! // ...
//! ```
//!
//! ### Module Level Logging
//!
//! `print_logger` offers the possibility to restrict components which can log.
//! Many crates use [log](https://docs.rs/log/*/log/) but you may not
//! want their output in your application. For example
//! [hyper](https://docs.rs/hyper/*/hyper/) makes heavy use of log but
//! when your application receives `-vvvvv` to enable the `trace!()`
//! messages you don't want the output of `hyper`'s `trace!()` level.
//!
//! To support this `print_logger` includes two methods:
//!
//! 1. With `targets_by_name` a list of log targets (see
//!    <https://docs.rs/log/latest/log/macro.error.html>) can be specified.
//!    Only messages for these targets are displayed.
//!
//! 2. With `targets_by_regex` a list of regular expressions can be specified.
//!    Messages are only displayed if their target matches at least one of
//!    these expressions. In the example above only messages from the binary
//!    itself but none of its dependencies are displayed (per default the
//!    current module is used as target in log messages).
//!
//! If both lists are given, a message is displayed if its target either is in
//! `targets_by_name` or if it matches one to the expressions of
//! `targets_by_regex`.
//! Target names (i.e., per default the actual module path) are displayed at the
//! beginning of the log message if the `print_target_filter` is greater or equal
//! than the specified `level_filter`.

use log::{Level, Log, Metadata, Record};
use regex::Regex;
use std::{
    fmt::{self, Debug},
    io::Write,
};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};

pub use log::LevelFilter;

/// Print logger data structure
pub struct PrintLogger {
    level_filter: LevelFilter,
    targets_by_name: Option<Vec<String>>,
    targets_by_regex: Option<Vec<Regex>>,
    print_targets_filter: LevelFilter,
}

impl Debug for PrintLogger {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut builder = f.debug_struct("PrintLogger");
        builder.field("level_filter", &self.level_filter);
        builder
            .field("targets_by_name", &self.targets_by_name)
            .field("targets_by_regex", &self.targets_by_regex)
            .field("print_targets_filter", &self.print_targets_filter)
            .finish()
    }
}

impl Clone for PrintLogger {
    fn clone(&self) -> PrintLogger {
        PrintLogger {
            targets_by_name: self.targets_by_name.clone(),
            targets_by_regex: self.targets_by_regex.clone(),
            ..*self
        }
    }
}

impl Default for PrintLogger {
    fn default() -> Self {
        Self::new()
    }
}

impl Log for PrintLogger {
    /// Decide if a log messages will be printed. That is the case if ...
    /// - the level filter of the logger is higher than the message level
    /// - the message target is relevant
    fn enabled(&self, metadata: &Metadata) -> bool {
        if metadata.level() > self.level_filter {
            return false;
        };

        if let Some(targets_by_name) = &self.targets_by_name {
            return targets_by_name
                .binary_search(&metadata.target().to_string())
                .is_ok();
        };

        if let Some(targets_by_regex) = &self.targets_by_regex {
            return targets_by_regex
                .iter()
                .any(|r| r.is_match(metadata.target()));
        };

        self.targets_by_name.is_none() || self.targets_by_regex.is_none()
    }

    /// Log a message
    fn log(&self, record: &Record) {
        // Nothing to do if message is not relevant
        if !self.enabled(record.metadata()) {
            return;
        }

        // Assemble message text for printing
        let text = format!(
            "{}{}",
            // Add target to message if the level filter for targets is lower
            // than the overall level filter of the logger. If the level filter
            // for targets is off, targets will not be printed
            if self.print_targets_filter != LevelFilter::Off
                && self.level_filter >= self.print_targets_filter
            {
                format!("[{}] ", record.metadata().target())
            } else {
                "".to_string()
            },
            record.args()
        );

        // Set output stream. Error messages are printed to stderr, all other
        // messages to stdout. Color choice is done automatically depending on
        // the platform/environment
        let mut out = match record.metadata().level() {
            Level::Error => StandardStream::stderr(ColorChoice::Auto),
            _ => StandardStream::stdout(ColorChoice::Auto),
        };

        // Specifiy color and text decoration
        let mut spec = ColorSpec::new();
        out.set_color(match record.metadata().level() {
            Level::Error => spec.set_fg(Some(Color::Red)).set_bold(true),
            Level::Warn => spec.set_fg(Some(Color::Yellow)).set_bold(true),
            Level::Info => spec.set_fg(Some(Color::White)).set_bold(true),
            Level::Debug => spec.set_fg(Some(Color::White)),
            Level::Trace => spec.set_fg(Some(Color::Cyan)),
        })
        .unwrap_or_else(|_| panic!("Cannot set text color"));

        // Print message
        _ = writeln!(&mut out, "{}", text);
    }

    fn flush(&self) {}
}

impl PrintLogger {
    /// Create new logger
    fn new() -> PrintLogger {
        PrintLogger {
            level_filter: LevelFilter::Error,
            targets_by_name: None,
            targets_by_regex: None,
            print_targets_filter: LevelFilter::Off,
        }
    }

    /// Activate the logger
    pub fn init(&mut self) -> Result<(), log::SetLoggerError> {
        log::set_max_level(self.level_filter);
        log::set_boxed_logger(Box::new(self.clone()))
    }

    /// Set the maximum level of messages that will be displayed
    pub fn level_filter<L>(&mut self, level_filter: L) -> &mut PrintLogger
    where
        L: Into<LevelFilter>,
    {
        self.level_filter = level_filter.into();
        self
    }

    /// Set the minimum level for printing targets in log messages. This level
    /// compared to the overall log level that print_logger was initialized with.
    /// If that level is equal or greater than the print_targets_filter, targets
    /// are displayed
    pub fn print_targets_filter<L>(&mut self, print_targets_filter: L) -> &mut PrintLogger
    where
        L: Into<LevelFilter>,
    {
        self.print_targets_filter = print_targets_filter.into();
        self
    }

    /// Add target names to the array of relevant targets.
    /// Note: Some(targets_by_name) is expected to be sorted!
    pub fn targets_by_name(&mut self, names: &[String]) -> &mut PrintLogger {
        if names.is_empty() {
            return self;
        }

        if self.targets_by_name.is_none() {
            self.targets_by_name = Some(Vec::new());
        }

        let targets_by_name = self.targets_by_name.as_mut().unwrap();

        for n in names {
            if let Err(i) = targets_by_name.binary_search(n) {
                targets_by_name.insert(i, n.to_string());
            }
        }

        self
    }

    /// Add regular expressions to the array of regex's that determines if activate
    /// target is relevant
    pub fn targets_by_regex(&mut self, rs: &[Regex]) -> &mut PrintLogger {
        if rs.is_empty() {
            return self;
        }

        if self.targets_by_regex.is_none() {
            self.targets_by_regex = Some(Vec::new());
        }

        self.targets_by_regex
            .as_mut()
            .unwrap()
            .extend_from_slice(rs);

        self
    }
}

/// Creates a new logger
pub fn new() -> PrintLogger {
    PrintLogger::new()
}