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