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::from_borrowed(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::from_generated(trace_id);
78            Ok((trace_id, correlation, InboundTrace::Created))
79        }
80    }
81}
82
83impl From<TraceCorrelationIdError> for TraceIdError {
84    fn from(error: TraceCorrelationIdError) -> Self {
85        match error {
86            TraceCorrelationIdError::Empty => Self::Empty,
87            TraceCorrelationIdError::TooLong => Self::TooLong,
88            TraceCorrelationIdError::ControlCharacter => Self::ControlCharacter,
89        }
90    }
91}
92
93/// Parses Saddle's fixed-width hexadecimal trace-id representation.
94pub fn trace_id_from_hex(value: &str) -> Result<TraceId, TraceIdError> {
95    if value.len() != 32 {
96        return Err(TraceIdError::InvalidLength);
97    }
98    if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
99        return Err(TraceIdError::InvalidHex);
100    }
101
102    let value = u128::from_str_radix(value, 16).map_err(|_| TraceIdError::InvalidHex)?;
103    if value == 0 {
104        return Err(TraceIdError::Zero);
105    }
106    Ok(TraceId::from_u128(value))
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn accepts_only_non_zero_fixed_width_hex_trace_ids() {
115        let expected = "00112233445566778899aabbccddeeff";
116        assert_eq!(trace_id_from_hex(expected).unwrap().to_string(), expected);
117        assert_eq!(trace_id_from_hex("abcd"), Err(TraceIdError::InvalidLength));
118        assert_eq!(
119            trace_id_from_hex("00112233445566778899aabbccddeefg"),
120            Err(TraceIdError::InvalidHex)
121        );
122        assert_eq!(
123            trace_id_from_hex("00000000000000000000000000000000"),
124            Err(TraceIdError::Zero)
125        );
126    }
127
128    #[test]
129    fn entry_selector_preserves_opaque_protocol_trace_id() {
130        let internal = TraceId::from_u128(42);
131        let (selected, correlation, source) =
132            select_entry_trace_id(Some("trace-1"), || internal).unwrap();
133        assert_eq!(selected, internal);
134        assert_eq!(correlation.as_str(), "trace-1");
135        assert_eq!(source, InboundTrace::Inherited);
136        assert_eq!(
137            select_entry_trace_id(Some("trace\n1"), || internal).unwrap_err(),
138            TraceIdError::ControlCharacter
139        );
140    }
141}