saddle-observability 0.1.1

Saddle structured logging and trace correlation
Documentation
use std::{error::Error, fmt};

use saddle_core::TraceId;

/// How an external request's trace identifier was selected.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InboundTrace {
    /// A non-zero, 32-character hexadecimal identifier was inherited.
    Inherited,
    /// No identifier was supplied, so Saddle created one.
    Created,
    /// The supplied identifier was invalid, so Saddle created a safe replacement.
    ReplacedInvalid,
}

impl InboundTrace {
    pub(crate) const fn as_str(self) -> &'static str {
        match self {
            Self::Inherited => "inherited",
            Self::Created => "created",
            Self::ReplacedInvalid => "replaced_invalid",
        }
    }
}

/// The reason a textual trace identifier cannot be inherited.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TraceIdError {
    InvalidLength,
    InvalidHex,
    Zero,
}

impl fmt::Display for TraceIdError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::InvalidLength => "trace id must contain exactly 32 hexadecimal characters",
            Self::InvalidHex => "trace id contains a non-hexadecimal character",
            Self::Zero => "trace id must not be zero",
        })
    }
}

impl Error for TraceIdError {}

pub(crate) fn select_trace_id(
    value: Option<&str>,
    generated: impl FnOnce() -> TraceId,
) -> (TraceId, InboundTrace) {
    match value {
        Some(value) => match trace_id_from_hex(value) {
            Ok(trace_id) => (trace_id, InboundTrace::Inherited),
            Err(_) => (generated(), InboundTrace::ReplacedInvalid),
        },
        None => (generated(), InboundTrace::Created),
    }
}

/// Parses Saddle's fixed-width hexadecimal trace-id representation.
pub fn trace_id_from_hex(value: &str) -> Result<TraceId, TraceIdError> {
    if value.len() != 32 {
        return Err(TraceIdError::InvalidLength);
    }
    if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(TraceIdError::InvalidHex);
    }

    let value = u128::from_str_radix(value, 16).map_err(|_| TraceIdError::InvalidHex)?;
    if value == 0 {
        return Err(TraceIdError::Zero);
    }
    Ok(TraceId::from_u128(value))
}

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

    #[test]
    fn accepts_only_non_zero_fixed_width_hex_trace_ids() {
        let expected = "00112233445566778899aabbccddeeff";
        assert_eq!(trace_id_from_hex(expected).unwrap().to_string(), expected);
        assert_eq!(trace_id_from_hex("abcd"), Err(TraceIdError::InvalidLength));
        assert_eq!(
            trace_id_from_hex("00112233445566778899aabbccddeefg"),
            Err(TraceIdError::InvalidHex)
        );
        assert_eq!(
            trace_id_from_hex("00000000000000000000000000000000"),
            Err(TraceIdError::Zero)
        );
    }
}