saddle-observability 0.3.2

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

use saddle_core::{TraceCorrelationId, TraceCorrelationIdError, TraceId};

/// How an external request's trace identifier was selected.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InboundTrace {
    /// A bounded, non-empty opaque protocol identifier was inherited.
    Inherited,
    /// No identifier was supplied, so Saddle created one.
    Created,
    /// Legacy compatibility only: the supplied identifier was invalid.
    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 or parsed.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TraceIdError {
    Empty,
    TooLong,
    ControlCharacter,
    InvalidLength,
    InvalidHex,
    Zero,
}

impl fmt::Display for TraceIdError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Empty => "trace id must not be empty",
            Self::TooLong => "trace id exceeds 256 UTF-8 bytes",
            Self::ControlCharacter => "trace id contains a control character",
            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),
    }
}

pub(crate) fn select_entry_trace_id(
    value: Option<&str>,
    generated: impl FnOnce() -> TraceId,
) -> Result<(TraceId, TraceCorrelationId, InboundTrace), TraceIdError> {
    match value {
        Some(value) => {
            let correlation = TraceCorrelationId::new(value).map_err(TraceIdError::from)?;
            let trace_id = trace_id_from_hex(value).unwrap_or_else(|_| generated());
            Ok((trace_id, correlation, InboundTrace::Inherited))
        }
        None => {
            let trace_id = generated();
            let correlation = TraceCorrelationId::new(trace_id.to_string())
                .expect("Saddle-generated trace ids satisfy the correlation contract");
            Ok((trace_id, correlation, InboundTrace::Created))
        }
    }
}

impl From<TraceCorrelationIdError> for TraceIdError {
    fn from(error: TraceCorrelationIdError) -> Self {
        match error {
            TraceCorrelationIdError::Empty => Self::Empty,
            TraceCorrelationIdError::TooLong => Self::TooLong,
            TraceCorrelationIdError::ControlCharacter => Self::ControlCharacter,
        }
    }
}

/// 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)
        );
    }

    #[test]
    fn entry_selector_preserves_opaque_protocol_trace_id() {
        let internal = TraceId::from_u128(42);
        let (selected, correlation, source) =
            select_entry_trace_id(Some("trace-1"), || internal).unwrap();
        assert_eq!(selected, internal);
        assert_eq!(correlation.as_str(), "trace-1");
        assert_eq!(source, InboundTrace::Inherited);
        assert_eq!(
            select_entry_trace_id(Some("trace\n1"), || internal).unwrap_err(),
            TraceIdError::ControlCharacter
        );
    }
}