saddle_observability/
trace.rs1use std::{error::Error, fmt};
2
3use saddle_core::{TraceCorrelationId, TraceCorrelationIdError, TraceId};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum InboundTrace {
8 Inherited,
10 Created,
12 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#[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
94pub 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}