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