1use relay_protocol::{
2 Annotated, Array, Empty, Error, ErrorKind, FromValue, IntoValue, Object, SkipSerialization,
3 Value,
4};
5use serde::{Serialize, 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, Default, PartialEq, Empty, ProcessValue)]
30pub struct TraceId(Uuid);
31
32impl TraceId {
33 pub fn parse_str(input: &str) -> Result<TraceId, Error> {
34 Self::from_str(input)
35 }
36}
37
38relay_common::impl_str_serde!(TraceId, "a trace identifier");
39
40impl FromStr for TraceId {
41 type Err = Error;
42
43 fn from_str(s: &str) -> Result<Self, Self::Err> {
44 Uuid::parse_str(s).map(Into::into).map_err(|_| {
45 Error::with(ErrorKind::InvalidData, |e| {
46 e.insert("reason", "the trace id is not valid");
47 })
48 })
49 }
50}
51
52impl fmt::Display for TraceId {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(f, "{}", self.0.as_simple())
55 }
56}
57
58impl fmt::Debug for TraceId {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "TraceId(\"{}\")", self.0.as_simple())
61 }
62}
63
64impl From<Uuid> for TraceId {
65 fn from(uuid: Uuid) -> Self {
66 TraceId(uuid)
67 }
68}
69
70impl Deref for TraceId {
71 type Target = Uuid;
72
73 fn deref(&self) -> &Self::Target {
74 &self.0
75 }
76}
77
78impl FromValue for TraceId {
79 fn from_value(value: Annotated<Value>) -> Annotated<Self>
80 where
81 Self: Sized,
82 {
83 match value {
84 Annotated(Some(Value::String(value)), mut meta) => match value.parse() {
85 Ok(trace_id) => Annotated(Some(trace_id), meta),
86 Err(_) => {
87 meta.add_error(Error::invalid("not a valid trace id"));
88 meta.set_original_value(Some(value));
89 Annotated(None, meta)
90 }
91 },
92 Annotated(None, meta) => Annotated(None, meta),
93 Annotated(Some(value), mut meta) => {
94 meta.add_error(Error::expected("trace id"));
95 meta.set_original_value(Some(value));
96 Annotated(None, meta)
97 }
98 }
99 }
100}
101
102impl IntoValue for TraceId {
103 fn into_value(self) -> Value
104 where
105 Self: Sized,
106 {
107 Value::String(self.to_string())
108 }
109
110 fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
111 where
112 Self: Sized,
113 S: Serializer,
114 {
115 Serialize::serialize(&self.to_string(), s)
116 }
117}
118
119#[derive(
121 Clone, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Empty, IntoValue, ProcessValue,
122)]
123pub struct SpanId(pub String);
124
125relay_common::impl_str_serde!(SpanId, "a span identifier");
126
127impl FromStr for SpanId {
128 type Err = Error;
129
130 fn from_str(s: &str) -> Result<Self, Self::Err> {
131 Ok(SpanId(s.to_string()))
132 }
133}
134
135impl fmt::Display for SpanId {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}", self.0)
138 }
139}
140
141impl FromValue for SpanId {
142 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
143 match value {
144 Annotated(Some(Value::String(mut value)), mut meta) => {
145 if !is_hex_string(&value, 16) || value.bytes().all(|x| x == b'0') {
146 meta.add_error(Error::invalid("not a valid span id"));
147 meta.set_original_value(Some(value));
148 Annotated(None, meta)
149 } else {
150 value.make_ascii_lowercase();
151 Annotated(Some(SpanId(value)), meta)
152 }
153 }
154 Annotated(None, meta) => Annotated(None, meta),
155 Annotated(Some(value), mut meta) => {
156 meta.add_error(Error::expected("span id"));
157 meta.set_original_value(Some(value));
158 Annotated(None, meta)
159 }
160 }
161 }
162}
163
164impl AsRef<str> for SpanId {
165 fn as_ref(&self) -> &str {
166 &self.0
167 }
168}
169
170fn is_hex_string(string: &str, len: usize) -> bool {
171 string.len() == len && string.bytes().all(|b| b.is_ascii_hexdigit())
172}
173
174#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
176#[metastructure(process_func = "process_trace_context")]
177pub struct TraceContext {
178 #[metastructure(required = true)]
180 pub trace_id: Annotated<TraceId>,
181
182 #[metastructure(required = true)]
184 pub span_id: Annotated<SpanId>,
185
186 pub parent_span_id: Annotated<SpanId>,
188
189 #[metastructure(max_chars = 128)]
191 pub op: Annotated<OperationType>,
192
193 pub status: Annotated<SpanStatus>,
196
197 pub exclusive_time: Annotated<f64>,
200
201 pub client_sample_rate: Annotated<f64>,
206
207 #[metastructure(max_chars = 128, allow_chars = "a-zA-Z0-9_.")]
209 pub origin: Annotated<OriginType>,
210
211 pub sampled: Annotated<bool>,
215
216 #[metastructure(pii = "maybe", skip_serialization = "null")]
218 pub data: Annotated<SpanData>,
219
220 #[metastructure(pii = "maybe", skip_serialization = "null")]
222 pub links: Annotated<Array<SpanLink>>,
223
224 #[metastructure(additional_properties, retain = true, pii = "maybe")]
226 pub other: Object<Value>,
227}
228
229impl super::DefaultContext for TraceContext {
230 fn default_key() -> &'static str {
231 "trace"
232 }
233
234 fn from_context(context: super::Context) -> Option<Self> {
235 match context {
236 super::Context::Trace(c) => Some(*c),
237 _ => None,
238 }
239 }
240
241 fn cast(context: &super::Context) -> Option<&Self> {
242 match context {
243 super::Context::Trace(c) => Some(c),
244 _ => None,
245 }
246 }
247
248 fn cast_mut(context: &mut super::Context) -> Option<&mut Self> {
249 match context {
250 super::Context::Trace(c) => Some(c),
251 _ => None,
252 }
253 }
254
255 fn into_context(self) -> super::Context {
256 super::Context::Trace(Box::new(self))
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::protocol::{Context, Route};
264
265 #[test]
266 fn test_trace_id_as_u128() {
267 let trace_id: TraceId = "4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap();
269 assert_eq!(trace_id.as_u128(), 0x4c79f60c11214eb38604f4ae0781bfb2);
270
271 let empty_trace_id: Result<TraceId, Error> = "".parse();
273 assert!(empty_trace_id.is_err());
274
275 let short_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb".parse(); assert!(short_trace_id.is_err());
278
279 let long_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfb2a".parse(); assert!(long_trace_id.is_err());
281
282 let invalid_trace_id: Result<TraceId, Error> = "4c79f60c11214eb38604f4ae0781bfbg".parse(); assert!(invalid_trace_id.is_err());
285 }
286
287 #[test]
288 fn test_trace_context_roundtrip() {
289 let json = r#"{
290 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
291 "span_id": "fa90fdead5f74052",
292 "parent_span_id": "fa90fdead5f74053",
293 "op": "http",
294 "status": "ok",
295 "exclusive_time": 0.0,
296 "client_sample_rate": 0.5,
297 "origin": "auto.http",
298 "data": {
299 "route": {
300 "name": "/users",
301 "params": {
302 "tok": "test"
303 },
304 "custom_field": "something"
305 },
306 "custom_field_empty": ""
307 },
308 "links": [
309 {
310 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
311 "span_id": "ea90fdead5f74052",
312 "sampled": true,
313 "attributes": {
314 "sentry.link.type": "previous_trace"
315 }
316 }
317 ],
318 "other": "value",
319 "type": "trace"
320}"#;
321 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
322 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
323 span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
324 parent_span_id: Annotated::new(SpanId("fa90fdead5f74053".into())),
325 op: Annotated::new("http".into()),
326 status: Annotated::new(SpanStatus::Ok),
327 exclusive_time: Annotated::new(0.0),
328 client_sample_rate: Annotated::new(0.5),
329 origin: Annotated::new("auto.http".to_owned()),
330 data: Annotated::new(SpanData {
331 route: Annotated::new(Route {
332 name: Annotated::new("/users".into()),
333 params: Annotated::new({
334 let mut map = Object::new();
335 map.insert(
336 "tok".to_string(),
337 Annotated::new(Value::String("test".into())),
338 );
339 map
340 }),
341 other: Object::from([(
342 "custom_field".into(),
343 Annotated::new(Value::String("something".into())),
344 )]),
345 }),
346 other: Object::from([(
347 "custom_field_empty".into(),
348 Annotated::new(Value::String("".into())),
349 )]),
350 ..Default::default()
351 }),
352 links: Annotated::new(Array::from(vec![Annotated::new(SpanLink {
353 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
354 span_id: Annotated::new(SpanId("ea90fdead5f74052".into())),
355 sampled: Annotated::new(true),
356 attributes: Annotated::new({
357 let mut map: std::collections::BTreeMap<String, Annotated<Value>> =
358 Object::new();
359 map.insert(
360 "sentry.link.type".into(),
361 Annotated::new(Value::String("previous_trace".into())),
362 );
363 map
364 }),
365 ..Default::default()
366 })])),
367 other: {
368 let mut map = Object::new();
369 map.insert(
370 "other".to_string(),
371 Annotated::new(Value::String("value".to_string())),
372 );
373 map
374 },
375 sampled: Annotated::empty(),
376 })));
377
378 assert_eq!(context, Annotated::from_json(json).unwrap());
379 assert_eq!(json, context.to_json_pretty().unwrap());
380 }
381
382 #[test]
383 fn test_trace_context_normalization() {
384 let json = r#"{
385 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
386 "span_id": "FA90FDEAD5F74052",
387 "type": "trace"
388}"#;
389 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
390 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
391 span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
392 ..Default::default()
393 })));
394
395 assert_eq!(context, Annotated::from_json(json).unwrap());
396 }
397
398 #[test]
399 fn test_trace_id_formatting() {
400 let test_cases = [
401 (
403 r#"{
404 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
405 "type": "trace"
406}"#,
407 "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
408 true,
409 ),
410 (
412 r#"{
413 "trace_id": "b1e2a9dc-9b8e-4cd0-af0e-80e6b83b56e6",
414 "type": "trace"
415}"#,
416 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
417 false,
418 ),
419 (
421 r#"{
422 "trace_id": "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
423 "type": "trace"
424}"#,
425 "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
426 true,
427 ),
428 (
430 r#"{
431 "trace_id": "B1E2A9DC9B8E4CD0AF0E80E6B83B56E6",
432 "type": "trace"
433}"#,
434 "b1e2a9dc9b8e4cd0af0e80e6b83b56e6",
435 false,
436 ),
437 ];
438
439 for (json, trace_id_str, is_to_json) in test_cases {
440 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
441 trace_id: Annotated::new(trace_id_str.parse().unwrap()),
442 ..Default::default()
443 })));
444
445 if is_to_json {
446 assert_eq!(json, context.to_json_pretty().unwrap());
447 } else {
448 assert_eq!(context, Annotated::from_json(json).unwrap());
449 }
450 }
451 }
452
453 #[test]
454 fn test_trace_context_with_routes() {
455 let json = r#"{
456 "trace_id": "4C79F60C11214EB38604F4AE0781BFB2",
457 "span_id": "FA90FDEAD5F74052",
458 "type": "trace",
459 "data": {
460 "route": "HomeRoute"
461 }
462}"#;
463 let context = Annotated::new(Context::Trace(Box::new(TraceContext {
464 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
465 span_id: Annotated::new(SpanId("fa90fdead5f74052".into())),
466 data: Annotated::new(SpanData {
467 route: Annotated::new(Route {
468 name: Annotated::new("HomeRoute".into()),
469 ..Default::default()
470 }),
471 ..Default::default()
472 }),
473 ..Default::default()
474 })));
475
476 assert_eq!(context, Annotated::from_json(json).unwrap());
477 }
478}