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