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, Debug, Eq, Hash, PartialEq)]
48struct InlineCorrelation {
49 bytes: [u8; MAX_TRACE_CORRELATION_ID_BYTES],
50 len: u16,
51}
52impl InlineCorrelation {
53 fn copy(value: &str) -> Self {
54 let mut text = Self { bytes: [0; MAX_TRACE_CORRELATION_ID_BYTES], len: value.len() as u16 };
55 text.bytes[..value.len()].copy_from_slice(value.as_bytes());
56 text
57 }
58 fn as_str(&self) -> &str {
59 std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("validated correlation")
60 }
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct RpcCorrelationId(InlineCorrelation);
65
66impl RpcCorrelationId {
67 pub fn new(value: &str) -> Option<Self> {
68 if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
69 return None;
70 }
71 Some(Self(InlineCorrelation::copy(value)))
72 }
73
74 pub fn as_str(&self) -> &str {
75 self.0.as_str()
76 }
77}
78
79#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
80pub struct TraceId(u128);
81
82impl TraceId {
83 pub const fn from_u128(value: u128) -> Self {
84 Self(value)
85 }
86
87 pub const fn as_u128(self) -> u128 {
88 self.0
89 }
90}
91
92impl fmt::Display for TraceId {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(formatter, "{:032x}", self.0)
95 }
96}
97
98#[derive(Clone, Debug, Eq, Hash, PartialEq)]
99pub struct TraceCorrelationId(InlineCorrelation);
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum TraceCorrelationIdError {
103 Empty,
104 TooLong,
105 ControlCharacter,
106}
107
108impl TraceCorrelationId {
109 #[doc(hidden)]
110 pub fn from_generated(trace_id: TraceId) -> Self {
111 const HEX: &[u8; 16] = b"0123456789abcdef";
112 let mut text = InlineCorrelation { bytes: [0; MAX_TRACE_CORRELATION_ID_BYTES], len: 32 };
113 let mut value = trace_id.as_u128();
114 for index in (0..32).rev() {
115 text.bytes[index] = HEX[(value & 0x0f) as usize];
116 value >>= 4;
117 }
118 Self(text)
119 }
120 pub fn new(value: impl Into<Arc<str>>) -> Result<Self, TraceCorrelationIdError> {
121 let value = value.into();
122 Self::from_borrowed(&value)
123 }
124
125 #[doc(hidden)]
126 pub fn from_borrowed(value: &str) -> Result<Self, TraceCorrelationIdError> {
127 if value.is_empty() {
128 return Err(TraceCorrelationIdError::Empty);
129 }
130 if value.len() > MAX_TRACE_CORRELATION_ID_BYTES {
131 return Err(TraceCorrelationIdError::TooLong);
132 }
133 if value.chars().any(char::is_control) {
134 return Err(TraceCorrelationIdError::ControlCharacter);
135 }
136 Ok(Self(InlineCorrelation::copy(value)))
137 }
138
139 pub fn as_str(&self) -> &str {
140 self.0.as_str()
141 }
142}
143
144impl fmt::Display for TraceCorrelationId {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 formatter.write_str(self.as_str())
147 }
148}
149
150#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
151pub struct SpanId(u64);
152
153impl SpanId {
154 pub const fn from_u64(value: u64) -> Self {
155 Self(value)
156 }
157
158 pub const fn as_u64(self) -> u64 {
159 self.0
160 }
161}
162
163impl fmt::Display for SpanId {
164 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165 write!(formatter, "{:016x}", self.0)
166 }
167}
168
169#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct CallContext {
175 application: ApplicationId,
176 module: ModuleId,
177 service: ServiceId,
178 operation: OperationId,
179 trace_id: TraceId,
180 trace_correlation_id: TraceCorrelationId,
181 span_id: SpanId,
182 rpc_correlation_id: Option<RpcCorrelationId>,
183}
184
185impl CallContext {
186 pub fn new(
187 application: ApplicationId,
188 module: ModuleId,
189 service: ServiceId,
190 operation: OperationId,
191 trace_id: TraceId,
192 span_id: SpanId,
193 ) -> Self {
194 let trace_correlation_id = TraceCorrelationId::from_generated(trace_id);
195 Self {
196 application,
197 module,
198 service,
199 operation,
200 trace_id,
201 trace_correlation_id,
202 span_id,
203 rpc_correlation_id: None,
204 }
205 }
206
207 pub fn with_trace_correlation_id(mut self, trace_correlation_id: TraceCorrelationId) -> Self {
208 self.trace_correlation_id = trace_correlation_id;
209 self
210 }
211
212 pub fn application(&self) -> &ApplicationId {
213 &self.application
214 }
215
216 pub fn with_rpc_correlation_id(mut self, rpc: Option<RpcCorrelationId>) -> Self {
217 self.rpc_correlation_id = rpc;
218 self
219 }
220
221 pub fn rpc_correlation_id(&self) -> Option<&RpcCorrelationId> {
222 self.rpc_correlation_id.as_ref()
223 }
224
225 pub fn module(&self) -> &ModuleId {
226 &self.module
227 }
228
229 pub fn service(&self) -> &ServiceId {
230 &self.service
231 }
232
233 pub fn operation(&self) -> &OperationId {
234 &self.operation
235 }
236
237 pub const fn trace_id(&self) -> TraceId {
238 self.trace_id
239 }
240
241 pub fn trace_correlation_id(&self) -> &TraceCorrelationId {
242 &self.trace_correlation_id
243 }
244
245 pub const fn span_id(&self) -> SpanId {
246 self.span_id
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn protocol_rpc_validation_does_not_mint_or_replace_identity() {
256 for value in ["0", "0.1", "0.12.3"] {
257 assert_eq!(RpcCorrelationId::new(value).unwrap().as_str(), value);
258 }
259 for value in ["", " ", "0\n1", "0\u{7f}"] {
260 assert!(RpcCorrelationId::new(value).is_none());
261 }
262 assert!(RpcCorrelationId::new(&"1".repeat(256)).is_some());
263 assert!(RpcCorrelationId::new(&"1".repeat(257)).is_none());
264 }
265
266 #[test]
267 fn inline_correlations_preserve_generated_hex_and_bounded_utf8() {
268 let trace = TraceId::from_u128(0x1234_5678_90ab_cdef_0123_4567_89ab_cdef);
269 assert_eq!(TraceCorrelationId::from_generated(trace).as_str(), trace.to_string());
270 let inbound = format!("{}a", "界".repeat(85));
271 assert_eq!(TraceCorrelationId::from_borrowed(&inbound).unwrap().as_str(), inbound);
272 assert_eq!(RpcCorrelationId::new(&inbound).unwrap().as_str(), inbound);
273 assert!(!std::mem::needs_drop::<TraceCorrelationId>());
274 assert!(!std::mem::needs_drop::<RpcCorrelationId>());
275 }
276
277 #[test]
278 fn identifiers_have_stable_display_forms() {
279 assert_eq!(TraceId::from_u128(42).to_string().len(), 32);
280 assert_eq!(SpanId::from_u64(42).to_string().len(), 16);
281 assert_eq!(ServiceId::from("orders").to_string(), "orders");
282 }
283
284 #[test]
285 fn opaque_trace_correlation_id_is_bounded_and_rejects_controls() {
286 assert_eq!(
287 TraceCorrelationId::new("trace-1").unwrap().as_str(),
288 "trace-1"
289 );
290 assert_eq!(
291 TraceCorrelationId::new(""),
292 Err(TraceCorrelationIdError::Empty)
293 );
294 assert_eq!(
295 TraceCorrelationId::new("x".repeat(MAX_TRACE_CORRELATION_ID_BYTES + 1)),
296 Err(TraceCorrelationIdError::TooLong)
297 );
298 assert_eq!(
299 TraceCorrelationId::new("trace\n1"),
300 Err(TraceCorrelationIdError::ControlCharacter)
301 );
302 }
303}