1use chrono::{TimeZone, Utc};
2use opentelemetry_proto::tonic::common::v1::InstrumentationScope;
3use opentelemetry_proto::tonic::resource::v1::Resource;
4use opentelemetry_proto::tonic::trace::v1::span::Link as OtelLink;
5use opentelemetry_proto::tonic::trace::v1::span::SpanKind as OtelSpanKind;
6use relay_conventions::STATUS_MESSAGE;
7use relay_event_schema::protocol::{Attributes, SpanKind};
8use relay_otel::otel_value_to_attribute;
9use relay_protocol::ErrorKind;
10
11use crate::otel_trace::{
12 Span as OtelSpan, SpanFlags as OtelSpanFlags, status::StatusCode as OtelStatusCode,
13};
14use relay_event_schema::protocol::{
15 SpanId, SpanV2 as SentrySpanV2, SpanV2Link, SpanV2Status, Timestamp, TraceId,
16};
17use relay_protocol::{Annotated, Error, Value};
18
19pub fn otel_to_sentry_span(
29 otel_span: OtelSpan,
30 resource: Option<&Resource>,
31 scope: Option<&InstrumentationScope>,
32) -> SentrySpanV2 {
33 let OtelSpan {
34 trace_id,
35 span_id,
36 parent_span_id,
37 flags,
38 name,
39 kind,
40 attributes,
41 status,
42 links,
43 start_time_unix_nano,
44 end_time_unix_nano,
45 trace_state: _,
46 dropped_attributes_count: _,
47 events: _,
48 dropped_events_count: _,
49 dropped_links_count: _,
50 } = otel_span;
51
52 let start_timestamp = Utc.timestamp_nanos(start_time_unix_nano as i64);
53 let end_timestamp = Utc.timestamp_nanos(end_time_unix_nano as i64);
54
55 let span_id = SpanId::try_from(span_id.as_slice()).into();
56 let trace_id = TraceId::try_from_slice_or_random(trace_id.as_slice());
57
58 let parent_span_id = match parent_span_id.as_slice() {
59 &[] => Annotated::empty(),
60 bytes => SpanId::try_from(bytes).into(),
61 };
62
63 let mut sentry_attributes = Attributes::new();
64
65 relay_otel::otel_scope_into_attributes(&mut sentry_attributes, resource, scope);
66
67 let mut name = if name.is_empty() { None } else { Some(name) };
68 for (key, value) in attributes.into_iter().flat_map(|attribute| {
69 let value = attribute.value?.value?;
70 Some((attribute.key, value))
71 }) {
72 match key.as_str() {
73 key if key.starts_with("db") => {
74 name = name.or(Some("db".to_owned()));
75 }
76 "http.method" | "http.request.method" => {
77 let http_op = match kind {
78 2 => "http.server",
79 3 => "http.client",
80 _ => "http",
81 };
82 name = name.or(Some(http_op.to_owned()));
83 }
84 _ => (),
85 }
86
87 if let Some(v) = otel_value_to_attribute(value) {
88 sentry_attributes.insert_raw(key, Annotated::new(v));
89 }
90 }
91
92 let sentry_links: Vec<Annotated<SpanV2Link>> = links
93 .into_iter()
94 .map(|link| otel_to_sentry_link(link).into())
95 .collect();
96
97 if let Some(status_message) = status.clone().map(|status| status.message) {
98 sentry_attributes.insert(STATUS_MESSAGE.to_owned(), status_message);
99 }
100
101 SentrySpanV2 {
102 name: name.into(),
103 trace_id,
104 span_id,
105 parent_span_id,
106 is_remote: otel_flags_is_remote(flags).unwrap_or(false).into(),
107 start_timestamp: Timestamp(start_timestamp).into(),
108 end_timestamp: Timestamp(end_timestamp).into(),
109 status: status
110 .map(|status| otel_to_sentry_status(status.code))
111 .unwrap_or(SpanV2Status::Ok)
112 .into(),
113 kind: otel_to_sentry_kind(kind),
114 links: sentry_links.into(),
115 attributes: Annotated::new(sentry_attributes),
116 ..Default::default()
117 }
118}
119
120fn otel_flags_is_remote(value: u32) -> Option<bool> {
121 if value & OtelSpanFlags::ContextHasIsRemoteMask as u32 == 0 {
122 None
123 } else {
124 Some(value & OtelSpanFlags::ContextIsRemoteMask as u32 != 0)
125 }
126}
127
128fn otel_to_sentry_kind(kind: i32) -> Annotated<SpanKind> {
129 match kind {
130 kind if kind == OtelSpanKind::Unspecified as i32 => Annotated::empty(),
131 kind if kind == OtelSpanKind::Internal as i32 => Annotated::new(SpanKind::Internal),
132 kind if kind == OtelSpanKind::Server as i32 => Annotated::new(SpanKind::Server),
133 kind if kind == OtelSpanKind::Client as i32 => Annotated::new(SpanKind::Client),
134 kind if kind == OtelSpanKind::Producer as i32 => Annotated::new(SpanKind::Producer),
135 kind if kind == OtelSpanKind::Consumer as i32 => Annotated::new(SpanKind::Consumer),
136 _ => Annotated::from_error(ErrorKind::InvalidData, Some(Value::I64(kind as i64))),
137 }
138}
139
140fn otel_to_sentry_status(status_code: i32) -> SpanV2Status {
141 if status_code == OtelStatusCode::Unset as i32 || status_code == OtelStatusCode::Ok as i32 {
142 SpanV2Status::Ok
143 } else {
144 SpanV2Status::Error
145 }
146}
147
148fn otel_to_sentry_link(otel_link: OtelLink) -> Result<SpanV2Link, Error> {
151 const W3C_TRACE_CONTEXT_SAMPLED: u32 = 1 << 0;
154
155 let attributes = Attributes::from_iter(otel_link.attributes.into_iter().filter_map(|kv| {
156 let value = kv.value?.value?;
157 let attr_value = otel_value_to_attribute(value)?;
158 Some((kv.key, Annotated::new(attr_value)))
159 }));
160
161 let span_link = SpanV2Link {
162 trace_id: Annotated::new(hex::encode(otel_link.trace_id).parse()?),
163 span_id: SpanId::try_from(otel_link.span_id.as_slice())?.into(),
164 sampled: (otel_link.flags & W3C_TRACE_CONTEXT_SAMPLED != 0).into(),
165 attributes: Annotated::new(attributes),
166 other: Default::default(),
167 };
168
169 Ok(span_link)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use relay_protocol::SerializableAnnotated;
176
177 #[test]
178 fn parse_span() {
179 let json = r#"{
180 "traceId": "89143b0763095bd9c9955e8175d1fb23",
181 "spanId": "e342abb1214ca181",
182 "parentSpanId": "0c7a7dea069bf5a6",
183 "name": "middleware - fastify -> @fastify/multipart",
184 "kind": 1,
185 "startTimeUnixNano": "1697620454980000000",
186 "endTimeUnixNano": "1697620454980078800",
187 "attributes": [
188 {
189 "key": "sentry.environment",
190 "value": {
191 "stringValue": "test"
192 }
193 },
194 {
195 "key": "fastify.type",
196 "value": {
197 "stringValue": "middleware"
198 }
199 },
200 {
201 "key": "plugin.name",
202 "value": {
203 "stringValue": "fastify -> @fastify/multipart"
204 }
205 },
206 {
207 "key": "hook.name",
208 "value": {
209 "stringValue": "onResponse"
210 }
211 },
212 {
213 "key": "sentry.sample_rate",
214 "value": {
215 "intValue": "1"
216 }
217 },
218 {
219 "key": "sentry.parentSampled",
220 "value": {
221 "boolValue": true
222 }
223 },
224 {
225 "key": "sentry.exclusive_time",
226 "value": {
227 "doubleValue": 1000.0
228 }
229 }
230 ],
231 "droppedAttributesCount": 0,
232 "events": [],
233 "droppedEventsCount": 0,
234 "status": {
235 "code": 0,
236 "message": "test"
237 },
238 "links": [],
239 "droppedLinksCount": 0
240 }"#;
241
242 let resource = serde_json::from_value(serde_json::json!({
243 "attributes": [{
244 "key": "service.name",
245 "value": {"stringValue": "test-service"},
246 }]
247 }))
248 .unwrap();
249
250 let scope = InstrumentationScope {
251 name: "Eins Name".to_owned(),
252 version: "123.42".to_owned(),
253 attributes: Vec::new(),
254 dropped_attributes_count: 12,
255 };
256
257 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
258 let event_span = otel_to_sentry_span(otel_span, Some(&resource), Some(&scope));
259 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
260 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
261 {
262 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
263 "parent_span_id": "0c7a7dea069bf5a6",
264 "span_id": "e342abb1214ca181",
265 "name": "middleware - fastify -> @fastify/multipart",
266 "status": "ok",
267 "is_remote": false,
268 "kind": "internal",
269 "start_timestamp": 1697620454.98,
270 "end_timestamp": 1697620454.980079,
271 "links": [],
272 "attributes": {
273 "fastify.type": {
274 "type": "string",
275 "value": "middleware"
276 },
277 "hook.name": {
278 "type": "string",
279 "value": "onResponse"
280 },
281 "instrumentation.name": {
282 "type": "string",
283 "value": "Eins Name"
284 },
285 "instrumentation.version": {
286 "type": "string",
287 "value": "123.42"
288 },
289 "plugin.name": {
290 "type": "string",
291 "value": "fastify -> @fastify/multipart"
292 },
293 "resource.service.name": {
294 "type": "string",
295 "value": "test-service"
296 },
297 "sentry.environment": {
298 "type": "string",
299 "value": "test"
300 },
301 "sentry.exclusive_time": {
302 "type": "double",
303 "value": 1000.0
304 },
305 "sentry.parentSampled": {
306 "type": "boolean",
307 "value": true
308 },
309 "sentry.sample_rate": {
310 "type": "integer",
311 "value": 1
312 },
313 "sentry.status.message": {
314 "type": "string",
315 "value": "test"
316 }
317 }
318 }
319 "#);
320 }
321
322 #[test]
323 fn parse_span_with_exclusive_time_attribute() {
324 let json = r#"{
325 "traceId": "89143b0763095bd9c9955e8175d1fb23",
326 "spanId": "e342abb1214ca181",
327 "parentSpanId": "0c7a7dea069bf5a6",
328 "name": "middleware - fastify -> @fastify/multipart",
329 "kind": 1,
330 "startTimeUnixNano": "1697620454980000000",
331 "endTimeUnixNano": "1697620454980078800",
332 "attributes": [
333 {
334 "key": "sentry.exclusive_time",
335 "value": {
336 "doubleValue": 3200.000000
337 }
338 }
339 ]
340 }"#;
341 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
342 let event_span = otel_to_sentry_span(otel_span, None, None);
343 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
344 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
345 {
346 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
347 "parent_span_id": "0c7a7dea069bf5a6",
348 "span_id": "e342abb1214ca181",
349 "name": "middleware - fastify -> @fastify/multipart",
350 "status": "ok",
351 "is_remote": false,
352 "kind": "internal",
353 "start_timestamp": 1697620454.98,
354 "end_timestamp": 1697620454.980079,
355 "links": [],
356 "attributes": {
357 "sentry.exclusive_time": {
358 "type": "double",
359 "value": 3200.0
360 }
361 }
362 }
363 "#);
364 }
365
366 #[test]
367 fn parse_span_with_db_attributes() {
368 let json = r#"{
369 "traceId": "89143b0763095bd9c9955e8175d1fb23",
370 "spanId": "e342abb1214ca181",
371 "parentSpanId": "0c7a7dea069bf5a6",
372 "name": "database query",
373 "kind": 3,
374 "startTimeUnixNano": "1697620454980000000",
375 "endTimeUnixNano": "1697620454980078800",
376 "attributes": [
377 {
378 "key": "db.name",
379 "value": {
380 "stringValue": "database"
381 }
382 },
383 {
384 "key": "db.type",
385 "value": {
386 "stringValue": "sql"
387 }
388 },
389 {
390 "key": "db.statement",
391 "value": {
392 "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
393 }
394 }
395 ]
396 }"#;
397 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
398 let event_span = otel_to_sentry_span(otel_span, None, None);
399 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
400 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
401 {
402 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
403 "parent_span_id": "0c7a7dea069bf5a6",
404 "span_id": "e342abb1214ca181",
405 "name": "database query",
406 "status": "ok",
407 "is_remote": false,
408 "kind": "client",
409 "start_timestamp": 1697620454.98,
410 "end_timestamp": 1697620454.980079,
411 "links": [],
412 "attributes": {
413 "db.name": {
414 "type": "string",
415 "value": "database"
416 },
417 "db.statement": {
418 "type": "string",
419 "value": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
420 },
421 "db.type": {
422 "type": "string",
423 "value": "sql"
424 }
425 }
426 }
427 "#);
428 }
429
430 #[test]
431 fn parse_span_with_db_attributes_and_description() {
432 let json = r#"{
433 "traceId": "89143b0763095bd9c9955e8175d1fb23",
434 "spanId": "e342abb1214ca181",
435 "parentSpanId": "0c7a7dea069bf5a6",
436 "name": "database query",
437 "kind": 3,
438 "startTimeUnixNano": "1697620454980000000",
439 "endTimeUnixNano": "1697620454980078800",
440 "attributes": [
441 {
442 "key": "db.name",
443 "value": {
444 "stringValue": "database"
445 }
446 },
447 {
448 "key": "db.type",
449 "value": {
450 "stringValue": "sql"
451 }
452 },
453 {
454 "key": "db.statement",
455 "value": {
456 "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
457 }
458 },
459 {
460 "key": "sentry.description",
461 "value": {
462 "stringValue": "index view query"
463 }
464 }
465 ]
466 }"#;
467 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
468 let event_span = otel_to_sentry_span(otel_span, None, None);
469 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
470 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
471 {
472 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
473 "parent_span_id": "0c7a7dea069bf5a6",
474 "span_id": "e342abb1214ca181",
475 "name": "database query",
476 "status": "ok",
477 "is_remote": false,
478 "kind": "client",
479 "start_timestamp": 1697620454.98,
480 "end_timestamp": 1697620454.980079,
481 "links": [],
482 "attributes": {
483 "db.name": {
484 "type": "string",
485 "value": "database"
486 },
487 "db.statement": {
488 "type": "string",
489 "value": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
490 },
491 "db.type": {
492 "type": "string",
493 "value": "sql"
494 },
495 "sentry.description": {
496 "type": "string",
497 "value": "index view query"
498 }
499 }
500 }
501 "#);
502 }
503
504 #[test]
505 fn parse_span_with_http_attributes() {
506 let json = r#"{
507 "traceId": "89143b0763095bd9c9955e8175d1fb23",
508 "spanId": "e342abb1214ca181",
509 "parentSpanId": "0c7a7dea069bf5a6",
510 "name": "http client request",
511 "kind": 3,
512 "startTimeUnixNano": "1697620454980000000",
513 "endTimeUnixNano": "1697620454980078800",
514 "attributes": [
515 {
516 "key": "http.request.method",
517 "value": {
518 "stringValue": "GET"
519 }
520 },
521 {
522 "key": "url.path",
523 "value": {
524 "stringValue": "/api/search?q=foobar"
525 }
526 }
527 ]
528 }"#;
529 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
530 let event_span = otel_to_sentry_span(otel_span, None, None);
531 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
532 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
533 {
534 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
535 "parent_span_id": "0c7a7dea069bf5a6",
536 "span_id": "e342abb1214ca181",
537 "name": "http client request",
538 "status": "ok",
539 "is_remote": false,
540 "kind": "client",
541 "start_timestamp": 1697620454.98,
542 "end_timestamp": 1697620454.980079,
543 "links": [],
544 "attributes": {
545 "http.request.method": {
546 "type": "string",
547 "value": "GET"
548 },
549 "url.path": {
550 "type": "string",
551 "value": "/api/search?q=foobar"
552 }
553 }
554 }
555 "#);
556 }
557
558 #[test]
560 fn parse_sentry_attributes() {
561 let json = r#"{
562 "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
563 "spanId": "fa90fdead5f74052",
564 "parentSpanId": "fa90fdead5f74051",
565 "startTimeUnixNano": "123000000000",
566 "endTimeUnixNano": "123500000000",
567 "name": "myname",
568 "status": {
569 "code": 0,
570 "message": "foo"
571 },
572 "attributes": [
573 {
574 "key": "browser.name",
575 "value": {
576 "stringValue": "Chrome"
577 }
578 },
579 {
580 "key": "sentry.description",
581 "value": {
582 "stringValue": "mydescription"
583 }
584 },
585 {
586 "key": "sentry.environment",
587 "value": {
588 "stringValue": "prod"
589 }
590 },
591 {
592 "key": "sentry.op",
593 "value": {
594 "stringValue": "myop"
595 }
596 },
597 {
598 "key": "sentry.platform",
599 "value": {
600 "stringValue": "php"
601 }
602 },
603 {
604 "key": "sentry.profile_id",
605 "value": {
606 "stringValue": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"
607 }
608 },
609 {
610 "key": "sentry.release",
611 "value": {
612 "stringValue": "myapp@1.0.0"
613 }
614 },
615 {
616 "key": "sentry.sdk.name",
617 "value": {
618 "stringValue": "sentry.php"
619 }
620 },
621 {
622 "key": "sentry.segment.id",
623 "value": {
624 "stringValue": "FA90FDEAD5F74052"
625 }
626 },
627 {
628 "key": "sentry.segment.name",
629 "value": {
630 "stringValue": "my 1st transaction"
631 }
632 },
633 {
634 "key": "sentry.metrics_summary.some_metric",
635 "value": {
636 "arrayValue": {
637 "values": [
638 {
639 "kvlistValue": {
640 "values": [
641 {
642 "key": "min",
643 "value": {
644 "doubleValue": 1
645 }
646 },
647 {
648 "key": "max",
649 "value": {
650 "doubleValue": 2
651 }
652 },
653 {
654 "key": "sum",
655 "value": {
656 "doubleValue": 3
657 }
658 },
659 {
660 "key": "count",
661 "value": {
662 "intValue": "2"
663 }
664 },
665 {
666 "key": "tags",
667 "value": {
668 "kvlistValue": {
669 "values": [
670 {
671 "key": "environment",
672 "value": {
673 "stringValue": "test"
674 }
675 }
676 ]
677 }
678 }
679 }
680 ]
681 }
682 }
683 ]
684 }
685 }
686 }
687 ]
688 }"#;
689
690 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
691 let event_span = otel_to_sentry_span(otel_span, None, None);
692
693 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
694 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
695 {
696 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
697 "parent_span_id": "fa90fdead5f74051",
698 "span_id": "fa90fdead5f74052",
699 "name": "myname",
700 "status": "ok",
701 "is_remote": false,
702 "start_timestamp": 123.0,
703 "end_timestamp": 123.5,
704 "links": [],
705 "attributes": {
706 "browser.name": {
707 "type": "string",
708 "value": "Chrome"
709 },
710 "sentry.description": {
711 "type": "string",
712 "value": "mydescription"
713 },
714 "sentry.environment": {
715 "type": "string",
716 "value": "prod"
717 },
718 "sentry.metrics_summary.some_metric": {
719 "type": "string",
720 "value": "[]"
721 },
722 "sentry.op": {
723 "type": "string",
724 "value": "myop"
725 },
726 "sentry.platform": {
727 "type": "string",
728 "value": "php"
729 },
730 "sentry.profile_id": {
731 "type": "string",
732 "value": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"
733 },
734 "sentry.release": {
735 "type": "string",
736 "value": "myapp@1.0.0"
737 },
738 "sentry.sdk.name": {
739 "type": "string",
740 "value": "sentry.php"
741 },
742 "sentry.segment.id": {
743 "type": "string",
744 "value": "FA90FDEAD5F74052"
745 },
746 "sentry.segment.name": {
747 "type": "string",
748 "value": "my 1st transaction"
749 },
750 "sentry.status.message": {
751 "type": "string",
752 "value": "foo"
753 }
754 }
755 }
756 "#);
757 }
758
759 #[test]
760 fn parse_span_is_remote() {
761 let json = r#"{
762 "traceId": "89143b0763095bd9c9955e8175d1fb23",
763 "spanId": "e342abb1214ca181",
764 "parentSpanId": "0c7a7dea069bf5a6",
765 "startTimeUnixNano": "123000000000",
766 "endTimeUnixNano": "123500000000",
767 "flags": 768
768 }"#;
769 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
770 let event_span = otel_to_sentry_span(otel_span, None, None);
771 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
772 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
773 {
774 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
775 "parent_span_id": "0c7a7dea069bf5a6",
776 "span_id": "e342abb1214ca181",
777 "status": "ok",
778 "is_remote": true,
779 "start_timestamp": 123.0,
780 "end_timestamp": 123.5,
781 "links": [],
782 "attributes": {}
783 }
784 "#);
785 }
786
787 #[test]
788 fn parse_span_is_not_remote() {
789 let json = r#"{
790 "traceId": "89143b0763095bd9c9955e8175d1fb23",
791 "spanId": "e342abb1214ca181",
792 "parentSpanId": "0c7a7dea069bf5a6",
793 "startTimeUnixNano": "123000000000",
794 "endTimeUnixNano": "123500000000",
795 "flags": 256
796 }"#;
797 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
798 let event_span = otel_to_sentry_span(otel_span, None, None);
799 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
800 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
801 {
802 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
803 "parent_span_id": "0c7a7dea069bf5a6",
804 "span_id": "e342abb1214ca181",
805 "status": "ok",
806 "is_remote": false,
807 "start_timestamp": 123.0,
808 "end_timestamp": 123.5,
809 "links": [],
810 "attributes": {}
811 }
812 "#);
813 }
814
815 #[test]
816 fn extract_span_kind() {
817 let json = r#"{
818 "traceId": "89143b0763095bd9c9955e8175d1fb23",
819 "spanId": "e342abb1214ca181",
820 "parentSpanId": "0c7a7dea069bf5a6",
821 "startTimeUnixNano": "123000000000",
822 "endTimeUnixNano": "123500000000",
823 "kind": 3
824 }"#;
825 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
826 let event_span = otel_to_sentry_span(otel_span, None, None);
827 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
828 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
829 {
830 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
831 "parent_span_id": "0c7a7dea069bf5a6",
832 "span_id": "e342abb1214ca181",
833 "status": "ok",
834 "is_remote": false,
835 "kind": "client",
836 "start_timestamp": 123.0,
837 "end_timestamp": 123.5,
838 "links": [],
839 "attributes": {}
840 }
841 "#);
842 }
843
844 #[test]
845 fn parse_link() {
846 let json = r#"{
847 "traceId": "3c79f60c11214eb38604f4ae0781bfb2",
848 "spanId": "e342abb1214ca181",
849 "links": [
850 {
851 "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
852 "spanId": "fa90fdead5f74052",
853 "attributes": [
854 {
855 "key": "str_key",
856 "value": {
857 "stringValue": "str_value"
858 }
859 },
860 {
861 "key": "bool_key",
862 "value": {
863 "boolValue": true
864 }
865 },
866 {
867 "key": "int_key",
868 "value": {
869 "intValue": "123"
870 }
871 },
872 {
873 "key": "double_key",
874 "value": {
875 "doubleValue": 1.23
876 }
877 }
878 ],
879 "flags": 1
880 }
881 ]
882 }"#;
883 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
884 let event_span = otel_to_sentry_span(otel_span, None, None);
885 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
886
887 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
888 {
889 "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
890 "span_id": "e342abb1214ca181",
891 "status": "ok",
892 "is_remote": false,
893 "start_timestamp": 0.0,
894 "end_timestamp": 0.0,
895 "links": [
896 {
897 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
898 "span_id": "fa90fdead5f74052",
899 "sampled": true,
900 "attributes": {
901 "bool_key": {
902 "type": "boolean",
903 "value": true
904 },
905 "double_key": {
906 "type": "double",
907 "value": 1.23
908 },
909 "int_key": {
910 "type": "integer",
911 "value": 123
912 },
913 "str_key": {
914 "type": "string",
915 "value": "str_value"
916 }
917 }
918 }
919 ],
920 "attributes": {}
921 }
922 "#);
923 }
924
925 #[test]
926 fn parse_span_error_status() {
927 let json = r#"{
928 "traceId": "89143b0763095bd9c9955e8175d1fb23",
929 "spanId": "e342abb1214ca181",
930 "status": {
931 "code": 2,
932 "message": "2 is the error status code"
933 }
934 }"#;
935 let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
936 let event_span = otel_to_sentry_span(otel_span, None, None);
937 let annotated_span: Annotated<SentrySpanV2> = Annotated::new(event_span);
938 insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
939 {
940 "trace_id": "89143b0763095bd9c9955e8175d1fb23",
941 "span_id": "e342abb1214ca181",
942 "status": "error",
943 "is_remote": false,
944 "start_timestamp": 0.0,
945 "end_timestamp": 0.0,
946 "links": [],
947 "attributes": {
948 "sentry.status.message": {
949 "type": "string",
950 "value": "2 is the error status code"
951 }
952 }
953 }
954 "#);
955 }
956}