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