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