censer 0.1.0

Beautiful, structured logging for the terminal ⚒️
Documentation
//! Logger implementation.

use crate::level::Level;
use crate::style::LogStyle;
use chrono::Local;
use glyphs::style;
use std::fmt::Display;
use std::io::{self, Write};

/// A styled logger.
#[derive(Debug, Clone)]
pub struct Logger {
    style: LogStyle,
    min_level: Level,
    prefix: Option<String>,
}

impl Logger {
    /// Create a new logger.
    pub fn new() -> Self {
        Self {
            style: LogStyle::default(),
            min_level: Level::Debug,
            prefix: None,
        }
    }

    /// Set the style.
    #[must_use]
    pub fn style(mut self, style: LogStyle) -> Self {
        self.style = style;
        self
    }

    /// Enable timestamps.
    #[must_use]
    pub fn with_timestamp(mut self) -> Self {
        self.style.timestamp = true;
        self
    }

    /// Enable icons.
    #[must_use]
    pub fn with_icons(mut self) -> Self {
        self.style.show_icons = true;
        self
    }

    /// Set minimum log level.
    #[must_use]
    pub fn min_level(mut self, level: Level) -> Self {
        self.min_level = level;
        self
    }

    /// Set a prefix for all messages.
    #[must_use]
    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
        self.prefix = Some(prefix.into());
        self
    }

    /// Log at a specific level.
    pub fn log(&self, level: Level, message: &str) {
        if level < self.min_level {
            return;
        }
        self.write_log(level, message, &[]);
    }

    /// Log with key-value pairs.
    pub fn log_kv(&self, level: Level, message: &str, kvs: &[(&str, &dyn Display)]) {
        if level < self.min_level {
            return;
        }
        self.write_log(level, message, kvs);
    }

    fn write_log(&self, level: Level, message: &str, kvs: &[(&str, &dyn Display)]) {
        let mut output = String::new();

        // Timestamp
        if self.style.timestamp {
            let timestamp = Local::now().format(&self.style.timestamp_format).to_string();
            output.push_str(&style(&timestamp).fg(self.style.timestamp_color).to_string());
            output.push(' ');
        }

        // Level
        if self.style.show_level {
            let level_str = if self.style.short_level {
                level.short_name()
            } else {
                level.name()
            };

            if self.style.show_icons {
                output.push_str(level.icon());
                output.push(' ');
            }

            output.push_str(&style(level_str).fg(level.color()).bold().to_string());
            output.push(' ');
        }

        // Prefix
        if let Some(ref prefix) = self.prefix {
            output.push_str(&style(format!("[{}]", prefix)).dim().to_string());
            output.push(' ');
        }

        // Message
        output.push_str(&style(message).fg(self.style.message_color).to_string());

        // Key-value pairs
        if !kvs.is_empty() {
            output.push(' ');
            let kv_parts: Vec<String> = kvs
                .iter()
                .map(|(k, v)| {
                    format!(
                        "{}{}{}",
                        style(*k).fg(self.style.key_color),
                        self.style.kv_separator,
                        style(v.to_string()).fg(self.style.value_color)
                    )
                })
                .collect();
            output.push_str(&kv_parts.join(&self.style.separator));
        }

        // Write to stderr
        let mut stderr = io::stderr().lock();
        writeln!(stderr, "{}", output).ok();
    }

    /// Log at debug level.
    pub fn debug(&self, message: &str) {
        self.log(Level::Debug, message);
    }

    /// Log at debug level with key-value pairs.
    pub fn debug_kv(&self, message: &str, kvs: &[(&str, &dyn Display)]) {
        self.log_kv(Level::Debug, message, kvs);
    }

    /// Log at info level.
    pub fn info(&self, message: &str) {
        self.log(Level::Info, message);
    }

    /// Log at info level with key-value pairs.
    pub fn info_kv(&self, message: &str, kvs: &[(&str, &dyn Display)]) {
        self.log_kv(Level::Info, message, kvs);
    }

    /// Log at warn level.
    pub fn warn(&self, message: &str) {
        self.log(Level::Warn, message);
    }

    /// Log at warn level with key-value pairs.
    pub fn warn_kv(&self, message: &str, kvs: &[(&str, &dyn Display)]) {
        self.log_kv(Level::Warn, message, kvs);
    }

    /// Log at error level.
    pub fn error(&self, message: &str) {
        self.log(Level::Error, message);
    }

    /// Log at error level with key-value pairs.
    pub fn error_kv(&self, message: &str, kvs: &[(&str, &dyn Display)]) {
        self.log_kv(Level::Error, message, kvs);
    }

    /// Log at fatal level.
    pub fn fatal(&self, message: &str) {
        self.log(Level::Fatal, message);
    }

    /// Log at fatal level with key-value pairs.
    pub fn fatal_kv(&self, message: &str, kvs: &[(&str, &dyn Display)]) {
        self.log_kv(Level::Fatal, message, kvs);
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_logger_creation() {
        let logger = Logger::new().with_timestamp().with_icons();
        assert!(logger.style.timestamp);
        assert!(logger.style.show_icons);
    }
}