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