sozu-command-lib 2.0.1

configuration library to command a sozu instance
Documentation
use std::fmt;

use crate::{
    AsStr,
    logging::{
        EndpointRecord, FullTags, LogContext, LogDuration, LogLevel, LogMessage, LoggerBackend,
        Rfc3339Time,
    },
};

impl LogLevel {
    pub const fn as_str(&self, access: bool, colored: bool) -> &'static str {
        match (self, access, colored) {
            (LogLevel::Error, false, false) => "ERROR",
            (LogLevel::Warn, false, false) => "WARN ",
            (LogLevel::Info, false, false) => "INFO ",
            (LogLevel::Debug, false, false) => "DEBUG",
            (LogLevel::Trace, false, false) => "TRACE",

            (LogLevel::Error, false, true) => "\x1b[;31;1mERROR",
            (LogLevel::Warn, false, true) => "\x1b[;33;1mWARN ",
            (LogLevel::Info, false, true) => "\x1b[;32;1mINFO ",
            (LogLevel::Debug, false, true) => "\x1b[;36mDEBUG",
            (LogLevel::Trace, false, true) => "\x1b[;90mTRACE",

            (LogLevel::Error, true, false) => "ERROR-ACCESS",
            (LogLevel::Info, true, false) => "INFO-ACCESS ",
            (_, true, false) => "???",

            (LogLevel::Error, true, true) => "\x1b[;35;1mERROR-ACCESS",
            (LogLevel::Info, true, true) => "\x1b[;35;1mINFO-ACCESS ",
            (_, true, true) => "\x1b[;35;1m???",
        }
    }
}

impl AsRef<str> for LoggerBackend {
    fn as_ref(&self) -> &str {
        match self {
            LoggerBackend::Stdout(_) => "stdout",
            LoggerBackend::Unix(_) => "UNIX socket",
            LoggerBackend::Udp(_, _) => "UDP socket",
            LoggerBackend::Tcp(_) => "TCP socket",
            LoggerBackend::File(_) => "file",
        }
    }
}

impl fmt::Display for Rfc3339Time {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        let t = self.inner;
        write!(
            f,
            "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
            t.year(),
            t.month() as u8,
            t.day(),
            t.hour(),
            t.minute(),
            t.second(),
            t.microsecond()
        )
    }
}

impl fmt::Display for LogMessage<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.0 {
            Some(message) => write!(f, " | {message}"),
            None => Ok(()),
        }
    }
}

impl fmt::Display for LogDuration {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.0 {
            None => write!(f, "-"),
            Some(duration) => {
                let secs = duration.as_secs();
                if secs >= 10 {
                    return write!(f, "{secs}s");
                }

                let ms = duration.as_millis();
                if ms < 10 {
                    let us = duration.as_micros();
                    if us >= 10 {
                        return write!(f, "{us}μs");
                    }

                    let ns = duration.as_nanos();
                    return write!(f, "{ns}ns");
                }

                write!(f, "{ms}ms")
            }
        }
    }
}

impl fmt::Display for LogContext<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{} ", self.session_id)?;
        match self.request_id {
            Some(id) => write!(f, "{id}")?,
            None => f.write_str("-")?,
        }
        write!(
            f,
            " {} {}]",
            self.cluster_id.unwrap_or("-"),
            self.backend_id.unwrap_or("-")
        )
    }
}

impl fmt::Display for EndpointRecord<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Http {
                method,
                authority,
                path,
                status,
                ..
            } => write!(
                f,
                "{} {} {} {}",
                authority.as_str_or("-"),
                method.as_str_or("-"),
                path.as_str_or("-"),
                display_status(*status, f.alternate()),
            ),
            Self::Tcp => {
                write!(f, "-")
            }
        }
    }
}

fn display_status(status: Option<u16>, pretty: bool) -> String {
    match (status, pretty) {
        (Some(s @ 200..=299), true) => format!("\x1b[32m{s}"),
        (Some(s @ 300..=399), true) => format!("\x1b[34m{s}"),
        (Some(s @ 400..=499), true) => format!("\x1b[33m{s}"),
        (Some(s @ 500..=599), true) => format!("\x1b[31m{s}"),
        (Some(s), _) => s.to_string(),
        (None, _) => "-".to_string(),
    }
}

impl fmt::Display for FullTags<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match (self.concatenated, self.user_agent) {
            (None, None) => Ok(()),
            (Some(tags), None) => write!(f, "{tags}"),
            (Some(tags), Some(ua)) if !tags.is_empty() => {
                write!(f, "{tags}, user-agent={}", prepare_user_agent(ua))
            }
            (_, Some(ua)) => write!(f, "user-agent={}", prepare_user_agent(ua)),
        }
    }
}

fn prepare_user_agent(user_agent: &str) -> String {
    user_agent
        .replace(' ', "_")
        .replace('[', "{")
        .replace(']', "}")
}

#[cfg(test)]
mod tests {
    use rusty_ulid::Ulid;

    use crate::logging::LogContext;

    #[test]
    fn log_context_display_all_fields_present() {
        let session = Ulid::from(0x01_23_45_67_89_AB_CD_EF_FE_DC_BA_98_76_54_32_10_u128);
        let request = Ulid::from(0x01_23_45_67_89_AB_CD_EF_FE_DC_BA_98_76_54_32_11_u128);
        let ctx = LogContext {
            session_id: session,
            request_id: Some(request),
            cluster_id: Some("cluster-abc"),
            backend_id: Some("backend-1"),
        };
        let rendered = format!("{ctx}");
        assert_eq!(
            rendered,
            format!("[{session} {request} cluster-abc backend-1]")
        );
    }

    #[test]
    fn log_context_display_dashes_when_missing() {
        let session = Ulid::from(0xABCDu128);
        let ctx = LogContext {
            session_id: session,
            request_id: None,
            cluster_id: None,
            backend_id: None,
        };
        assert_eq!(format!("{ctx}"), format!("[{session} - - -]"));
    }

    #[test]
    fn log_context_display_partial() {
        let session = Ulid::from(0x42u128);
        let request = Ulid::from(0x43u128);
        let ctx = LogContext {
            session_id: session,
            request_id: Some(request),
            cluster_id: None,
            backend_id: Some("backend-2"),
        };
        assert_eq!(
            format!("{ctx}"),
            format!("[{session} {request} - backend-2]")
        );
    }
}