1use std::str::FromStr;
2
3use relay_event_schema::protocol::{SpanKind, SpanV2Link};
4
5use crate::status_codes;
6use relay_event_schema::protocol::{
7 Attributes, EventId, Span as SpanV1, SpanData, SpanId, SpanLink, SpanStatus, SpanV2,
8 SpanV2Kind, SpanV2Status,
9};
10use relay_protocol::{Annotated, FromValue, Object, Value};
11use url::Url;
12
13pub fn span_v2_to_span_v1(span_v2: SpanV2) -> SpanV1 {
31 let mut exclusive_time_ms = 0f64;
32 let mut data = Object::new();
33
34 let inferred_op = derive_op_for_v2_span(&span_v2);
35 let inferred_description = derive_description_for_v2_span(&span_v2);
38
39 let SpanV2 {
40 start_timestamp,
41 end_timestamp,
42 trace_id,
43 span_id,
44 parent_span_id,
45 name,
46 kind,
47 links,
48 attributes,
49 status,
50 is_remote,
51 other: _other,
52 } = span_v2;
53
54 let mut description = Annotated::empty();
55 let mut op = Annotated::empty();
56 let mut http_status_code = Annotated::empty();
57 let mut grpc_status_code = Annotated::empty();
58 let mut platform = Annotated::empty();
59 let mut segment_id = Annotated::empty();
60 let mut profile_id = Annotated::empty();
61
62 for (key, value) in attributes.into_value().into_iter().flat_map(|attributes| {
63 attributes.into_iter().flat_map(|(key, attribute)| {
64 let attribute = attribute.into_value()?;
65 Some((key, attribute.value.value))
66 })
67 }) {
68 match key.as_str() {
69 "sentry.description" => {
70 description = String::from_value(value);
71 }
72 "sentry.op" => {
73 op = String::from_value(value);
74 }
75 key if key.contains("exclusive_time_nano") => {
76 let value = match value.value() {
77 Some(Value::I64(v)) => *v as f64,
78 Some(Value::U64(v)) => *v as f64,
79 Some(Value::F64(v)) => *v,
80 Some(Value::String(v)) => v.parse::<f64>().unwrap_or_default(),
81 _ => 0f64,
82 };
83 exclusive_time_ms = value / 1e6f64;
84 }
85 "http.status_code" => {
86 http_status_code = i64::from_value(value.clone());
87 data.insert(key.to_owned(), value);
88 }
89 "rpc.grpc.status_code" => {
90 grpc_status_code = i64::from_value(value.clone());
91 data.insert(key.to_owned(), value);
92 }
93 "sentry.platform" => {
94 platform = String::from_value(value);
95 }
96 "sentry.segment.id" => {
97 segment_id = SpanId::from_value(value);
98 }
99 "sentry.profile.id" => {
100 profile_id = EventId::from_value(value);
101 }
102 _ => {
103 data.insert(key.to_owned(), value);
104 }
105 }
106 }
107
108 if let Some(name) = name.value() {
111 data.insert(
112 "sentry.name".to_owned(),
113 Annotated::new(Value::String(name.to_owned())),
114 );
115 }
116
117 if exclusive_time_ms == 0f64 {
118 if let (Some(start), Some(end)) = (start_timestamp.value(), end_timestamp.value()) {
119 if let Some(nanos) = (end.0 - start.0).num_nanoseconds() {
120 exclusive_time_ms = nanos as f64 / 1e6f64;
121 }
122 }
123 }
124
125 let links = links.map_value(|links| {
126 links
127 .into_iter()
128 .map(|link| link.map_value(span_v2_link_to_span_v1_link))
129 .collect()
130 });
131
132 let status = span_v2_status_to_span_v1_status(status, http_status_code, grpc_status_code);
133
134 let op = op.or_else(|| Annotated::from(inferred_op));
136
137 let description = description.or_else(|| Annotated::from(inferred_description));
139
140 SpanV1 {
141 op,
142 description,
143 data: SpanData::from_value(Annotated::new(data.into())),
144 exclusive_time: exclusive_time_ms.into(),
145 parent_span_id,
146 segment_id,
147 span_id,
148 is_remote,
149 profile_id,
150 start_timestamp,
151 status,
152 timestamp: end_timestamp,
153 trace_id,
154 platform,
155 kind: kind.map_value(span_v2_kind_to_span_v1_kind),
156 links,
157 ..Default::default()
158 }
159}
160
161fn span_v2_status_to_span_v1_status(
162 status: Annotated<SpanV2Status>,
163 http_status_code: Annotated<i64>,
164 grpc_status_code: Annotated<i64>,
165) -> Annotated<SpanStatus> {
166 status
167 .and_then(|status| (status == SpanV2Status::Ok).then_some(SpanStatus::Ok))
168 .or_else(|| {
169 http_status_code.and_then(|http_status_code| {
170 status_codes::HTTP
171 .get(&http_status_code)
172 .and_then(|sentry_status| SpanStatus::from_str(sentry_status).ok())
173 })
174 })
175 .or_else(|| {
176 grpc_status_code.and_then(|grpc_status_code| {
177 status_codes::GRPC
178 .get(&grpc_status_code)
179 .and_then(|sentry_status| SpanStatus::from_str(sentry_status).ok())
180 })
181 })
182 .or_else(|| Annotated::new(SpanStatus::Unknown))
183}
184
185fn span_v2_kind_to_span_v1_kind(kind: SpanV2Kind) -> SpanKind {
186 match kind {
187 SpanV2Kind::Internal => SpanKind::Internal,
188 SpanV2Kind::Server => SpanKind::Server,
189 SpanV2Kind::Client => SpanKind::Client,
190 SpanV2Kind::Producer => SpanKind::Producer,
191 SpanV2Kind::Consumer => SpanKind::Consumer,
192 }
193}
194
195fn span_v2_link_to_span_v1_link(link: SpanV2Link) -> SpanLink {
196 let SpanV2Link {
197 trace_id,
198 span_id,
199 sampled,
200 attributes,
201 other,
202 } = link;
203
204 let attributes = attributes.map_value(|attributes| {
205 attributes
206 .into_iter()
207 .map(|(key, attribute)| {
208 (
209 key,
210 attribute.and_then(|attribute| attribute.value.value.into_value()),
211 )
212 })
213 .collect()
214 });
215 SpanLink {
216 trace_id,
217 span_id,
218 sampled,
219 attributes,
220 other,
221 }
222}
223
224fn derive_op_for_v2_span(span: &SpanV2) -> String {
231 let op = String::from("default");
233
234 let Some(attributes) = span.attributes.value() else {
235 return op;
236 };
237
238 if attributes.contains_key("http.request.method") || attributes.contains_key("http.method") {
239 return match span.kind.value() {
240 Some(SpanV2Kind::Client) => String::from("http.client"),
241 Some(SpanV2Kind::Server) => String::from("http.server"),
242 _ => {
243 if attributes.contains_key("sentry.http.prefetch") {
244 String::from("http.prefetch")
245 } else {
246 String::from("http")
247 }
248 }
249 };
250 }
251
252 if attributes.contains_key("db.system") || attributes.contains_key("db.system.name") {
253 return String::from("db");
254 }
255
256 if attributes.contains_key("gen_ai.system") {
257 return String::from("gen_ai");
258 }
259
260 if attributes.contains_key("rpc.service") {
261 return String::from("rpc");
262 }
263
264 if attributes.contains_key("messaging.system") {
265 return String::from("message");
266 }
267
268 if let Some(faas_trigger) = attributes
269 .get_value("faas.trigger")
270 .and_then(|v| v.as_str())
271 {
272 return faas_trigger.to_owned();
273 }
274
275 op
276}
277
278fn derive_description_for_v2_span(span: &SpanV2) -> Option<String> {
284 let description = span.name.value().map(|v| v.to_owned());
286
287 let Some(attributes) = span.attributes.value() else {
288 return description;
289 };
290
291 if let Some(http_description) = derive_http_description(attributes, &span.kind.value()) {
292 return Some(http_description);
293 }
294
295 if let Some(database_description) = derive_db_description(attributes) {
296 return Some(database_description);
297 }
298
299 description
300}
301
302fn derive_http_description(attributes: &Attributes, kind: &Option<&SpanV2Kind>) -> Option<String> {
303 let http_method = attributes
305 .get_value("http.request.method")
306 .or_else(|| attributes.get_value("http.method"))
307 .and_then(|v| v.as_str())?;
308
309 let description = http_method.to_owned();
310
311 let url_path = match kind {
313 Some(SpanV2Kind::Server) => get_server_url_path(attributes),
314 Some(SpanV2Kind::Client) => get_client_url_path(attributes),
315 _ => None,
316 };
317
318 let Some(url_path) = url_path else {
319 return Some(description);
320 };
321 let base_description = format!("{http_method} {url_path}");
322
323 if let Some(graphql_ops) = attributes
325 .get_value("sentry.graphql.operation")
326 .and_then(|v| v.as_str())
327 {
328 return Some(format!("{base_description} ({graphql_ops})"));
329 }
330
331 Some(base_description)
332}
333
334fn derive_db_description(attributes: &Attributes) -> Option<String> {
335 if attributes
339 .get_value("sentry.op")
340 .and_then(|v| v.as_str())
341 .is_some_and(|op| op.starts_with("cache."))
342 {
343 return None;
344 }
345
346 attributes
349 .get_value("db.system")
350 .or_else(|| attributes.get_value("db.system.name"))
351 .and_then(|v| v.as_str())?;
352
353 if let Some(query_text) = attributes
356 .get_value("db.query.text")
357 .and_then(|v| v.as_str())
358 {
359 return Some(query_text.to_owned());
360 }
361
362 if let Some(statement) = attributes
364 .get_value("db.statement")
365 .and_then(|v| v.as_str())
366 {
367 return Some(statement.to_owned());
368 }
369
370 None
371}
372
373fn get_server_url_path(attributes: &Attributes) -> Option<String> {
374 if let Some(route) = attributes.get_value("http.route").and_then(|v| v.as_str()) {
377 return Some(route.to_owned());
378 }
379
380 if let Some(path) = attributes.get_value("url.path").and_then(|v| v.as_str()) {
382 return Some(path.to_owned());
383 }
384
385 if let Some(target) = attributes.get_value("http.target").and_then(|v| v.as_str()) {
387 return Some(strip_url_query_and_fragment(target));
388 }
389
390 None
391}
392
393fn strip_url_query_and_fragment(url: &str) -> String {
394 url.split(&['?', '#']).next().unwrap_or(url).to_owned()
395}
396
397fn get_client_url_path(attributes: &Attributes) -> Option<String> {
398 let url = attributes
399 .get_value("url.full")
400 .or_else(|| attributes.get_value("http.url"))?
401 .as_str()?;
402
403 let parsed_url = Url::parse(url).ok()?;
404
405 Some(format!(
406 "{}://{}{}",
407 parsed_url.scheme(),
408 parsed_url.domain().unwrap_or(""),
409 parsed_url.path()
410 ))
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use relay_protocol::SerializableAnnotated;
417
418 #[test]
419 fn parse_span() {
420 let json = r#"{
421 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
422 "span_id": "e342abb1214ca181",
423 "parent_span_id": "0c7a7dea069bf5a6",
424 "name": "middleware - fastify -> @fastify/multipart",
425 "kind": "internal",
426 "start_timestamp": "2023-10-18T09:14:14.980Z",
427 "end_timestamp": "2023-10-18T09:14:14.980078800Z",
428 "links": [],
429 "attributes": {
430 "sentry.environment": {
431 "value": "test",
432 "type": "string"
433 },
434 "fastify.type": {
435 "value": "middleware",
436 "type": "string"
437 },
438 "plugin.name": {
439 "value": "fastify -> @fastify/multipart",
440 "type": "string"
441 },
442 "hook.name": {
443 "value": "onResponse",
444 "type": "string"
445 },
446 "sentry.sample_rate": {
447 "value": 1,
448 "type": "u64"
449 },
450 "sentry.parentSampled": {
451 "value": true,
452 "type": "boolean"
453 },
454 "sentry.exclusive_time_nano": {
455 "value": "1000000000",
456 "type": "u64"
457 }
458 },
459 "status": "ok",
460 "links": []
461 }"#;
462 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
463 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
464 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
465 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
466 {
467 "timestamp": 1697620454.980079,
468 "start_timestamp": 1697620454.98,
469 "exclusive_time": 1000.0,
470 "op": "default",
471 "span_id": "e342abb1214ca181",
472 "parent_span_id": "0c7a7dea069bf5a6",
473 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
474 "status": "ok",
475 "description": "middleware - fastify -> @fastify/multipart",
476 "data": {
477 "sentry.environment": "test",
478 "fastify.type": "middleware",
479 "hook.name": "onResponse",
480 "plugin.name": "fastify -> @fastify/multipart",
481 "sentry.name": "middleware - fastify -> @fastify/multipart",
482 "sentry.parentSampled": true,
483 "sentry.sample_rate": 1
484 },
485 "links": [],
486 "kind": "internal"
487 }
488 "###);
489 }
490
491 #[test]
492 fn parse_span_with_exclusive_time_nano_attribute() {
493 let json = r#"{
494 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
495 "span_id": "e342abb1214ca181",
496 "parent_span_id": "0c7a7dea069bf5a6",
497 "name": "middleware - fastify -> @fastify/multipart",
498 "kind": "internal",
499 "start_timestamp": "2023-10-18T09:14:14.980Z",
500 "end_timestamp": "2023-10-18T09:14:14.980078800Z",
501 "links": [],
502 "attributes": {
503 "sentry.exclusive_time_nano": {
504 "value": 3200000000,
505 "type": "u64"
506 }
507 }
508 }"#;
509 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
510 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
511 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
512 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
513 {
514 "timestamp": 1697620454.980079,
515 "start_timestamp": 1697620454.98,
516 "exclusive_time": 3200.0,
517 "op": "default",
518 "span_id": "e342abb1214ca181",
519 "parent_span_id": "0c7a7dea069bf5a6",
520 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
521 "status": "unknown",
522 "description": "middleware - fastify -> @fastify/multipart",
523 "data": {
524 "sentry.name": "middleware - fastify -> @fastify/multipart"
525 },
526 "links": [],
527 "kind": "internal"
528 }
529 "###);
530 }
531
532 #[test]
533 fn parse_span_no_exclusive_time_nano_attribute() {
534 let json = r#"{
535 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
536 "span_id": "e342abb1214ca181",
537 "parent_span_id": "0c7a7dea069bf5a6",
538 "name": "middleware - fastify -> @fastify/multipart",
539 "kind": "internal",
540 "start_timestamp": "2023-10-18T09:14:14.980Z",
541 "end_timestamp": "2023-10-18T09:14:14.980078800Z",
542 "links": []
543 }"#;
544 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
545 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
546 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
547 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
548 {
549 "timestamp": 1697620454.980079,
550 "start_timestamp": 1697620454.98,
551 "exclusive_time": 0.0788,
552 "op": "default",
553 "span_id": "e342abb1214ca181",
554 "parent_span_id": "0c7a7dea069bf5a6",
555 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
556 "status": "unknown",
557 "description": "middleware - fastify -> @fastify/multipart",
558 "data": {
559 "sentry.name": "middleware - fastify -> @fastify/multipart"
560 },
561 "links": [],
562 "kind": "internal"
563 }
564 "###);
565 }
566
567 #[test]
568 fn parse_sentry_attributes() {
569 let json = r#"{
570 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
571 "span_id": "fa90fdead5f74052",
572 "parent_span_id": "fa90fdead5f74051",
573 "start_timestamp": 123,
574 "end_timestamp": 123.5,
575 "name": "myname",
576 "status": "ok",
577 "links": [],
578 "attributes": {
579 "browser.name": {
580 "value": "Chrome",
581 "type": "string"
582 },
583 "sentry.description": {
584 "value": "mydescription",
585 "type": "string"
586 },
587 "sentry.environment": {
588 "value": "prod",
589 "type": "string"
590 },
591 "sentry.op": {
592 "value": "myop",
593 "type": "string"
594 },
595 "sentry.platform": {
596 "value": "php",
597 "type": "string"
598 },
599 "sentry.profile.id": {
600 "value": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab",
601 "type": "string"
602 },
603 "sentry.release": {
604 "value": "myapp@1.0.0",
605 "type": "string"
606 },
607 "sentry.sdk.name": {
608 "value": "sentry.php",
609 "type": "string"
610 },
611 "sentry.segment.id": {
612 "value": "FA90FDEAD5F74052",
613 "type": "string"
614 },
615 "sentry.segment.name": {
616 "value": "my 1st transaction",
617 "type": "string"
618 }
619 }
620 }"#;
621
622 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
623 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
624
625 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
626 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
627 {
628 "timestamp": 123.5,
629 "start_timestamp": 123.0,
630 "exclusive_time": 500.0,
631 "op": "myop",
632 "span_id": "fa90fdead5f74052",
633 "parent_span_id": "fa90fdead5f74051",
634 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
635 "segment_id": "fa90fdead5f74052",
636 "status": "ok",
637 "description": "mydescription",
638 "profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
639 "data": {
640 "browser.name": "Chrome",
641 "sentry.environment": "prod",
642 "sentry.release": "myapp@1.0.0",
643 "sentry.segment.name": "my 1st transaction",
644 "sentry.sdk.name": "sentry.php",
645 "sentry.name": "myname"
646 },
647 "links": [],
648 "platform": "php"
649 }
650 "###);
651 }
652
653 #[test]
654 fn parse_span_is_remote() {
655 let json = r#"{
656 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
657 "span_id": "e342abb1214ca181",
658 "parent_span_id": "0c7a7dea069bf5a6",
659 "start_timestamp": 123,
660 "end_timestamp": 123.5,
661 "is_remote": true,
662 "links": []
663 }"#;
664 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
665 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
666 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
667 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
668 {
669 "timestamp": 123.5,
670 "start_timestamp": 123.0,
671 "exclusive_time": 500.0,
672 "op": "default",
673 "span_id": "e342abb1214ca181",
674 "parent_span_id": "0c7a7dea069bf5a6",
675 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
676 "is_remote": true,
677 "status": "unknown",
678 "data": {},
679 "links": []
680 }
681 "###);
682 }
683
684 #[test]
685 fn parse_span_is_not_remote() {
686 let json = r#"{
687 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
688 "span_id": "e342abb1214ca181",
689 "parent_span_id": "0c7a7dea069bf5a6",
690 "start_timestamp": 123,
691 "end_timestamp": 123.5,
692 "is_remote": false,
693 "links": []
694 }"#;
695 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
696 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
697 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
698 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
699 {
700 "timestamp": 123.5,
701 "start_timestamp": 123.0,
702 "exclusive_time": 500.0,
703 "op": "default",
704 "span_id": "e342abb1214ca181",
705 "parent_span_id": "0c7a7dea069bf5a6",
706 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
707 "is_remote": false,
708 "status": "unknown",
709 "data": {},
710 "links": []
711 }
712 "###);
713 }
714
715 #[test]
716 fn parse_http_client_span_only_method() {
717 let json = r#"{
718 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
719 "span_id": "e342abb1214ca181",
720 "parent_span_id": "0c7a7dea069bf5a6",
721 "start_timestamp": 123,
722 "end_timestamp": 123.5,
723 "kind": "client",
724 "attributes": {
725 "http.method": {
726 "value": "GET",
727 "type": "string"
728 }
729 }
730 }"#;
731 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
732 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
733 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
734 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
735 {
736 "timestamp": 123.5,
737 "start_timestamp": 123.0,
738 "exclusive_time": 500.0,
739 "op": "http.client",
740 "span_id": "e342abb1214ca181",
741 "parent_span_id": "0c7a7dea069bf5a6",
742 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
743 "status": "unknown",
744 "description": "GET",
745 "data": {
746 "http.request_method": "GET"
747 },
748 "kind": "client"
749 }
750 "###);
751 }
752
753 #[test]
754 fn parse_semantic_http_client_span() {
755 let json = r#"{
756 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
757 "span_id": "e342abb1214ca181",
758 "parent_span_id": "0c7a7dea069bf5a6",
759 "start_timestamp": 123,
760 "end_timestamp": 123.5,
761 "kind": "client",
762 "attributes": {
763 "server.address": {
764 "value": "github.com",
765 "type": "string"
766 },
767 "server.port": {
768 "value": 443,
769 "type": "integer"
770 },
771 "http.request.method": {
772 "value": "GET",
773 "type": "string"
774 },
775 "url.full": {
776 "value": "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open",
777 "type": "string"
778 }
779 }
780 }"#;
781 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
782 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
783 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
784 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
785 {
786 "timestamp": 123.5,
787 "start_timestamp": 123.0,
788 "exclusive_time": 500.0,
789 "op": "http.client",
790 "span_id": "e342abb1214ca181",
791 "parent_span_id": "0c7a7dea069bf5a6",
792 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
793 "status": "unknown",
794 "description": "GET https://github.com/rust-lang/rust/issues",
795 "data": {
796 "server.address": "github.com",
797 "url.full": "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open",
798 "http.request.method": "GET",
799 "server.port": 443
800 },
801 "kind": "client"
802 }
803 "###);
804 }
805
806 #[test]
807 fn parse_http_server_span_only_method() {
808 let json = r#"{
809 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
810 "span_id": "e342abb1214ca181",
811 "parent_span_id": "0c7a7dea069bf5a6",
812 "start_timestamp": 123,
813 "end_timestamp": 123.5,
814 "kind": "server",
815 "attributes": {
816 "http.method": {
817 "value": "GET",
818 "type": "string"
819 }
820 }
821 }"#;
822 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
823 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
824 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
825 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
826 {
827 "timestamp": 123.5,
828 "start_timestamp": 123.0,
829 "exclusive_time": 500.0,
830 "op": "http.server",
831 "span_id": "e342abb1214ca181",
832 "parent_span_id": "0c7a7dea069bf5a6",
833 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
834 "status": "unknown",
835 "description": "GET",
836 "data": {
837 "http.request_method": "GET"
838 },
839 "kind": "server"
840 }
841 "###);
842 }
843
844 #[test]
845 fn parse_semantic_http_server_span() {
846 let json = r#"{
847 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
848 "span_id": "e342abb1214ca181",
849 "parent_span_id": "0c7a7dea069bf5a6",
850 "start_timestamp": 123,
851 "end_timestamp": 123.5,
852 "kind": "server",
853 "attributes": {
854 "http.request.method": {
855 "value": "GET",
856 "type": "string"
857 },
858 "url.path": {
859 "value": "/users",
860 "type": "string"
861 },
862 "url.scheme": {
863 "value": "GET",
864 "type": "string"
865 }
866 }
867 }"#;
868 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
869 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
870 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
871 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
872 {
873 "timestamp": 123.5,
874 "start_timestamp": 123.0,
875 "exclusive_time": 500.0,
876 "op": "http.server",
877 "span_id": "e342abb1214ca181",
878 "parent_span_id": "0c7a7dea069bf5a6",
879 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
880 "status": "unknown",
881 "description": "GET /users",
882 "data": {
883 "url.scheme": "GET",
884 "http.request.method": "GET",
885 "url.path": "/users"
886 },
887 "kind": "server"
888 }
889 "###);
890 }
891
892 #[test]
893 fn parse_database_span_only_system() {
894 let json = r#"{
895 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
896 "span_id": "e342abb1214ca181",
897 "parent_span_id": "0c7a7dea069bf5a6",
898 "start_timestamp": 123,
899 "name": "SELECT users",
900 "end_timestamp": 123.5,
901 "kind": "client",
902 "attributes": {
903 "db.system": {
904 "value": "postgres",
905 "type": "string"
906 }
907 }
908 }"#;
909 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
910 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
911 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
912 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
913 {
914 "timestamp": 123.5,
915 "start_timestamp": 123.0,
916 "exclusive_time": 500.0,
917 "op": "db",
918 "span_id": "e342abb1214ca181",
919 "parent_span_id": "0c7a7dea069bf5a6",
920 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
921 "status": "unknown",
922 "description": "SELECT users",
923 "data": {
924 "db.system": "postgres",
925 "sentry.name": "SELECT users"
926 },
927 "kind": "client"
928 }
929 "###);
930 }
931
932 #[test]
933 fn parse_cache_span() {
934 let json = r#"{
935 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
936 "span_id": "e342abb1214ca181",
937 "parent_span_id": "0c7a7dea069bf5a6",
938 "start_timestamp": 123,
939 "name": "CACHE HIT",
940 "end_timestamp": 123.1,
941 "kind": "client",
942 "attributes": {
943 "db.system": {
944 "value": "redis",
945 "type": "string"
946 },
947 "db.statement": {
948 "value": "GET s:user:123",
949 "type": "string"
950 },
951 "sentry.op": {
952 "value": "cache.hit",
953 "type": "string"
954 }
955 }
956 }"#;
957 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
958 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
959 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
960 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
961 {
962 "timestamp": 123.1,
963 "start_timestamp": 123.0,
964 "exclusive_time": 99.999999,
965 "op": "cache.hit",
966 "span_id": "e342abb1214ca181",
967 "parent_span_id": "0c7a7dea069bf5a6",
968 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
969 "status": "unknown",
970 "description": "CACHE HIT",
971 "data": {
972 "db.system": "redis",
973 "db.statement": "GET s:user:123",
974 "sentry.name": "CACHE HIT"
975 },
976 "kind": "client"
977 }
978 "###);
979 }
980
981 #[test]
982 fn parse_semantic_database_span() {
983 let json = r#"{
984 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
985 "span_id": "e342abb1214ca181",
986 "parent_span_id": "0c7a7dea069bf5a6",
987 "start_timestamp": 123,
988 "end_timestamp": 123.5,
989 "kind": "client",
990 "attributes": {
991 "db.system": {
992 "value": "postgres",
993 "type": "string"
994 },
995 "db.statement": {
996 "value": "SELECT * FROM users",
997 "type": "string"
998 }
999 }
1000 }"#;
1001 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1002 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1003 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1004 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1005 {
1006 "timestamp": 123.5,
1007 "start_timestamp": 123.0,
1008 "exclusive_time": 500.0,
1009 "op": "db",
1010 "span_id": "e342abb1214ca181",
1011 "parent_span_id": "0c7a7dea069bf5a6",
1012 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1013 "status": "unknown",
1014 "description": "SELECT * FROM users",
1015 "data": {
1016 "db.system": "postgres",
1017 "db.statement": "SELECT * FROM users"
1018 },
1019 "kind": "client"
1020 }
1021 "###);
1022 }
1023
1024 #[test]
1025 fn parse_gen_ai_span() {
1026 let json = r#"{
1027 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1028 "span_id": "e342abb1214ca181",
1029 "parent_span_id": "0c7a7dea069bf5a6",
1030 "start_timestamp": 123,
1031 "end_timestamp": 123.5,
1032 "kind": "client",
1033 "attributes": {
1034 "gen_ai.system": {
1035 "value": "openai",
1036 "type": "string"
1037 },
1038 "gen_ai.agent.name": {
1039 "value": "Seer",
1040 "type": "string"
1041 }
1042 }
1043 }"#;
1044 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1045 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1046 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1047 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1048 {
1049 "timestamp": 123.5,
1050 "start_timestamp": 123.0,
1051 "exclusive_time": 500.0,
1052 "op": "gen_ai",
1053 "span_id": "e342abb1214ca181",
1054 "parent_span_id": "0c7a7dea069bf5a6",
1055 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1056 "status": "unknown",
1057 "data": {
1058 "gen_ai.agent.name": "Seer",
1059 "gen_ai.system": "openai"
1060 },
1061 "kind": "client"
1062 }
1063 "###);
1064 }
1065
1066 #[test]
1067 fn parse_span_with_sentry_op() {
1068 let json = r#"{
1069 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1070 "span_id": "e342abb1214ca181",
1071 "parent_span_id": "0c7a7dea069bf5a6",
1072 "start_timestamp": 123,
1073 "end_timestamp": 123.5,
1074 "kind": "client",
1075 "attributes": {
1076 "db.system": {
1077 "value": "postgres",
1078 "type": "string"
1079 },
1080 "sentry.op": {
1081 "value": "function",
1082 "type": "string"
1083 }
1084 }
1085 }"#;
1086 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1087 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1088 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1089 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1090 {
1091 "timestamp": 123.5,
1092 "start_timestamp": 123.0,
1093 "exclusive_time": 500.0,
1094 "op": "function",
1095 "span_id": "e342abb1214ca181",
1096 "parent_span_id": "0c7a7dea069bf5a6",
1097 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1098 "status": "unknown",
1099 "data": {
1100 "db.system": "postgres"
1101 },
1102 "kind": "client"
1103 }
1104 "###);
1105 }
1106
1107 #[test]
1108 fn extract_span_kind() {
1109 let json = r#"{
1110 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1111 "span_id": "e342abb1214ca181",
1112 "parent_span_id": "0c7a7dea069bf5a6",
1113 "start_timestamp": 123,
1114 "end_timestamp": 123.5,
1115 "kind": "client",
1116 "links": []
1117 }"#;
1118 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1119 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1120 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1121 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1122 {
1123 "timestamp": 123.5,
1124 "start_timestamp": 123.0,
1125 "exclusive_time": 500.0,
1126 "op": "default",
1127 "span_id": "e342abb1214ca181",
1128 "parent_span_id": "0c7a7dea069bf5a6",
1129 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1130 "status": "unknown",
1131 "data": {},
1132 "links": [],
1133 "kind": "client"
1134 }
1135 "###);
1136 }
1137
1138 #[test]
1139 fn parse_link() {
1140 let json = r#"{
1141 "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
1142 "links": [
1143 {
1144 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1145 "span_id": "fa90fdead5f74052",
1146 "sampled": true,
1147 "attributes": {
1148 "str_key": {
1149 "value": "str_value",
1150 "type": "string"
1151 },
1152 "bool_key": {
1153 "value": true,
1154 "type": "boolean"
1155 },
1156 "int_key": {
1157 "value": 123,
1158 "type": "i64"
1159 },
1160 "double_key": {
1161 "value": 1.23,
1162 "type": "f64"
1163 }
1164 }
1165 }
1166 ]
1167 }"#;
1168 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1169 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1170 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1171
1172 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1173 {
1174 "exclusive_time": 0.0,
1175 "op": "default",
1176 "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
1177 "status": "unknown",
1178 "data": {},
1179 "links": [
1180 {
1181 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1182 "span_id": "fa90fdead5f74052",
1183 "sampled": true,
1184 "attributes": {
1185 "bool_key": true,
1186 "double_key": 1.23,
1187 "int_key": 123,
1188 "str_key": "str_value"
1189 }
1190 }
1191 ]
1192 }
1193 "###);
1194 }
1195
1196 #[test]
1197 fn parse_faas_trigger_span() {
1198 let json = r#"{
1199 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1200 "span_id": "e342abb1214ca181",
1201 "parent_span_id": "0c7a7dea069bf5a6",
1202 "name": "FAAS",
1203 "attributes": {
1204 "faas.trigger": {
1205 "value": "http",
1206 "type": "string"
1207 }
1208 }
1209 }"#;
1210 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1211 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1212 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1213 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1214 {
1215 "exclusive_time": 0.0,
1216 "op": "http",
1217 "span_id": "e342abb1214ca181",
1218 "parent_span_id": "0c7a7dea069bf5a6",
1219 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1220 "status": "unknown",
1221 "description": "FAAS",
1222 "data": {
1223 "faas.trigger": "http",
1224 "sentry.name": "FAAS"
1225 }
1226 }
1227 "###);
1228 }
1229
1230 #[test]
1231 fn parse_http_span_with_route() {
1232 let json = r#"{
1233 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1234 "span_id": "e342abb1214ca181",
1235 "parent_span_id": "0c7a7dea069bf5a6",
1236 "start_timestamp": 123,
1237 "end_timestamp": 123.5,
1238 "name": "GET /api/users",
1239 "kind": "server",
1240 "attributes": {
1241 "http.method": {
1242 "value": "GET",
1243 "type": "string"
1244 },
1245 "http.route": {
1246 "value": "/api/users",
1247 "type": "string"
1248 }
1249 }
1250 }"#;
1251 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1252 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1253 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1254 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1255 {
1256 "timestamp": 123.5,
1257 "start_timestamp": 123.0,
1258 "exclusive_time": 500.0,
1259 "op": "http.server",
1260 "span_id": "e342abb1214ca181",
1261 "parent_span_id": "0c7a7dea069bf5a6",
1262 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1263 "status": "unknown",
1264 "description": "GET /api/users",
1265 "data": {
1266 "http.request_method": "GET",
1267 "http.route": "/api/users",
1268 "sentry.name": "GET /api/users"
1269 },
1270 "kind": "server"
1271 }
1272 "###);
1273 }
1274
1275 #[test]
1276 fn parse_db_span_with_statement() {
1277 let json = r#"{
1278 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1279 "span_id": "e342abb1214ca181",
1280 "parent_span_id": "0c7a7dea069bf5a6",
1281 "start_timestamp": 123,
1282 "end_timestamp": 123.5,
1283 "name": "SELECT users",
1284 "kind": "client",
1285 "attributes": {
1286 "db.system": {
1287 "value": "postgres",
1288 "type": "string"
1289 },
1290 "db.statement": {
1291 "value": "SELECT * FROM users WHERE id = $1",
1292 "type": "string"
1293 }
1294 }
1295 }"#;
1296 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1297 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1298 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1299 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1300 {
1301 "timestamp": 123.5,
1302 "start_timestamp": 123.0,
1303 "exclusive_time": 500.0,
1304 "op": "db",
1305 "span_id": "e342abb1214ca181",
1306 "parent_span_id": "0c7a7dea069bf5a6",
1307 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1308 "status": "unknown",
1309 "description": "SELECT * FROM users WHERE id = $1",
1310 "data": {
1311 "db.system": "postgres",
1312 "db.statement": "SELECT * FROM users WHERE id = $1",
1313 "sentry.name": "SELECT users"
1314 },
1315 "kind": "client"
1316 }
1317 "###);
1318 }
1319
1320 #[test]
1321 fn parse_http_span_with_graphql() {
1322 let json = r#"{
1323 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1324 "span_id": "e342abb1214ca181",
1325 "parent_span_id": "0c7a7dea069bf5a6",
1326 "start_timestamp": 123,
1327 "end_timestamp": 123.5,
1328 "name": "POST /graphql",
1329 "kind": "server",
1330 "attributes": {
1331 "http.method": {
1332 "value": "POST",
1333 "type": "string"
1334 },
1335 "http.route": {
1336 "value": "/graphql",
1337 "type": "string"
1338 },
1339 "sentry.graphql.operation": {
1340 "value": "getUserById",
1341 "type": "string"
1342 }
1343 }
1344 }"#;
1345 let span_v2 = Annotated::from_json(json).unwrap().into_value().unwrap();
1346 let span_v1: SpanV1 = span_v2_to_span_v1(span_v2);
1347 let annotated_span: Annotated<SpanV1> = Annotated::new(span_v1);
1348 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r###"
1349 {
1350 "timestamp": 123.5,
1351 "start_timestamp": 123.0,
1352 "exclusive_time": 500.0,
1353 "op": "http.server",
1354 "span_id": "e342abb1214ca181",
1355 "parent_span_id": "0c7a7dea069bf5a6",
1356 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
1357 "status": "unknown",
1358 "description": "POST /graphql (getUserById)",
1359 "data": {
1360 "http.request_method": "POST",
1361 "http.route": "/graphql",
1362 "sentry.graphql.operation": "getUserById",
1363 "sentry.name": "POST /graphql"
1364 },
1365 "kind": "server"
1366 }
1367 "###);
1368 }
1369}