1use relay_protocol::{
2 Annotated, Array, Empty, Error, FromValue, HexId, IntoValue, Object, SkipSerialization, Val,
3 Value,
4};
5use serde::Serializer;
6use std::fmt;
7use std::ops::Deref;
8use std::str::FromStr;
9use uuid::Uuid;
10
11use crate::processor::ProcessValue;
12use crate::protocol::{OperationType, OriginType, SpanData, SpanLink, SpanStatus};
13
14#[derive(Clone, Copy, Default, PartialEq, Empty, ProcessValue)]
30pub struct TraceId(Uuid);
31
32relay_common::impl_str_serde!(TraceId, "a trace identifier");
33
34impl FromStr for TraceId {
35 type Err = Error;
36
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
38 Uuid::parse_str(s)
39 .map(Into::into)
40 .map_err(|_| Error::invalid("the trace id is not valid"))
41 }
42}
43
44impl TryFrom<&[u8]> for TraceId {
45 type Error = Error;
46
47 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
48 let uuid =
49 Uuid::from_slice(value).map_err(|_| Error::invalid("the trace id is not valid"))?;
50 Ok(Self(uuid))
51 }
52}
53
54impl fmt::Display for TraceId {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.0.as_simple())
57 }
58}
59
60impl fmt::Debug for TraceId {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(f, "TraceId(\"{}\")", self.0.as_simple())
63 }
64}
65
66impl From<Uuid> for TraceId {
67 fn from(uuid: Uuid) -> Self {
68 TraceId(uuid)
69 }
70}
71
72impl Deref for TraceId {
73 type Target = Uuid;
74
75 fn deref(&self) -> &Self::Target {
76 &self.0
77 }
78}
79
80impl FromValue for TraceId {
81 fn from_value(value: Annotated<Value>) -> Annotated<Self>
82 where
83 Self: Sized,
84 {
85 match value {
86 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
87 Ok(trace_id) => Annotated(Some(trace_id), meta),
88 Err(_) => {
89 meta.add_error(Error::invalid("not a valid trace id"));
90 meta.set_original_value(Some(value));
91 Annotated(None, meta)
92 }
93 },
94 Annotated(None, meta) => Annotated(None, meta),
95 Annotated(Some(value), mut meta) => {
96 meta.add_error(Error::expected("trace id"));
97 meta.set_original_value(Some(value));
98 Annotated(None, meta)
99 }
100 }
101 }
102}
103
104impl IntoValue for TraceId {
105 fn into_value(self) -> Value
106 where
107 Self: Sized,
108 {
109 Value::String(self.to_string())
110 }
111
112 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
113 where
114 Self: Sized,
115 S: Serializer,
116 {
117 s.collect_str(self)
118 }
119}
120
121#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)]
124pub struct SpanId([u8; 8]);
125
126relay_common::impl_str_serde!(SpanId, "a span identifier");
127
128impl FromStr for SpanId {
129 type Err = Error;
130
131 fn from_str(s: &str) -> Result<Self, Self::Err> {
132 match u64::from_str_radix(s, 16) {
133 Ok(id) if s.len() == 16 && id > 0 => Ok(Self(id.to_be_bytes())),
134 _ => Err(Error::invalid("not a valid span id")),
135 }
136 }
137}
138
139impl TryFrom<&[u8]> for SpanId {
140 type Error = Error;
141
142 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
143 match <[u8; 8]>::try_from(value) {
144 Ok(bytes) if !bytes.iter().all(|&x| x == 0) => Ok(Self(bytes)),
145 _ => Err(Error::invalid("not a valid span id")),
146 }
147 }
148}
149
150impl fmt::Debug for SpanId {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 write!(f, "SpanId(\"")?;
153 for b in self.0 {
154 write!(f, "{b:02x}")?;
155 }
156 write!(f, "\")")
157 }
158}
159
160impl fmt::Display for SpanId {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 for b in self.0 {
163 write!(f, "{b:02x}")?;
164 }
165 Ok(())
166 }
167}
168
169impl FromValue for SpanId {
170 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
171 match value {
172 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
173 Ok(span_id) => Annotated::new(span_id),
174 Err(e) => {
175 meta.add_error(e);
176 meta.set_original_value(Some(value));
177 Annotated(None, meta)
178 }
179 },
180 Annotated(None, meta) => Annotated(None, meta),
181 Annotated(Some(value), mut meta) => {
182 meta.add_error(Error::expected("span id"));
183 meta.set_original_value(Some(value));
184 Annotated(None, meta)
185 }
186 }
187 }
188}
189
190impl Empty for SpanId {
191 fn is_empty(&self) -> bool {
192 false
193 }
194}
195
196impl IntoValue for SpanId {
197 fn into_value(self) -> Value
198 where
199 Self: Sized,
200 {
201 Value::String(self.to_string())
202 }
203
204 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
205 where
206 Self: Sized,
207 S: serde::Serializer,
208 {
209 s.collect_str(self)
210 }
211}
212
213impl ProcessValue for SpanId {}
214
215impl std::ops::Deref for SpanId {
216 type Target = [u8];
217
218 fn deref(&self) -> &Self::Target {
219 &self.0
220 }
221}
222
223impl<'a> From<&'a SpanId> for Val<'a> {
224 fn from(value: &'a SpanId) -> Self {
225 Val::HexId(HexId(&value.0))
226 }
227}
228
229#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
231#[metastructure(process_func = "process_trace_context")]
232pub struct TraceContext {
233 #[metastructure(required = true)]
235 pub trace_id: Annotated<TraceId>,
236
237 #[metastructure(required = true)]
239 pub span_id: Annotated<SpanId>,
240
241 pub parent_span_id: Annotated<SpanId>,
243
244 #[metastructure(max_chars = 128)]
246 pub op: Annotated<OperationType>,
247
248 pub status: Annotated<SpanStatus>,
251
252 pub exclusive_time: Annotated<f64>,
255
256 pub client_sample_rate: Annotated<f64>,
261
262 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
264 pub origin: Annotated<OriginType>,
265
266 pub sampled: Annotated<bool>,
270
271 #[metastructure(pii = "maybe", skip_serialization = "null")]
273 pub data: Annotated<SpanData>,
274
275 #[metastructure(pii = "maybe", skip_serialization = "null")]
277 pub links: Annotated<Array<SpanLink>>,
278
279 #[metastructure(additional_properties, retain = true, pii = "maybe")]
281 pub other: Object<Value>,
282}
283
284impl super::DefaultContext for TraceContext {
285 fn default_key() -> &'static str {
286 "trace"
287 }
288
289 fn from_context(context: super::Context) -> Option<Self> {
290 match context {
291 super::Context::Trace(c) => Some(*c),
292 _ => None,
293 }
294 }
295
296 fn cast(context: &super::Context) -> Option<&Self> {
297 match context {
298 super::Context::Trace(c) => Some(c),
299 _ => None,
300 }
301 }
302
303 fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
304 match context {
305 super::Context::Trace(c) => Some(c),
306 _ => None,
307 }
308 }
309
310 fn into_context(self) -> super::Context {
311 super::Context::Trace(Box::new(self))
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::protocol::{Context, Route};
319
320 #[test]
321 fn test_trace_id_as_u128() {
322 let trace_id: TraceId = "4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap();
324 assert_eq!(trace_id.as_u128(), 0x4c79f60c11214eb38604f4ae0781bfb2);
325
326 let empty_trace_id: Result<TraceId, Error> = "".parse();
328 assert!(empty_trace_id.is_err());
329
330 let short_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb".parse(); assert!(short_trace_id.is_err());
333
334 let long_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb2a".parse(); assert!(long_trace_id.is_err());
336
337 let invalid_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfbg".parse(); assert!(invalid_trace_id.is_err());
340 }
341
342 #[test]
343 fn test_trace_context_roundtrip() {
344 let json = r#"{
345 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
346 "span_id": "fa90fdead5f74052",
347 "parent_span_id": "fa90fdead5f74053",
348 "op": "http",
349 "status": "ok",
350 "exclusive_time": 0.0,
351 "client_sample_rate": 0.5,
352 "origin": "auto.http",
353 "data": {
354 "route": {
355 "name": "/users",
356 "params": {
357 "tok": "test"
358 },
359 "custom_field": "something"
360 },
361 "custom_field_empty": ""
362 },
363 "links": [
364 {
365 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
366 "span_id": "ea90fdead5f74052",
367 "sampled": true,
368 "attributes": {
369 "sentry.link.type": "previous_trace"
370 }
371 }
372 ],
373 "other": "value",
374 "type": "trace"
375}"#;
376 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
377 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
378 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
379 parent_span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
380 op: Annotated::new("http".into()),
381 status: Annotated::new(SpanStatus::Ok),
382 exclusive_time: Annotated::new(0.0),
383 client_sample_rate: Annotated::new(0.5),
384 origin: Annotated::new("auto.http".to_owned()),
385 data: Annotated::new(SpanData {
386 route: Annotated::new(Route {
387 name: Annotated::new("/users".into()),
388 params: Annotated::new({
389 let mut map = Object::new();
390 map.insert(
391 "tok".to_string(),
392 Annotated::new(Value::String("test".into())),
393 );
394 map
395 }),
396 other: Object::from([(
397 "custom_field".into(),
398 Annotated::new(Value::String("something".into())),
399 )]),
400 }),
401 other: Object::from([(
402 "custom_field_empty".into(),
403 Annotated::new(Value::String("".into())),
404 )]),
405 ..Default::default()
406 }),
407 links: Annotated::new(Array::from(vec![Annotated::new(SpanLink {
408 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
409 span_id: Annotated::new("ea90fdead5f74052".parse().unwrap()),
410 sampled: Annotated::new(true),
411 attributes: Annotated::new({
412 let mut map: std::collections::BTreeMap<String, Annotated<Value>> =
413 Object::new();
414 map.insert(
415 "sentry.link.type".into(),
416 Annotated::new(Value::String("previous_trace".into())),
417 );
418 map
419 }),
420 ..Default::default()
421 })])),
422 other: {
423 let mut map = Object::new();
424 map.insert(
425 "other".to_string(),
426 Annotated::new(Value::String("value".to_string())),
427 );
428 map
429 },
430 sampled: Annotated::empty(),
431 })));
432
433 assert_eq!(context, Annotated::from_json(json).unwrap());
434 assert_eq!(json, context.to_json_pretty().unwrap());
435 }
436
437 #[test]
438 fn test_trace_context_normalization() {
439 let json = r#"{
440 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
441 "span_id": "FA90FDEAD5F74052",
442 "type": "trace"
443}"#;
444 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
445 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
446 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
447 ..Default::default()
448 })));
449
450 assert_eq!(context, Annotated::from_json(json).unwrap());
451 }
452
453 #[test]
454 fn test_trace_id_formatting() {
455 let test_cases = [
456 (
458 r#"{
459 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
460 "type": "trace"
461}"#,
462 "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
463 true,
464 ),
465 (
467 r#"{
468 "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
469 "type": "trace"
470}"#,
471 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
472 false,
473 ),
474 (
476 r#"{
477 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
478 "type": "trace"
479}"#,
480 "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
481 true,
482 ),
483 (
485 r#"{
486 "trace_id": "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
487 "type": "trace"
488}"#,
489 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
490 false,
491 ),
492 ];
493
494 for (json, trace_id_str, is_to_json) in test_cases {
495 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
496 trace_id: Annotated::new(trace_id_str.parse().unwrap()),
497 ..Default::default()
498 })));
499
500 if is_to_json {
501 assert_eq!(json, context.to_json_pretty().unwrap());
502 } else {
503 assert_eq!(context, Annotated::from_json(json).unwrap());
504 }
505 }
506 }
507
508 #[test]
509 fn test_trace_context_with_routes() {
510 let json = r#"{
511 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
512 "span_id": "FA90FDEAD5F74052",
513 "type": "trace",
514 "data": {
515 "route": "HomeRoute"
516 }
517}"#;
518 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
519 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
520 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
521 data: Annotated::new(SpanData {
522 route: Annotated::new(Route {
523 name: Annotated::new("HomeRoute".into()),
524 ..Default::default()
525 }),
526 ..Default::default()
527 }),
528 ..Default::default()
529 })));
530
531 assert_eq!(context, Annotated::from_json(json).unwrap());
532 }
533}