Skip to main content

saddle_observability/
trace.rs

1use std::{error::Error, fmt};
2
3use saddle_core::{TraceCorrelationId, TraceCorrelationIdError, TraceId};
4
5/// How an external request's trace identifier was selected.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum InboundTrace {
8    /// A bounded, non-empty opaque protocol identifier was inherited.
9    Inherited,
10    /// No identifier was supplied, so Saddle created one.
11    Created,
12    /// Legacy compatibility only: the supplied identifier was invalid.
13    ReplacedInvalid,
14}
15
16impl InboundTrace {
17    pub(crate) const fn as_str(self) -> &'static str {
18        match self {
19            Self::Inherited => "inherited",
20            Self::Created => "created",
21            Self::ReplacedInvalid => "replaced_invalid",
22        }
23    }
24}
25
26/// The reason a textual trace identifier cannot be inherited or parsed.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum TraceIdError {
29    Empty,
30    TooLong,
31    ControlCharacter,
32    InvalidLength,
33    InvalidHex,
34    Zero,
35}
36
37impl fmt::Display for TraceIdError {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        formatter.write_str(match self {
40            Self::Empty => "trace id must not be empty",
41            Self::TooLong => "trace id exceeds 256 UTF-8 bytes",
42            Self::ControlCharacter => "trace id contains a control character",
43            Self::InvalidLength => "trace id must contain exactly 32 hexadecimal characters",
44            Self::InvalidHex => "trace id contains a non-hexadecimal character",
45            Self::Zero => "trace id must not be zero",
46        })
47    }
48}
49
50impl Error for TraceIdError {}
51
52pub(crate) fn select_trace_id(
53    value: Option<&str>,
54    generated: impl FnOnce() -> TraceId,
55) -> (TraceId, InboundTrace) {
56    match value {
57        Some(value) => match trace_id_from_hex(value) {
58            Ok(trace_id) => (trace_id, InboundTrace::Inherited),
59            Err(_) => (generated(), InboundTrace::ReplacedInvalid),
60        },
61        None => (generated(), InboundTrace::Created),
62    }
63}
64
65pub(crate) fn select_entry_trace_id(
66    value: Option<&str>,
67    generated: impl FnOnce() -> TraceId,
68) -> Result<(TraceId, TraceCorrelationId, InboundTrace), TraceIdError> {
69    match value {
70        Some(value) => {
71            let correlation = TraceCorrelationId::new(value).map_err(TraceIdError::from)?;
72            let trace_id = trace_id_from_hex(value).unwrap_or_else(|_| generated());
73            Ok((trace_id, correlation, InboundTrace::Inherited))
74        }
75        None => {
76            let trace_id = generated();
77            let correlation = TraceCorrelationId::new(trace_id.to_string())
78                .expect("Saddle-generated trace ids satisfy the correlation contract");
79            Ok((trace_id, correlation, InboundTrace::Created))
80        }
81    }
82}
83
84impl From<TraceCorrelationIdError> for TraceIdError {
85    fn from(error: TraceCorrelationIdError) -> Self {
86        match error {
87            TraceCorrelationIdError::Empty => Self::Empty,
88            TraceCorrelationIdError::TooLong => Self::TooLong,
89            TraceCorrelationIdError::ControlCharacter => Self::ControlCharacter,
90        }
91    }
92}
93
94/// Parses Saddle's fixed-width hexadecimal trace-id representation.
95pub fn trace_id_from_hex(value: &str) -> Result<TraceId, TraceIdError> {
96    if value.len() != 32 {
97        return Err(TraceIdError::InvalidLength);
98    }
99    if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
100        return Err(TraceIdError::InvalidHex);
101    }
102
103    let value = u128::from_str_radix(value, 16).map_err(|_| TraceIdError::InvalidHex)?;
104    if value == 0 {
105        return Err(TraceIdError::Zero);
106    }
107    Ok(TraceId::from_u128(value))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn accepts_only_non_zero_fixed_width_hex_trace_ids() {
116        let expected = "00112233445566778899aabbccddeeff";
117        assert_eq!(trace_id_from_hex(expected).unwrap().to_string(), expected);
118        assert_eq!(trace_id_from_hex("abcd"), Err(TraceIdError::InvalidLength));
119        assert_eq!(
120            trace_id_from_hex("00112233445566778899aabbccddeefg"),
121            Err(TraceIdError::InvalidHex)
122        );
123        assert_eq!(
124            trace_id_from_hex("00000000000000000000000000000000"),
125            Err(TraceIdError::Zero)
126        );
127    }
128
129    #[test]
130    fn entry_selector_preserves_opaque_protocol_trace_id() {
131        let internal = TraceId::from_u128(42);
132        let (selected, correlation, source) =
133            select_entry_trace_id(Some("trace-1"), || internal).unwrap();
134        assert_eq!(selected, internal);
135        assert_eq!(correlation.as_str(), "trace-1");
136        assert_eq!(source, InboundTrace::Inherited);
137        assert_eq!(
138            select_entry_trace_id(Some("trace\n1"), || internal).unwrap_err(),
139            TraceIdError::ControlCharacter
140        );
141    }
142}