logger2 0.1.5

A modern, feature-rich replacement for the classic `logger` CLI: syslog (local/remote, RFC3164 & RFC5424), truecolor hex output, emoji, JSON, and a fully configurable TOML config file.
Documentation
use crate::error::{Logger2Error, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

/// Syslog severities (RFC 5424 section 6.2.1), plus a non-standard `Trace`
/// which is treated as `Debug` severity for wire purposes but keeps its own
/// color/emoji identity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Level {
    Emergency,
    Alert,
    Critical,
    Error,
    Warning,
    Notice,
    Info,
    Debug,
    Trace,
}

impl Level {
    pub const ALL: [Level; 9] = [
        Level::Emergency,
        Level::Alert,
        Level::Critical,
        Level::Error,
        Level::Warning,
        Level::Notice,
        Level::Info,
        Level::Debug,
        Level::Trace,
    ];

    /// Numeric syslog severity (0-7). `Trace` maps to `Debug` (7).
    pub fn severity(self) -> u8 {
        match self {
            Level::Emergency => 0,
            Level::Alert => 1,
            Level::Critical => 2,
            Level::Error => 3,
            Level::Warning => 4,
            Level::Notice => 5,
            Level::Info => 6,
            Level::Debug | Level::Trace => 7,
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Level::Emergency => "emergency",
            Level::Alert => "alert",
            Level::Critical => "critical",
            Level::Error => "error",
            Level::Warning => "warning",
            Level::Notice => "notice",
            Level::Info => "info",
            Level::Debug => "debug",
            Level::Trace => "trace",
        }
    }

    /// Short 4-6 char label used in aligned console output, e.g. `WARN`.
    pub fn label(self) -> &'static str {
        match self {
            Level::Emergency => "EMERG",
            Level::Alert => "ALERT",
            Level::Critical => "CRIT",
            Level::Error => "ERROR",
            Level::Warning => "WARN",
            Level::Notice => "NOTICE",
            Level::Info => "INFO",
            Level::Debug => "DEBUG",
            Level::Trace => "TRACE",
        }
    }

    pub fn from_severity(sev: u8) -> Level {
        match sev {
            0 => Level::Emergency,
            1 => Level::Alert,
            2 => Level::Critical,
            3 => Level::Error,
            4 => Level::Warning,
            5 => Level::Notice,
            6 => Level::Info,
            _ => Level::Debug,
        }
    }
}

impl fmt::Display for Level {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl FromStr for Level {
    type Err = Logger2Error;

    fn from_str(s: &str) -> Result<Self> {
        match s.to_ascii_lowercase().as_str() {
            "emerg" | "emergency" | "panic" => Ok(Level::Emergency),
            "alert" => Ok(Level::Alert),
            "crit" | "critical" => Ok(Level::Critical),
            "err" | "error" => Ok(Level::Error),
            "warn" | "warning" => Ok(Level::Warning),
            "notice" => Ok(Level::Notice),
            "info" | "information" => Ok(Level::Info),
            "debug" => Ok(Level::Debug),
            "trace" => Ok(Level::Trace),
            other => Err(Logger2Error::UnknownLevel(other.to_string())),
        }
    }
}

/// Syslog facilities (RFC 5424 section 6.2.1).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Facility {
    Kern,
    User,
    Mail,
    Daemon,
    Auth,
    Syslog,
    Lpr,
    News,
    Uucp,
    Cron,
    AuthPriv,
    Ftp,
    Ntp,
    Security,
    Console,
    SolarisCron,
    Local0,
    Local1,
    Local2,
    Local3,
    Local4,
    Local5,
    Local6,
    Local7,
}

