Skip to main content

relay_event_schema/protocol/contexts/
trace.rs

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/// Represents a W3C Trace Context `trace-id`.
15///
16/// The `trace-id` is a globally unique identifier for a distributed trace,
17/// used to correlate requests across service boundaries.
18///
19/// Format:
20/// - 16-byte array (128 bits), represented as 32-character hexadecimal string
21/// - Example: `"4bf92f3577b34da6a3ce929d0e0e4736"`
22/// - MUST NOT be all zeros (`"00000000000000000000000000000000"`)
23/// - MUST contain only hex digits (`0-9`, `a-f`, `A-F`)
24///
25/// Our implementation allows uppercase hexadecimal characters for backward compatibility, even
26/// though the original spec only allows lowercase hexadecimal characters.
27///
28/// See: <https://www.w3.org/TR/trace-context/#trace-id>
29#[derive(Clone, Copy, PartialEq, Empty, ProcessValue)]
30pub struct TraceId(Uuid);
31
32impl TraceId {
33    /// Creates a new, random, trace id.
34    pub fn random() -> Self {
35        Self(Uuid::new_v4())
36    }
37
38    /// Parses a [`TraceId`] from a value that can be converted into one.
39    ///
40    /// If parsing fails, uses a [`Self::random`] id instead and adds a remark.
41    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    /// Parses a [`TraceId`] from a slice, if it fails uses a [`Self::random`] id instead.
57    pub fn try_from_slice_or_random(value: &[u8]) -> Annotated<Self> {
58        Self::try_from_or_random(value)
59    }
60
61    /// Parses a [`TraceId`] from a string, if it fails uses a [`Self::random`] id instead.
62    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/// A 16-character hex string as described in the W3C trace context spec, stored
171/// internally as an array of 8 bytes.
172#[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/// Trace context
286#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
287#[metastructure(process_func = "process_trace_context")]
288pub struct TraceContext {
289    /// The trace ID.
290    #[metastructure(required = true)]
291    pub trace_id: Annotated<TraceId>,
292
293    /// The ID of the span.
294    #[metastructure(required = true)]
295    pub span_id: Annotated<SpanId>,
296
297    /// The ID of the span enclosing this span.
298    pub parent_span_id: Annotated<SpanId>,
299
300    /// Span type (see `OperationType` docs).
301    #[metastructure(max_chars = 128)]
302    pub op: Annotated<OperationType>,
303
304    /// Whether the trace failed or succeeded. Currently only used to indicate status of individual
305    /// transactions.
306    pub status: Annotated<SpanStatus>,
307
308    /// The amount of time in milliseconds spent in this transaction span,
309    /// excluding its immediate child spans.
310    pub exclusive_time: Annotated<f64>,
311
312    /// The client-side sample rate as reported in the envelope's `trace.sample_rate` header.
313    ///
314    /// The server takes this field from envelope headers and writes it back into the event. Clients
315    /// should not ever send this value.
316    pub client_sample_rate: Annotated<f64>,
317
318    /// The origin of the trace indicates what created the trace (see [OriginType] docs).
319    #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
320    pub origin: Annotated<OriginType>,
321
322    /// Track whether the trace connected to this event has been sampled entirely.
323    ///
324    /// This flag only applies to events with [`Error`] type that have an associated dynamic sampling context.
325    pub sampled: Annotated<bool>,
326
327    /// Data of the trace's root span.
328    #[metastructure(pii = "maybe", skip_serialization = "null")]
329    pub data: Annotated<SpanData>,
330
331    /// Links to other spans from the trace's root span.
332    #[metastructure(pii = "maybe", skip_serialization = "null")]
333    pub links: Annotated<Array<SpanLink>>,
334
335    /// Additional arbitrary fields for forwards compatibility.
336    #[metastructure(additional_properties, retain = true, pii = "maybe")]
337    pub other: Object<Value>,
338}
339
340impl TraceContext {
341    /// Generates a random [`SpanId`] and takes `[TraceId]` from the event's UUID.
342    /// Leaves all other fields blank.
343    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        // Test valid hex string
396        let trace_id: TraceId = "4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap();
397        assert_eq!(trace_id.as_u128(), 0x4c79f60c11214eb38604f4ae0781bfb2);
398
399        // Test empty string (should return 0)
400        let empty_trace_id: Result<TraceId, Error> = "".parse();
401        assert!(empty_trace_id.is_err());
402
403        // Test string with invalid length (should return 0)
404        let short_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb".parse(); // 31 chars
405        assert!(short_trace_id.is_err());
406
407        let long_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb2a".parse(); // 33 chars
408        assert!(long_trace_id.is_err());
409
410        // Test string with invalid hex characters (should return 0)
411        let invalid_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfbg".parse(); // 'g' is not a hex char
412        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            // Test case 1: Formatting with hyphens in input
530            (
531                r#"{
532  "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
533  "type": "trace"
534}"#,
535                "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
536                true,
537            ),
538            // Test case 2: Parsing with hyphens in JSON
539            (
540                r#"{
541  "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
542  "type": "trace"
543}"#,
544                "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
545                false,
546            ),
547            // Test case 3: Uppercase in input
548            (
549                r#"{
550  "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
551  "type": "trace"
552}"#,
553                "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
554                true,
555            ),
556            // Test case 4: Uppercase in JSON
557            (
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        // Test valid string
610        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        // Test invalid string (should return random + remark)
619        let invalid_str = "invalid";
620        let annotated = TraceId::try_from_str_or_random(invalid_str);
621        assert!(annotated.value().is_some()); // Random trace ID
622        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        // Test empty string (should return random + remark)
628        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        // Test valid slice
635        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        // Test invalid slice length
643        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}