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::level::Priority;
use chrono::Local;

/// Formats a message using the legacy BSD syslog format (RFC 3164):
/// `<PRI>Mmm dd hh:mm:ss hostname tag[pid]: message`
pub fn rfc3164(priority: Priority, hostname: &str, tag: &str, pid: Option<u32>, message: &str) -> String {
    let now = Local::now();
    let timestamp = now.format("%b %e %H:%M:%S");
    let tag_field = match pid {
        Some(p) => format!("{}[{}]", tag, p),
        None => tag.to_string(),
    };
    format!(
        "<{}>{} {} {}: {}",
        priority.pri(),
        timestamp,
        hostname,
        tag_field,
        message
    )
}

/// Formats a message using RFC 5424:
/// `<PRI>1 timestamp hostname app-name procid msgid structured-data msg`
pub fn rfc5424(
    priority: Priority,
    hostname: &str,
    tag: &str,
    pid: Option<u32>,
    msgid: Option<&str>,
    structured_data: &[(String, String)],
    message: &str,
) -> String {
    let now = Local::now();
    let timestamp = now.to_rfc3339_opts(chrono::SecondsFormat::Millis, false);
    let procid = pid.map(|p| p.to_string()).unwrap_or_else(|| "-".to_string());
    let msgid = msgid.unwrap_or("-");

    let sd = if structured_data.is_empty() {
        "-".to_string()
    } else {
        let pairs: Vec<String> = structured_data
            .iter()
            .map(|(k, v)| format!("{}=\"{}\"", sanitize_sd_key(k), escape_sd_value(v)))
            .collect();
        format!("[logger2@32473 {}]", pairs.join(" "))
    };

    format!(
        "<{}>1 {} {} {} {} {} {} {}",
        priority.pri(),
        timestamp,
        hostname,
        tag,
        procid,
        msgid,
        sd,
        message
    )
}

fn sanitize_sd_key(k: &str) -> String {
    k.chars()
        .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
        .collect()
}

fn escape_sd_value(v: &str) -> String {
    v.replace('\\', "\\\\").replace('"', "\\\"").replace(']', "\\]")
}

/// Frames a message for RFC 6587 octet-counting over TCP: `LEN SPACE MSG`.
pub fn octet_count_frame(message: &str) -> String {
    format!("{} {}", message.len(), message)
}

/// Frames a message for non-transparent-framing over TCP, terminated by LF.
pub fn newline_frame(message: &str) -> String {
    format!("{}\n", message)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::level::{Facility, Level};

    #[test]
    fn rfc3164_basic_shape() {
        let pri = Priority {
            facility: Facility::User,
            level: Level::Notice,
        };
        let out = rfc3164(pri, "myhost", "mytag", Some(1234), "hello world");
        assert!(out.starts_with("<13>"));
        assert!(out.contains("myhost"));
        assert!(out.contains("mytag[1234]:"));
        assert!(out.ends_with("hello world"));
    }

    #[test]
    fn rfc5424_basic_shape() {
        let pri = Priority {
            facility: Facility::Local0,
            level: Level::Error,
        };
        let out = rfc5424(pri, "myhost", "mytag", Some(42), None, &[], "boom");
        assert!(out.starts_with("<131>1 "));
        assert!(out.contains("myhost"));
        assert!(out.contains("mytag"));
        assert!(out.ends_with("boom"));
    }
}