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::{EventId, 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
69/// Error for an invalid trace ID.
70#[derive(Debug)]
71pub enum InvalidTraceId {
72    /// The trace ID is all zeros.
73    Nil,
74    /// The trace ID is syntactically invalid.
75    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/// A 16-character hex string as described in the W3C trace context spec, stored
195/// internally as an array of 8 bytes.
196#[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    /// Derives a [`SpanId`] deterministically from a [`TraceId`].
208    ///
209    /// ```
210    /// # use relay_event_schema::protocol::{SpanId, TraceId};
211    /// #
212    /// let trace_id: TraceId = "515539018c9b4260a6f999572f1661ee".parse().unwrap();
213    /// let span_id = SpanId::derive_from_trace_id(&trace_id);
214    /// assert_eq!(span_id, "515539018c9b4260".parse().unwrap());
215    ///
216    /// let trace_id: TraceId = "00000000000000000000000000000001".parse().unwrap();
217    /// let span_id = SpanId::derive_from_trace_id(&trace_id);
218    /// assert_eq!(span_id, "0000000000000001".parse().unwrap());
219    /// ```
220    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        // A trace id may never be nil, this means either the first or the second half needs to
225        // contain at least one non-zero value, making the resulting span id valid.
226        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/// Trace context
335#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
336#[metastructure(process_func = "process_trace_context")]
337pub struct TraceContext {
338    /// The trace ID.
339    #[metastructure(required = true)]
340    pub trace_id: Annotated<TraceId>,
341
342    /// The ID of the span.
343    #[metastructure(required = true)]
344    pub span_id: Annotated<SpanId>,
345
346    /// The ID of the span enclosing this span.
347    pub parent_span_id: Annotated<SpanId>,
348
349    /// Span type (see `OperationType` docs).
350    #[metastructure(max_chars = 128)]
351    pub op: Annotated<OperationType>,
352
353    /// Whether the trace failed or succeeded. Currently only used to indicate status of individual
354    /// transactions.
355    pub status: Annotated<SpanStatus>,
356
357    /// The amount of time in milliseconds spent in this transaction span,
358    /// excluding its immediate child spans.
359    pub exclusive_time: Annotated<f64>,
360
361    /// The client-side sample rate as reported in the envelope's `trace.sample_rate` header.
362    ///
363    /// The server takes this field from envelope headers and writes it back into the event. Clients
364    /// should not ever send this value.
365    pub client_sample_rate: Annotated<f64>,
366
367    /// The origin of the trace indicates what created the trace (see [OriginType] docs).
368    #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
369    pub origin: Annotated<OriginType>,
370
371    /// Track whether the trace connected to this event has been sampled entirely.
372    ///
373    /// This flag only applies to events with [`Error`] type that have an associated dynamic sampling context.
374    pub sampled: Annotated<bool>,
375
376    /// Data of the trace's root span.
377    #[metastructure(pii = "maybe", skip_serialization = "null")]
378    pub data: Annotated<SpanData>,
379
380    /// Links to other spans from the trace's root span.
381    #[metastructure(pii = "maybe", skip_serialization = "null")]
382    pub links: Annotated<Array<SpanLink>>,
383
384    /// Additional arbitrary fields for forwards compatibility.
385    #[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        // Test valid hex string
428        let trace_id: TraceId = "4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap();
429        assert_eq!(trace_id.as_u128(), 0x4c79f60c11214eb38604f4ae0781bfb2);
430
431        // Test empty string (should return 0)
432        let empty_trace_id: Result<TraceId, _> = "".parse();
433        assert!(empty_trace_id.is_err());
434
435        // Test string with invalid length (should return 0)
436        let short_trace_id: Result<TraceId, _> = "4c79f60c11214eb38604f4ae0781bfb".parse(); // 31 chars
437        assert!(short_trace_id.is_err());
438
439        let long_trace_id: Result<TraceId, _> = "4c79f60c11214eb38604f4ae0781bfb2a".parse(); // 33 chars
440        assert!(long_trace_id.is_err());
441
442        // Test string with invalid hex characters (should return 0)
443        let invalid_trace_id: Result<TraceId, _> = "4c79f60c11214eb38604f4ae0781bfbg".parse(); // 'g' is not a hex char
444        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            // Test case 1: Formatting with hyphens in input
562            (
563                r#"{
564  "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
565  "type": "trace"
566}"#,
567                "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
568                true,
569            ),
570            // Test case 2: Parsing with hyphens in JSON
571            (
572                r#"{
573  "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
574  "type": "trace"
575}"#,
576                "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
577                false,
578            ),
579            // Test case 3: Uppercase in input
580            (
581                r#"{
582  "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
583  "type": "trace"
584}"#,
585                "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
586                true,
587            ),
588            // Test case 4: Uppercase in JSON
589            (
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        // Test valid string
642        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        // Test invalid string (should return random + remark)
651        let invalid_str = "invalid";
652        let annotated = TraceId::try_from_str_or_random(invalid_str);
653        assert!(annotated.value().is_some()); // Random trace ID
654        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        // Test empty string (should return random + remark)
660        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        // Test valid slice
667        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        // Test invalid slice length
675        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}