Skip to main content

saddle_core/
context.rs

1use std::{fmt, sync::Arc};
2
3pub const MAX_TRACE_CORRELATION_ID_BYTES: usize = 256;
4
5macro_rules! string_id {
6    ($name:ident) => {
7        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8        pub struct $name(Arc<str>);
9
10        impl $name {
11            pub fn new(value: impl Into<Arc<str>>) -> Self {
12                Self(value.into())
13            }
14
15            pub fn as_str(&self) -> &str {
16                &self.0
17            }
18        }
19
20        impl From<&str> for $name {
21            fn from(value: &str) -> Self {
22                Self::new(value)
23            }
24        }
25
26        impl From<String> for $name {
27            fn from(value: String) -> Self {
28                Self::new(value)
29            }
30        }
31
32        impl fmt::Display for $name {
33            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34                formatter.write_str(&self.0)
35            }
36        }
37    };
38}
39
40string_id!(ApplicationId);
41string_id!(ModuleId);
42string_id!(ServiceId);
43string_id!(OperationId);
44
45#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
46pub struct TraceId(u128);
47
48impl TraceId {
49    pub const fn from_u128(value: u128) -> Self {
50        Self(value)
51    }
52
53    pub const fn as_u128(self) -> u128 {
54        self.0
55    }
56}
57
58impl fmt::Display for TraceId {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(formatter, "{:032x}", self.0)
61    }
62}
63
64#[derive(Clone, Debug, Eq, Hash, PartialEq)]
65pub struct TraceCorrelationId(Arc<str>);
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum TraceCorrelationIdError {
69    Empty,
70    TooLong,
71    ControlCharacter,
72}
73
74impl TraceCorrelationId {
75    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, TraceCorrelationIdError> {
76        let value = value.into();
77        if value.is_empty() {
78            return Err(TraceCorrelationIdError::Empty);
79        }
80        if value.len() > MAX_TRACE_CORRELATION_ID_BYTES {
81            return Err(TraceCorrelationIdError::TooLong);
82        }
83        if value.chars().any(char::is_control) {
84            return Err(TraceCorrelationIdError::ControlCharacter);
85        }
86        Ok(Self(value))
87    }
88
89    pub fn as_str(&self) -> &str {
90        &self.0
91    }
92}
93
94impl fmt::Display for TraceCorrelationId {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        formatter.write_str(&self.0)
97    }
98}
99
100#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
101pub struct SpanId(u64);
102
103impl SpanId {
104    pub const fn from_u64(value: u64) -> Self {
105        Self(value)
106    }
107
108    pub const fn as_u64(self) -> u64 {
109        self.0
110    }
111}
112
113impl fmt::Display for SpanId {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(formatter, "{:016x}", self.0)
116    }
117}
118
119/// The minimum identity propagated through Service, DB and Observability in V1.
120///
121/// Deadline, cancellation and resource-budget fields are deliberately absent
122/// until a version explicitly defines their behavior.
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct CallContext {
125    application: ApplicationId,
126    module: ModuleId,
127    service: ServiceId,
128    operation: OperationId,
129    trace_id: TraceId,
130    trace_correlation_id: TraceCorrelationId,
131    span_id: SpanId,
132}
133
134impl CallContext {
135    pub fn new(
136        application: ApplicationId,
137        module: ModuleId,
138        service: ServiceId,
139        operation: OperationId,
140        trace_id: TraceId,
141        span_id: SpanId,
142    ) -> Self {
143        let trace_correlation_id = TraceCorrelationId(Arc::from(trace_id.to_string()));
144        Self {
145            application,
146            module,
147            service,
148            operation,
149            trace_id,
150            trace_correlation_id,
151            span_id,
152        }
153    }
154
155    pub fn with_trace_correlation_id(mut self, trace_correlation_id: TraceCorrelationId) -> Self {
156        self.trace_correlation_id = trace_correlation_id;
157        self
158    }
159
160    pub fn application(&self) -> &ApplicationId {
161        &self.application
162    }
163
164    pub fn module(&self) -> &ModuleId {
165        &self.module
166    }
167
168    pub fn service(&self) -> &ServiceId {
169        &self.service
170    }
171
172    pub fn operation(&self) -> &OperationId {
173        &self.operation
174    }
175
176    pub const fn trace_id(&self) -> TraceId {
177        self.trace_id
178    }
179
180    pub fn trace_correlation_id(&self) -> &TraceCorrelationId {
181        &self.trace_correlation_id
182    }
183
184    pub const fn span_id(&self) -> SpanId {
185        self.span_id
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn identifiers_have_stable_display_forms() {
195        assert_eq!(TraceId::from_u128(42).to_string().len(), 32);
196        assert_eq!(SpanId::from_u64(42).to_string().len(), 16);
197        assert_eq!(ServiceId::from("orders").to_string(), "orders");
198    }
199
200    #[test]
201    fn opaque_trace_correlation_id_is_bounded_and_rejects_controls() {
202        assert_eq!(
203            TraceCorrelationId::new("trace-1").unwrap().as_str(),
204            "trace-1"
205        );
206        assert_eq!(
207            TraceCorrelationId::new(""),
208            Err(TraceCorrelationIdError::Empty)
209        );
210        assert_eq!(
211            TraceCorrelationId::new("x".repeat(MAX_TRACE_CORRELATION_ID_BYTES + 1)),
212            Err(TraceCorrelationIdError::TooLong)
213        );
214        assert_eq!(
215            TraceCorrelationId::new("trace\n1"),
216            Err(TraceCorrelationIdError::ControlCharacter)
217        );
218    }
219}