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