1use relay_protocol::{
2 Annotated, Array, Empty, Error, FromValue, HexId, IntoValue, Meta, Object, Remark, RemarkType,
3 SkipSerialization, Val, 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, PartialEq, Empty, ProcessValue)]
30pub struct TraceId(Uuid);
31
32impl TraceId {
33 pub fn random() -> Self {
35 Self(Uuid::new_v4())
36 }
37
38 pub fn try_from_or_random<T>(value: T) -> Annotated<Self>
42 where
43 T: TryInto<Self> + AsRef<[u8]> + Copy,
44 {
45 value.try_into().map(Annotated::new).unwrap_or_else(|_| {
46 let mut meta = Meta::default();
47 let rule_id = match value.as_ref().is_empty() {
48 true => "trace_id.missing",
49 false => "trace_id.invalid",
50 };
51 meta.add_remark(Remark::new(RemarkType::Substituted, rule_id));
52 Annotated(Some(TraceId::random()), meta)
53 })
54 }
55
56 pub fn try_from_slice_or_random(value: &[u8]) -> Annotated<Self> {
58 Self::try_from_or_random(value)
59 }
60
61 pub fn try_from_str_or_random(value: &str) -> Annotated<Self> {
63 Self::try_from_or_random(value)
64 }
65}
66
67relay_common::impl_str_serde!(TraceId, "a trace identifier");
68
69impl FromStr for TraceId {
70 type Err = Error;
71
72 fn from_str(s: &str) -> Result<Self, Self::Err> {
73 Uuid::parse_str(s)
74 .map(Into::into)
75 .map_err(|_| Error::invalid("the trace id is not valid"))
76 }
77}
78
79impl TryFrom<&str> for TraceId {
80 type Error = Error;
81
82 fn try_from(value: &str) -> Result<Self, Self::Error> {
83 value.parse()
84 }
85}
86
87impl TryFrom<&[u8]> for TraceId {
88 type Error = Error;
89
90 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
91 let uuid =
92 Uuid::from_slice(value).map_err(|_| Error::invalid("the trace id is not valid"))?;
93 Ok(Self(uuid))
94 }
95}
96
97impl fmt::Display for TraceId {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "{}", self.0.as_simple())
100 }
101}
102
103impl fmt::Debug for TraceId {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "TraceId(\"{}\")", self.0.as_simple())
106 }
107}
108
109impl From<Uuid> for TraceId {
110 fn from(uuid: Uuid) -> Self {
111 TraceId(uuid)
112 }
113}
114
115impl From<TraceId> for Uuid {
116 fn from(trace_id: TraceId) -> Self {
117 trace_id.0
118 }
119}
120
121impl Deref for TraceId {
122 type Target = Uuid;
123
124 fn deref(&self) -> &Self::Target {
125 &self.0
126 }
127}
128
129impl FromValue for TraceId {
130 fn from_value(value: Annotated<Value>) -> Annotated<Self>
131 where
132 Self: Sized,
133 {
134 match value {
135 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
136 Ok(trace_id) => Annotated(Some(trace_id), meta),
137 Err(_) => {
138 meta.add_error(Error::invalid("not a valid trace id"));
139 meta.set_original_value(Some(value));
140 Annotated(None, meta)
141 }
142 },
143 Annotated(None, meta) => Annotated(None, meta),
144 Annotated(Some(value), mut meta) => {
145 meta.add_error(Error::expected("trace id"));
146 meta.set_original_value(Some(value));
147 Annotated(None, meta)
148 }
149 }
150 }
151}
152
153impl IntoValue for TraceId {
154 fn into_value(self) -> Value
155 where
156 Self: Sized,
157 {
158 Value::String(self.to_string())
159 }
160
161 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
162 where
163 Self: Sized,
164 S: Serializer,
165 {
166 s.collect_str(self)
167 }
168}
169
170#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)]
173pub struct SpanId([u8; 8]);
174
175relay_common::impl_str_serde!(SpanId, "a span identifier");
176
177impl SpanId {
178 pub fn random() -> Self {
179 let value: u64 = rand::random_range(1..=u64::MAX);
180 Self(value.to_ne_bytes())
181 }
182}
183
184impl FromStr for SpanId {
185 type Err = Error;
186
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 match u64::from_str_radix(s, 16) {
189 Ok(id) if s.len() == 16 && id > 0 => Ok(Self(id.to_be_bytes())),
190 _ => Err(Error::invalid("not a valid span id")),
191 }
192 }
193}
194
195impl TryFrom<&[u8]> for SpanId {
196 type Error = Error;
197
198 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
199 match <[u8; 8]>::try_from(value) {
200 Ok(bytes) if !bytes.iter().all(|&x| x == 0) => Ok(Self(bytes)),
201 _ => Err(Error::invalid("not a valid span id")),
202 }
203 }
204}
205
206impl fmt::Debug for SpanId {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 write!(f, "SpanId(\"")?;
209 for b in self.0 {
210 write!(f, "{b:02x}")?;
211 }
212 write!(f, "\")")
213 }
214}
215
216impl fmt::Display for SpanId {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 for b in self.0 {
219 write!(f, "{b:02x}")?;
220 }
221 Ok(())
222 }
223}
224
225impl FromValue for SpanId {
226 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
227 match value {
228 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
229 Ok(span_id) => Annotated::new(span_id),
230 Err(e) => {
231 meta.add_error(e);
232 meta.set_original_value(Some(value));
233 Annotated(None, meta)
234 }
235 },
236 Annotated(None, meta) => Annotated(None, meta),
237 Annotated(Some(value), mut meta) => {
238 meta.add_error(Error::expected("span id"));
239 meta.set_original_value(Some(value));
240 Annotated(None, meta)
241 }
242 }
243 }
244}
245
246impl Empty for SpanId {
247 fn is_empty(&self) -> bool {
248 false
249 }
250}
251
252impl IntoValue for SpanId {
253 fn into_value(self) -> Value
254 where
255 Self: Sized,
256 {
257 Value::String(self.to_string())
258 }
259
260 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
261 where
262 Self: Sized,
263 S: serde::Serializer,
264 {
265 s.collect_str(self)
266 }
267}
268
269impl ProcessValue for SpanId {}
270
271impl std::ops::Deref for SpanId {
272 type Target = [u8];
273
274 fn deref(&self) -> &Self::Target {
275 &self.0
276 }
277}
278
279impl<'a> From<&'a SpanId> for Val<'a> {
280 fn from(value: &'a SpanId) -> Self {
281 Val::HexId(HexId(&value.0))
282 }
283}
284
285#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
287#[metastructure(process_func = "process_trace_context")]
288pub struct TraceContext {
289 #[metastructure(required = true)]
291 pub trace_id: Annotated<TraceId>,
292
293 #[metastructure(required = true)]
295 pub span_id: Annotated<SpanId>,
296
297 pub parent_span_id: Annotated<SpanId>,
299
300 #[metastructure(max_chars = 128)]
302 pub op: Annotated<OperationType>,
303
304 pub status: Annotated<SpanStatus>,
307
308 pub exclusive_time: Annotated<f64>,
311
312 pub client_sample_rate: Annotated<f64>,
317
318 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
320 pub origin: Annotated<OriginType>,
321
322 pub sampled: Annotated<bool>,
326
327 #[metastructure(pii = "maybe", skip_serialization = "null")]
329 pub data: Annotated<SpanData>,
330
331 #[metastructure(pii = "maybe", skip_serialization = "null")]
333 pub links: Annotated<Array<SpanLink>>,
334
335 #[metastructure(additional_properties, retain = true, pii = "maybe")]
337 pub other: Object<Value>,
338}
339
340impl TraceContext {
341 pub fn random(event_id: Uuid) -> Self {
344 let mut trace_meta = Meta::default();
345 trace_meta.add_remark(Remark::new(RemarkType::Substituted, "trace_id.missing"));
346
347 let mut span_meta = Meta::default();
348 span_meta.add_remark(Remark::new(RemarkType::Substituted, "span_id.missing"));
349 TraceContext {
350 trace_id: Annotated(Some(TraceId::from(event_id)), trace_meta),
351 span_id: Annotated(Some(SpanId::random()), span_meta),
352 ..Default::default()
353 }
354 }
355}
356
357impl super::DefaultContext for TraceContext {
358 fn default_key() -> &'static str {
359 "trace"
360 }
361
362 fn from_context(context: super::Context) -> Option<Self> {
363 match context {
364 super::Context::Trace(c) => Some(*c),
365 _ => None,
366 }
367 }
368
369 fn cast(context: &super::Context) -> Option<&Self> {
370 match context {
371 super::Context::Trace(c) => Some(c),
372 _ => None,
373 }
374 }
375
376 fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
377 match context {
378 super::Context::Trace(c) => Some(c),
379 _ => None,
380 }
381 }
382
383 fn into_context(self) -> super::Context {
384 super::Context::Trace(Box::new(self))
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::protocol::{Context, Route};
392
393 #[test]
394 fn test_trace_id_as_u128() {
395 let trace_id: TraceId = "4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap();
397 assert_eq!(trace_id.as_u128(), 0x4c79f60c11214eb38604f4ae0781bfb2);
398
399 let empty_trace_id: Result<TraceId, Error> = "".parse();
401 assert!(empty_trace_id.is_err());
402
403 let short_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb".parse(); assert!(short_trace_id.is_err());
406
407 let long_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb2a".parse(); assert!(long_trace_id.is_err());
409
410 let invalid_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfbg".parse(); assert!(invalid_trace_id.is_err());
413 }
414
415 #[test]
416 fn test_trace_context_roundtrip() {
417 let json = r#"{
418 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
419 "span_id": "fa90fdead5f74052",
420 "parent_span_id": "fa90fdead5f74053",
421 "op": "http",
422 "status": "ok",
423 "exclusive_time": 0.0,
424 "client_sample_rate": 0.5,
425 "origin": "auto.http",
426 "data": {
427 "route": {
428 "name": "/users",
429 "params": {
430 "tok": "test"
431 },
432 "custom_field": "something"
433 },
434 "custom_field_empty": ""
435 },
436 "links": [
437 {
438 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
439 "span_id": "ea90fdead5f74052",
440 "sampled": true,
441 "attributes": {
442 "sentry.link.type": "previous_trace"
443 }
444 }
445 ],
446 "other": "value",
447 "type": "trace"
448}"#;
449 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
450 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
451 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
452 parent_span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
453 op: Annotated::new("http".into()),
454 status: Annotated::new(SpanStatus::Ok),
455 exclusive_time: Annotated::new(0.0),
456 client_sample_rate: Annotated::new(0.5),
457 origin: Annotated::new("auto.http".to_owned()),
458 data: Annotated::new(SpanData {
459 route: Annotated::new(Route {
460 name: Annotated::new("/users".into()),
461 params: Annotated::new({
462 let mut map = Object::new();
463 map.insert(
464 "tok".to_owned(),
465 Annotated::new(Value::String("test".into())),
466 );
467 map
468 }),
469 other: Object::from([(
470 "custom_field".into(),
471 Annotated::new(Value::String("something".into())),
472 )]),
473 }),
474 other: Object::from([(
475 "custom_field_empty".into(),
476 Annotated::new(Value::String("".into())),
477 )]),
478 ..Default::default()
479 }),
480 links: Annotated::new(Array::from(vec![Annotated::new(SpanLink {
481 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
482 span_id: Annotated::new("ea90fdead5f74052".parse().unwrap()),
483 sampled: Annotated::new(true),
484 attributes: Annotated::new({
485 let mut map: std::collections::BTreeMap<String, Annotated<Value>> =
486 Object::new();
487 map.insert(
488 "sentry.link.type".into(),
489 Annotated::new(Value::String("previous_trace".into())),
490 );
491 map
492 }),
493 ..Default::default()
494 })])),
495 other: {
496 let mut map = Object::new();
497 map.insert(
498 "other".to_owned(),
499 Annotated::new(Value::String("value".to_owned())),
500 );
501 map
502 },
503 sampled: Annotated::empty(),
504 })));
505
506 assert_eq!(context, Annotated::from_json(json).unwrap());
507 assert_eq!(json, context.to_json_pretty().unwrap());
508 }
509
510 #[test]
511 fn test_trace_context_normalization() {
512 let json = r#"{
513 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
514 "span_id": "FA90FDEAD5F74052",
515 "type": "trace"
516}"#;
517 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
518 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
519 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
520 ..Default::default()
521 })));
522
523 assert_eq!(context, Annotated::from_json(json).unwrap());
524 }
525
526 #[test]
527 fn test_trace_id_formatting() {
528 let test_cases = [
529 (
531 r#"{
532 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
533 "type": "trace"
534}"#,
535 "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
536 true,
537 ),
538 (
540 r#"{
541 "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
542 "type": "trace"
543}"#,
544 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
545 false,
546 ),
547 (
549 r#"{
550 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
551 "type": "trace"
552}"#,
553 "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
554 true,
555 ),
556 (
558 r#"{
559 "trace_id": "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
560 "type": "trace"
561}"#,
562 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
563 false,
564 ),
565 ];
566
567 for (json, trace_id_str, is_to_json) in test_cases {
568 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
569 trace_id: Annotated::new(trace_id_str.parse().unwrap()),
570 ..Default::default()
571 })));
572
573 if is_to_json {
574 assert_eq!(json, context.to_json_pretty().unwrap());
575 } else {
576 assert_eq!(context, Annotated::from_json(json).unwrap());
577 }
578 }
579 }
580
581 #[test]
582 fn test_trace_context_with_routes() {
583 let json = r#"{
584 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
585 "span_id": "FA90FDEAD5F74052",
586 "type": "trace",
587 "data": {
588 "route": "HomeRoute"
589 }
590}"#;
591 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
592 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
593 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
594 data: Annotated::new(SpanData {
595 route: Annotated::new(Route {
596 name: Annotated::new("HomeRoute".into()),
597 ..Default::default()
598 }),
599 ..Default::default()
600 }),
601 ..Default::default()
602 })));
603
604 assert_eq!(context, Annotated::from_json(json).unwrap());
605 }
606
607 #[test]
608 fn test_try_from_or_random() {
609 let valid_str = "4c79f60c11214eb38604f4ae0781bfb2";
611 let annotated = TraceId::try_from_str_or_random(valid_str);
612 assert_eq!(
613 annotated.value().unwrap().as_u128(),
614 0x4c79f60c11214eb38604f4ae0781bfb2
615 );
616 assert!(annotated.meta().is_empty());
617
618 let invalid_str = "invalid";
620 let annotated = TraceId::try_from_str_or_random(invalid_str);
621 assert!(annotated.value().is_some()); assert_ne!(annotated.value().unwrap().as_u128(), 0);
623 assert_eq!(annotated.meta().iter_remarks().count(), 1);
624 let remark = annotated.meta().iter_remarks().next().unwrap();
625 assert_eq!(remark.rule_id(), "trace_id.invalid");
626
627 let empty_str = "";
629 let annotated = TraceId::try_from_str_or_random(empty_str);
630 assert!(annotated.value().is_some());
631 let remark = annotated.meta().iter_remarks().next().unwrap();
632 assert_eq!(remark.rule_id(), "trace_id.missing");
633
634 let valid_bytes = b"\x4c\x79\xf6\x0c\x11\x21\x4e\xb3\x86\x04\xf4\xae\x07\x81\xbf\xb2";
636 let annotated = TraceId::try_from_slice_or_random(valid_bytes.as_slice());
637 assert_eq!(
638 annotated.value().unwrap().as_u128(),
639 0x4c79f60c11214eb38604f4ae0781bfb2
640 );
641
642 let invalid_bytes = b"\x00";
644 let annotated = TraceId::try_from_slice_or_random(invalid_bytes.as_slice());
645 let remark = annotated.meta().iter_remarks().next().unwrap();
646 assert_eq!(remark.rule_id(), "trace_id.invalid");
647 }
648
649 #[test]
650 fn test_random_trace_context() {
651 let rand_context = TraceContext::random(Uuid::new_v4());
652 assert!(rand_context.trace_id.value().is_some());
653 assert_eq!(
654 rand_context
655 .trace_id
656 .meta()
657 .iter_remarks()
658 .next()
659 .unwrap()
660 .rule_id(),
661 "trace_id.missing"
662 );
663 assert!(rand_context.span_id.value().is_some());
664 assert_eq!(
665 rand_context
666 .span_id
667 .meta()
668 .iter_remarks()
669 .next()
670 .unwrap()
671 .rule_id(),
672 "span_id.missing"
673 );
674 }
675}