impl Facility {
    pub fn code(self) -> u8 {
        match self {
            Facility::Kern => 0,
            Facility::User => 1,
            Facility::Mail => 2,
            Facility::Daemon => 3,
            Facility::Auth => 4,
            Facility::Syslog => 5,
            Facility::Lpr => 6,
            Facility::News => 7,
            Facility::Uucp => 8,
            Facility::Cron => 9,
            Facility::AuthPriv => 10,
            Facility::Ftp => 11,
            Facility::Ntp => 12,
            Facility::Security => 13,
            Facility::Console => 14,
            Facility::SolarisCron => 15,
            Facility::Local0 => 16,
            Facility::Local1 => 17,
            Facility::Local2 => 18,
            Facility::Local3 => 19,
            Facility::Local4 => 20,
            Facility::Local5 => 21,
            Facility::Local6 => 22,
            Facility::Local7 => 23,
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Facility::Kern => "kern",
            Facility::User => "user",
            Facility::Mail => "mail",
            Facility::Daemon => "daemon",
            Facility::Auth => "auth",
            Facility::Syslog => "syslog",
            Facility::Lpr => "lpr",
            Facility::News => "news",
            Facility::Uucp => "uucp",
            Facility::Cron => "cron",
            Facility::AuthPriv => "authpriv",
            Facility::Ftp => "ftp",
            Facility::Ntp => "ntp",
            Facility::Security => "security",
            Facility::Console => "console",
            Facility::SolarisCron => "solaris-cron",
            Facility::Local0 => "local0",
            Facility::Local1 => "local1",
            Facility::Local2 => "local2",
            Facility::Local3 => "local3",
            Facility::Local4 => "local4",
            Facility::Local5 => "local5",
            Facility::Local6 => "local6",
            Facility::Local7 => "local7",
        }
    }
}

impl fmt::Display for Facility {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl FromStr for Facility {
    type Err = Logger2Error;

    fn from_str(s: &str) -> Result<Self> {
        match s.to_ascii_lowercase().replace('_', "-").as_str() {
            "kern" => Ok(Facility::Kern),
            "user" => Ok(Facility::User),
            "mail" => Ok(Facility::Mail),
            "daemon" => Ok(Facility::Daemon),
            "auth" => Ok(Facility::Auth),
            "syslog" => Ok(Facility::Syslog),
            "lpr" => Ok(Facility::Lpr),
            "news" => Ok(Facility::News),
            "uucp" => Ok(Facility::Uucp),
            "cron" => Ok(Facility::Cron),
            "authpriv" | "auth-priv" => Ok(Facility::AuthPriv),
            "ftp" => Ok(Facility::Ftp),
            "ntp" => Ok(Facility::Ntp),
            "security" => Ok(Facility::Security),
            "console" => Ok(Facility::Console),
            "solaris-cron" => Ok(Facility::SolarisCron),
            "local0" => Ok(Facility::Local0),
            "local1" => Ok(Facility::Local1),
            "local2" => Ok(Facility::Local2),
            "local3" => Ok(Facility::Local3),
            "local4" => Ok(Facility::Local4),
            "local5" => Ok(Facility::Local5),
            "local6" => Ok(Facility::Local6),
            "local7" => Ok(Facility::Local7),
            other => Err(Logger2Error::UnknownFacility(other.to_string())),
        }
    }
}

/// A parsed `facility.level` priority pair, e.g. `user.notice`.
#[derive(Debug, Clone, Copy)]
pub struct Priority {
    pub facility: Facility,
    pub level: Level,
}

impl Priority {
    /// The PRI value used in RFC3164/RFC5424 framing: `facility*8 + severity`.
    pub fn pri(self) -> u8 {
        self.facility.code() * 8 + self.level.severity()
    }
}

impl fmt::Display for Priority {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}.{}", self.facility, self.level)
    }
}

impl FromStr for Priority {
    type Err = Logger2Error;

    fn from_str(s: &str) -> Result<Self> {
        let (fac, lvl) = s
            .split_once('.')
            .ok_or_else(|| Logger2Error::InvalidPriority(s.to_string()))?;
        let facility = Facility::from_str(fac)
            .map_err(|_| Logger2Error::InvalidPriority(s.to_string()))?;
        let level =
            Level::from_str(lvl).map_err(|_| Logger2Error::InvalidPriority(s.to_string()))?;
        Ok(Priority { facility, level })
    }
}