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