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