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