agentguard_core/observability/
span.rs1use rand::RngCore;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::str::FromStr;
12
13use crate::error::{Error, Result};
14
15pub const TRACE_ID_LEN: usize = 16;
17pub const SPAN_ID_LEN: usize = 8;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(try_from = "String", into = "String")]
23pub struct TraceId(pub [u8; TRACE_ID_LEN]);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(try_from = "String", into = "String")]
28pub struct SpanId(pub [u8; SPAN_ID_LEN]);
29
30impl TraceId {
31 pub fn new(bytes: [u8; TRACE_ID_LEN]) -> Self {
32 Self(bytes)
33 }
34
35 pub fn random() -> Self {
37 let mut bytes = [0u8; TRACE_ID_LEN];
38 rand::rngs::OsRng.fill_bytes(&mut bytes);
39 Self(bytes)
40 }
41
42 pub fn as_hex(&self) -> String {
43 hex::encode(self.0)
44 }
45}
46
47impl fmt::Display for TraceId {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(&self.as_hex())
50 }
51}
52
53impl FromStr for TraceId {
54 type Err = Error;
55 fn from_str(s: &str) -> Result<Self> {
56 let bytes = hex::decode(s).map_err(|e| Error::Other(format!("trace_id hex: {}", e)))?;
57 if bytes.len() != TRACE_ID_LEN {
58 return Err(Error::Other(format!(
59 "trace_id must be {} bytes, got {}",
60 TRACE_ID_LEN,
61 bytes.len()
62 )));
63 }
64 let mut arr = [0u8; TRACE_ID_LEN];
65 arr.copy_from_slice(&bytes);
66 Ok(Self(arr))
67 }
68}
69
70impl From<TraceId> for String {
71 fn from(t: TraceId) -> Self {
72 t.as_hex()
73 }
74}
75
76impl TryFrom<String> for TraceId {
77 type Error = Error;
78 fn try_from(s: String) -> Result<Self> {
79 s.parse()
80 }
81}
82
83impl SpanId {
84 pub fn new(bytes: [u8; SPAN_ID_LEN]) -> Self {
85 Self(bytes)
86 }
87
88 pub fn random() -> Self {
89 let mut bytes = [0u8; SPAN_ID_LEN];
90 rand::rngs::OsRng.fill_bytes(&mut bytes);
91 Self(bytes)
92 }
93
94 pub fn as_hex(&self) -> String {
95 hex::encode(self.0)
96 }
97}
98
99impl fmt::Display for SpanId {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(&self.as_hex())
102 }
103}
104
105impl FromStr for SpanId {
106 type Err = Error;
107 fn from_str(s: &str) -> Result<Self> {
108 let bytes = hex::decode(s).map_err(|e| Error::Other(format!("span_id hex: {}", e)))?;
109 if bytes.len() != SPAN_ID_LEN {
110 return Err(Error::Other(format!(
111 "span_id must be {} bytes, got {}",
112 SPAN_ID_LEN,
113 bytes.len()
114 )));
115 }
116 let mut arr = [0u8; SPAN_ID_LEN];
117 arr.copy_from_slice(&bytes);
118 Ok(Self(arr))
119 }
120}
121
122impl From<SpanId> for String {
123 fn from(s: SpanId) -> Self {
124 s.as_hex()
125 }
126}
127
128impl TryFrom<String> for SpanId {
129 type Error = Error;
130 fn try_from(s: String) -> Result<Self> {
131 s.parse()
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140pub struct TraceContext {
141 pub trace_id: TraceId,
142 pub span_id: SpanId,
143 pub flags: u8,
145}
146
147impl TraceContext {
148 pub fn new(trace_id: TraceId, span_id: SpanId) -> Self {
151 Self {
152 trace_id,
153 span_id,
154 flags: 0x01,
155 }
156 }
157
158 pub fn fresh() -> Self {
160 Self::new(TraceId::random(), SpanId::random())
161 }
162
163 pub fn child(&self) -> Self {
166 Self {
167 trace_id: self.trace_id,
168 span_id: SpanId::random(),
169 flags: self.flags,
170 }
171 }
172}
173
174impl fmt::Display for TraceContext {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(
177 f,
178 "00-{}-{}-{:02x}",
179 self.trace_id, self.span_id, self.flags
180 )
181 }
182}
183
184impl FromStr for TraceContext {
185 type Err = Error;
186 fn from_str(s: &str) -> Result<Self> {
187 let parts: Vec<&str> = s.split('-').collect();
189 if parts.len() != 4 {
190 return Err(Error::Other(format!(
191 "traceparent must have 4 dash-separated parts, got {}",
192 parts.len()
193 )));
194 }
195 let version = parts[0];
196 if !version.starts_with("00") {
202 return Err(Error::Other(format!(
203 "unsupported traceparent version: {}",
204 version
205 )));
206 }
207 let trace_id: TraceId = parts[1].parse()?;
208 let span_id: SpanId = parts[2].parse()?;
209 let flags = u8::from_str_radix(parts[3], 16)
210 .map_err(|e| Error::Other(format!("flags hex: {}", e)))?;
211 Ok(Self {
212 trace_id,
213 span_id,
214 flags,
215 })
216 }
217}
218
219impl Serialize for TraceContext {
220 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
221 s.serialize_str(&self.to_string())
222 }
223}
224
225impl<'de> Deserialize<'de> for TraceContext {
226 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
227 let s = String::deserialize(d)?;
228 s.parse().map_err(serde::de::Error::custom)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn traceparent_roundtrip() {
238 let tc = TraceContext::fresh();
239 let s = tc.to_string();
240 let parsed: TraceContext = s.parse().unwrap();
241 assert_eq!(tc, parsed);
242 }
243
244 #[test]
245 fn trace_id_must_be_16_bytes() {
246 assert!("abcd".parse::<TraceId>().is_err());
247 let bytes = [1u8; 16];
248 let hex = hex::encode(bytes);
249 let t: TraceId = hex.parse().unwrap();
250 assert_eq!(t.0, bytes);
251 }
252
253 #[test]
254 fn span_id_must_be_8_bytes() {
255 let bytes = [1u8; 8];
256 let hex = hex::encode(bytes);
257 let s: SpanId = hex.parse().unwrap();
258 assert_eq!(s.0, bytes);
259 }
260
261 #[test]
262 fn traceparent_format() {
263 let trace_id = TraceId::new([0xab; 16]);
264 let span_id = SpanId::new([0xcd; 8]);
265 let tc = TraceContext::new(trace_id, span_id);
266 assert_eq!(
267 tc.to_string(),
268 "00-abababababababababababababababab-cdcdcdcdcdcdcdcd-01"
269 );
270 }
271
272 #[test]
273 fn traceparent_rejects_wrong_version() {
274 let bad = "01-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
276 assert!(bad.parse::<TraceContext>().is_err());
277 }
278
279 #[test]
280 fn traceparent_rejects_wrong_segment_count() {
281 let bad = "00-aaaa-bbbb-cccc-dddd";
282 assert!(bad.parse::<TraceContext>().is_err());
283 }
284
285 #[test]
286 fn traceparent_rejects_non_hex() {
287 let bad = "00-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-b7ad6b7169203331-01";
288 assert!(bad.parse::<TraceContext>().is_err());
289 }
290
291 #[test]
292 fn random_trace_id_is_unique_99pct_of_the_time() {
293 use std::collections::HashSet;
298 let mut set = HashSet::new();
299 for _ in 0..1000 {
300 set.insert(TraceId::random().0);
301 }
302 assert_eq!(set.len(), 1000, "TraceId::random had collisions");
303 }
304
305 #[test]
306 fn random_span_id_is_unique_99pct_of_the_time() {
307 use std::collections::HashSet;
308 let mut set = HashSet::new();
309 for _ in 0..1000 {
310 set.insert(SpanId::random().0);
311 }
312 assert_eq!(set.len(), 1000, "SpanId::random had collisions");
313 }
314
315 #[cfg(test)]
316 mod proptests {
317 use super::*;
318 use proptest::prelude::*;
319
320 proptest! {
321 #[test]
324 fn round_trip(seed in any::<u8>()) {
325 let trace_id = TraceId::new([seed; 16]);
326 let span_id = SpanId::new([seed.wrapping_mul(2); 8]);
327 let tc = TraceContext::new(trace_id, span_id);
328 let printed = tc.to_string();
329 let parsed: TraceContext = printed.parse().unwrap();
330 prop_assert_eq!(tc.trace_id, parsed.trace_id);
331 prop_assert_eq!(tc.span_id, parsed.span_id);
332 prop_assert_eq!(tc.flags, parsed.flags);
333 }
334 }
335 }
336}