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