relay_spans/
otel_to_sentry_v2.rs

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