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#[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_slice_or_random(value: &[u8]) -> Annotated<Self> {
42 TraceId::try_from(value)
43 .map(Annotated::new)
44 .unwrap_or_else(|_| {
45 let mut meta = Meta::default();
46 let rule_id = match value.is_empty() {
47 true => "trace_id.missing",
48 false => "trace_id.invalid",
49 };
50 meta.add_remark(Remark::new(RemarkType::Substituted, rule_id));
51 Annotated(Some(TraceId::random()), meta)
52 })
53 }
54}
55
56relay_common::impl_str_serde!(TraceId, "a trace identifier");
57
58impl FromStr for TraceId {
59 type Err = Error;
60
61 fn from_str(s: &str) -> Result<Self, Self::Err> {
62 Uuid::parse_str(s)
63 .map(Into::into)
64 .map_err(|_| Error::invalid("the trace id is not valid"))
65 }
66}
67
68impl TryFrom<&[u8]> for TraceId {
69 type Error = Error;
70
71 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
72 let uuid =
73 Uuid::from_slice(value).map_err(|_| Error::invalid("the trace id is not valid"))?;
74 Ok(Self(uuid))
75 }
76}
77
78impl fmt::Display for TraceId {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "{}", self.0.as_simple())
81 }
82}
83
84impl fmt::Debug for TraceId {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "TraceId(\"{}\")", self.0.as_simple())
87 }
88}
89
90impl From<Uuid> for TraceId {
91 fn from(uuid: Uuid) -> Self {
92 TraceId(uuid)
93 }
94}
95
96impl Deref for TraceId {
97 type Target = Uuid;
98
99 fn deref(&self) -> &Self::Target {
100 &self.0
101 }
102}
103
104impl FromValue for TraceId {
105 fn from_value(value: Annotated<Value>) -> Annotated<Self>
106 where
107 Self: Sized,
108 {
109 match value {
110 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
111 Ok(trace_id) => Annotated(Some(trace_id), meta),
112 Err(_) => {
113 meta.add_error(Error::invalid("not a valid trace id"));
114 meta.set_original_value(Some(value));
115 Annotated(None, meta)
116 }
117 },
118 Annotated(None, meta) => Annotated(None, meta),
119 Annotated(Some(value), mut meta) => {
120 meta.add_error(Error::expected("trace id"));
121 meta.set_original_value(Some(value));
122 Annotated(None, meta)
123 }
124 }
125 }
126}
127
128impl IntoValue for TraceId {
129 fn into_value(self) -> Value
130 where
131 Self: Sized,
132 {
133 Value::String(self.to_string())
134 }
135
136 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
137 where
138 Self: Sized,
139 S: Serializer,
140 {
141 s.collect_str(self)
142 }
143}
144
145#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)]
148pub struct SpanId([u8; 8]);
149
150relay_common::impl_str_serde!(SpanId, "a span identifier");
151
152impl FromStr for SpanId {
153 type Err = Error;
154
155 fn from_str(s: &str) -> Result<Self, Self::Err> {
156 match u64::from_str_radix(s, 16) {
157 Ok(id) if s.len() == 16 && id > 0 => Ok(Self(id.to_be_bytes())),
158 _ => Err(Error::invalid("not a valid span id")),
159 }
160 }
161}
162
163impl TryFrom<&[u8]> for SpanId {
164 type Error = Error;
165
166 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
167 match <[u8; 8]>::try_from(value) {
168 Ok(bytes) if !bytes.iter().all(|&x| x == 0) => Ok(Self(bytes)),
169 _ => Err(Error::invalid("not a valid span id")),
170 }
171 }
172}
173
174impl fmt::Debug for SpanId {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(f, "SpanId(\"")?;
177 for b in self.0 {
178 write!(f, "{b:02x}")?;
179 }
180 write!(f, "\")")
181 }
182}
183
184impl fmt::Display for SpanId {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 for b in self.0 {
187 write!(f, "{b:02x}")?;
188 }
189 Ok(())
190 }
191}
192
193impl FromValue for SpanId {
194 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
195 match value {
196 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
197 Ok(span_id) => Annotated::new(span_id),
198 Err(e) => {
199 meta.add_error(e);
200 meta.set_original_value(Some(value));
201 Annotated(None, meta)
202 }
203 },
204 Annotated(None, meta) => Annotated(None, meta),
205 Annotated(Some(value), mut meta) => {
206 meta.add_error(Error::expected("span id"));
207 meta.set_original_value(Some(value));
208 Annotated(None, meta)
209 }
210 }
211 }
212}
213
214impl Empty for SpanId {
215 fn is_empty(&self) -> bool {
216 false
217 }
218}
219
220impl IntoValue for SpanId {
221 fn into_value(self) -> Value
222 where
223 Self: Sized,
224 {
225 Value::String(self.to_string())
226 }
227
228 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
229 where
230 Self: Sized,
231 S: serde::Serializer,
232 {
233 s.collect_str(self)
234 }
235}
236
237impl ProcessValue for SpanId {}
238
239impl std::ops::Deref for SpanId {
240 type Target = [u8];
241
242 fn deref(&self) -> &Self::Target {
243 &self.0
244 }
245}
246
247impl<'a> From<&'a SpanId> for Val<'a> {
248 fn from(value: &'a SpanId) -> Self {
249 Val::HexId(HexId(&value.0))
250 }
251}
252
253#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
255#[metastructure(process_func = "process_trace_context")]
256pub struct TraceContext {
257 #[metastructure(required = true)]
259 pub trace_id: Annotated<TraceId>,
260
261 #[metastructure(required = true)]
263 pub span_id: Annotated<SpanId>,
264
265 pub parent_span_id: Annotated<SpanId>,
267
268 #[metastructure(max_chars = 128)]
270 pub op: Annotated<OperationType>,
271
272 pub status: Annotated<SpanStatus>,
275
276 pub exclusive_time: Annotated<f64>,
279
280 pub client_sample_rate: Annotated<f64>,
285
286 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
288 pub origin: Annotated<OriginType>,
289
290 pub sampled: Annotated<bool>,
294
295 #[metastructure(pii = "maybe", skip_serialization = "null")]
297 pub data: Annotated<SpanData>,
298
299 #[metastructure(pii = "maybe", skip_serialization = "null")]
301 pub links: Annotated<Array<SpanLink>>,
302
303 #[metastructure(additional_properties, retain = true, pii = "maybe")]
305 pub other: Object<Value>,
306}
307
308impl super::DefaultContext for TraceContext {
309 fn default_key() -> &'static str {
310 "trace"
311 }
312
313 fn from_context(context: super::Context) -> Option<Self> {
314 match context {
315 super::Context::Trace(c) => Some(*c),
316 _ => None,
317 }
318 }
319
320 fn cast(context: &super::Context) -> Option<&Self> {
321 match context {
322 super::Context::Trace(c) => Some(c),
323 _ => None,
324 }
325 }
326
327 fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
328 match context {
329 super::Context::Trace(c) => Some(c),
330 _ => None,
331 }
332 }
333
334 fn into_context(self) -> super::Context {
335 super::Context::Trace(Box::new(self))
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::protocol::{Context, Route};
343
344 #[test]
345 fn test_trace_id_as_u128() {
346 let trace_id: TraceId = "4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap();
348 assert_eq!(trace_id.as_u128(), 0x4c79f60c11214eb38604f4ae0781bfb2);
349
350 let empty_trace_id: Result<TraceId, Error> = "".parse();
352 assert!(empty_trace_id.is_err());
353
354 let short_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb".parse(); assert!(short_trace_id.is_err());
357
358 let long_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb2a".parse(); assert!(long_trace_id.is_err());
360
361 let invalid_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfbg".parse(); assert!(invalid_trace_id.is_err());
364 }
365
366 #[test]
367 fn test_trace_context_roundtrip() {
368 let json = r#"{
369 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
370 "span_id": "fa90fdead5f74052",
371 "parent_span_id": "fa90fdead5f74053",
372 "op": "http",
373 "status": "ok",
374 "exclusive_time": 0.0,
375 "client_sample_rate": 0.5,
376 "origin": "auto.http",
377 "data": {
378 "route": {
379 "name": "/users",
380 "params": {
381 "tok": "test"
382 },
383 "custom_field": "something"
384 },
385 "custom_field_empty": ""
386 },
387 "links": [
388 {
389 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
390 "span_id": "ea90fdead5f74052",
391 "sampled": true,
392 "attributes": {
393 "sentry.link.type": "previous_trace"
394 }
395 }
396 ],
397 "other": "value",
398 "type": "trace"
399}"#;
400 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
401 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
402 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
403 parent_span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
404 op: Annotated::new("http".into()),
405 status: Annotated::new(SpanStatus::Ok),
406 exclusive_time: Annotated::new(0.0),
407 client_sample_rate: Annotated::new(0.5),
408 origin: Annotated::new("auto.http".to_owned()),
409 data: Annotated::new(SpanData {
410 route: Annotated::new(Route {
411 name: Annotated::new("/users".into()),
412 params: Annotated::new({
413 let mut map = Object::new();
414 map.insert(
415 "tok".to_owned(),
416 Annotated::new(Value::String("test".into())),
417 );
418 map
419 }),
420 other: Object::from([(
421 "custom_field".into(),
422 Annotated::new(Value::String("something".into())),
423 )]),
424 }),
425 other: Object::from([(
426 "custom_field_empty".into(),
427 Annotated::new(Value::String("".into())),
428 )]),
429 ..Default::default()
430 }),
431 links: Annotated::new(Array::from(vec![Annotated::new(SpanLink {
432 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
433 span_id: Annotated::new("ea90fdead5f74052".parse().unwrap()),
434 sampled: Annotated::new(true),
435 attributes: Annotated::new({
436 let mut map: std::collections::BTreeMap<String, Annotated<Value>> =
437 Object::new();
438 map.insert(
439 "sentry.link.type".into(),
440 Annotated::new(Value::String("previous_trace".into())),
441 );
442 map
443 }),
444 ..Default::default()
445 })])),
446 other: {
447 let mut map = Object::new();
448 map.insert(
449 "other".to_owned(),
450 Annotated::new(Value::String("value".to_owned())),
451 );
452 map
453 },
454 sampled: Annotated::empty(),
455 })));
456
457 assert_eq!(context, Annotated::from_json(json).unwrap());
458 assert_eq!(json, context.to_json_pretty().unwrap());
459 }
460
461 #[test]
462 fn test_trace_context_normalization() {
463 let json = r#"{
464 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
465 "span_id": "FA90FDEAD5F74052",
466 "type": "trace"
467}"#;
468 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
469 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
470 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
471 ..Default::default()
472 })));
473
474 assert_eq!(context, Annotated::from_json(json).unwrap());
475 }
476
477 #[test]
478 fn test_trace_id_formatting() {
479 let test_cases = [
480 (
482 r#"{
483 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
484 "type": "trace"
485}"#,
486 "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
487 true,
488 ),
489 (
491 r#"{
492 "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
493 "type": "trace"
494}"#,
495 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
496 false,
497 ),
498 (
500 r#"{
501 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
502 "type": "trace"
503}"#,
504 "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
505 true,
506 ),
507 (
509 r#"{
510 "trace_id": "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
511 "type": "trace"
512}"#,
513 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
514 false,
515 ),
516 ];
517
518 for (json, trace_id_str, is_to_json) in test_cases {
519 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
520 trace_id: Annotated::new(trace_id_str.parse().unwrap()),
521 ..Default::default()
522 })));
523
524 if is_to_json {
525 assert_eq!(json, context.to_json_pretty().unwrap());
526 } else {
527 assert_eq!(context, Annotated::from_json(json).unwrap());
528 }
529 }
530 }
531
532 #[test]
533 fn test_trace_context_with_routes() {
534 let json = r#"{
535 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
536 "span_id": "FA90FDEAD5F74052",
537 "type": "trace",
538 "data": {
539 "route": "HomeRoute"
540 }
541}"#;
542 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
543 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
544 span_id: Annotated::new("fa90fdead5f74052".parse().unwrap()),
545 data: Annotated::new(SpanData {
546 route: Annotated::new(Route {
547 name: Annotated::new("HomeRoute".into()),
548 ..Default::default()
549 }),
550 ..Default::default()
551 }),
552 ..Default::default()
553 })));
554
555 assert_eq!(context, Annotated::from_json(json).unwrap());
556 }
557}