relay_spans/
otel_to_sentry.rs

1use crate::otel_to_sentry_v2;
2use crate::otel_trace::Span as OtelSpan;
3use crate::v2_to_v1;
4use opentelemetry_proto::tonic::common::v1::InstrumentationScope;
5use opentelemetry_proto::tonic::resource::v1::Resource;
6use relay_event_schema::protocol::Span as EventSpan;
7
8/// Transforms an OTEL span to a Sentry span.
9///
10/// This uses attributes in the OTEL span to populate various fields in the Sentry span.
11/// * The Sentry span's `name` field may be set based on `db` or `http` attributes
12///   if the OTEL span's `name` is empty.
13/// * The Sentry span's `op` field will be inferred based on the OTEL span's `sentry.op` attribute,
14///   or other available attributes if `sentry.op` is not provided.
15/// * The Sentry span's `description` field will be inferred based on the OTEL span's
16///   `sentry.description` attribute, or other available attributes if `sentry.description` is not
17///   provided.
18/// * The Sentry span's `status` field is set based on the OTEL span's `status` field and
19///   `http.status_code` and `rpc.grpc.status_code` attributes.
20///   attribute, or the difference between the start and end timestamp if that attribute is not set.
21/// * The Sentry span's `platform` field is set based on the OTEL span's `sentry.platform` attribute.
22/// * The Sentry span's `profile_id` field is set based on the OTEL span's `sentry.profile_id` attribute.
23/// * The Sentry span's `segment_id` field is set based on the OTEL span's `sentry.segment.id` attribute.
24///
25/// All other attributes are carried over from the OTEL span to the Sentry span's `data`.
26pub fn otel_to_sentry_span(
27    otel_span: OtelSpan,
28    resource: Option<&Resource>,
29    scope: Option<&InstrumentationScope>,
30) -> EventSpan {
31    let span_v2 = otel_to_sentry_v2::otel_to_sentry_span(otel_span, resource, scope);
32    v2_to_v1::span_v2_to_span_v1(span_v2)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use relay_protocol::{Annotated, SerializableAnnotated};
39
40    #[test]
41    fn parse_span() {
42        let json = r#"{
43            "traceId": "89143b0763095bd9c9955e8175d1fb23",
44            "spanId": "e342abb1214ca181",
45            "parentSpanId": "0c7a7dea069bf5a6",
46            "name": "middleware - fastify -> @fastify/multipart",
47            "kind": 2,
48            "startTimeUnixNano": "1697620454980000000",
49            "endTimeUnixNano": "1697620454980078800",
50            "attributes": [
51                {
52                    "key": "http.route", "value": {
53                        "stringValue": "/home"
54                    }
55                },
56                {
57                    "key": "http.request.method",
58                    "value": {
59                        "stringValue": "GET"
60                        }
61                    },
62                {
63                    "key": "sentry.environment",
64                    "value": {
65                        "stringValue": "test"
66                    }
67                },
68                {
69                "key": "fastify.type",
70                    "value": {
71                        "stringValue": "middleware"
72                    }
73                },
74                {
75                    "key": "plugin.name",
76                    "value": {
77                        "stringValue": "fastify -> @fastify/multipart"
78                    }
79                },
80                {
81                    "key": "hook.name",
82                    "value": {
83                        "stringValue": "onResponse"
84                    }
85                },
86                {
87                    "key": "sentry.sample_rate",
88                    "value": {
89                        "intValue": "1"
90                    }
91                },
92                {
93                    "key": "sentry.parentSampled",
94                    "value": {
95                        "boolValue": true
96                    }
97                },
98                {
99                    "key": "sentry.exclusive_time",
100                    "value": {
101                        "doubleValue": 1000.000000
102                    }
103                }
104            ],
105            "droppedAttributesCount": 0,
106            "events": [],
107            "droppedEventsCount": 0,
108            "status": {
109                "code": 0,
110                "message": "test"
111            },
112            "links": [],
113            "droppedLinksCount": 0
114        }"#;
115
116        let resource = serde_json::from_value(serde_json::json!({
117            "attributes": [{
118                "key": "service.name",
119                "value": {"intValue": 42},
120            }]
121        }))
122        .unwrap();
123
124        let scope = InstrumentationScope {
125            name: "Eins Name".to_owned(),
126            version: "123.42".to_owned(),
127            attributes: Vec::new(),
128            dropped_attributes_count: 12,
129        };
130
131        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
132        let event_span: EventSpan = otel_to_sentry_span(otel_span, Some(&resource), Some(&scope));
133        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
134        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
135        {
136          "timestamp": 1697620454.980079,
137          "start_timestamp": 1697620454.98,
138          "exclusive_time": 1000.0,
139          "op": "http.server",
140          "span_id": "e342abb1214ca181",
141          "parent_span_id": "0c7a7dea069bf5a6",
142          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
143          "is_remote": false,
144          "status": "ok",
145          "description": "GET /home",
146          "data": {
147            "sentry.environment": "test",
148            "sentry.name": "middleware - fastify -> @fastify/multipart",
149            "fastify.type": "middleware",
150            "hook.name": "onResponse",
151            "http.request.method": "GET",
152            "http.route": "/home",
153            "instrumentation.name": "Eins Name",
154            "instrumentation.version": "123.42",
155            "plugin.name": "fastify -> @fastify/multipart",
156            "resource.service.name": 42,
157            "sentry.parentSampled": true,
158            "sentry.sample_rate": 1,
159            "sentry.status.message": "test"
160          },
161          "links": [],
162          "kind": "server"
163        }
164        "#);
165    }
166
167    #[test]
168    fn parse_span_with_exclusive_time_attribute() {
169        let json = r#"{
170            "traceId": "89143b0763095bd9c9955e8175d1fb23",
171            "spanId": "e342abb1214ca181",
172            "parentSpanId": "0c7a7dea069bf5a6",
173            "name": "middleware - fastify -> @fastify/multipart",
174            "kind": 1,
175            "startTimeUnixNano": "1697620454980000000",
176            "endTimeUnixNano": "1697620454980078800",
177            "attributes": [
178                {
179                    "key": "sentry.exclusive_time",
180                    "value": {
181                        "doubleValue": 3200.0
182                    }
183                }
184            ]
185        }"#;
186        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
187        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
188        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
189        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
190        {
191          "timestamp": 1697620454.980079,
192          "start_timestamp": 1697620454.98,
193          "exclusive_time": 3200.0,
194          "op": "default",
195          "span_id": "e342abb1214ca181",
196          "parent_span_id": "0c7a7dea069bf5a6",
197          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
198          "is_remote": false,
199          "status": "ok",
200          "description": "middleware - fastify -> @fastify/multipart",
201          "data": {
202            "sentry.name": "middleware - fastify -> @fastify/multipart"
203          },
204          "links": [],
205          "kind": "internal"
206        }
207        "#);
208    }
209
210    #[test]
211    fn parse_span_no_exclusive_time_attribute() {
212        let json = r#"{
213            "traceId": "89143b0763095bd9c9955e8175d1fb23",
214            "spanId": "e342abb1214ca181",
215            "parentSpanId": "0c7a7dea069bf5a6",
216            "name": "middleware - fastify -> @fastify/multipart",
217            "kind": 1,
218            "startTimeUnixNano": "1697620454980000000",
219            "endTimeUnixNano": "1697620454980078800"
220        }"#;
221        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
222        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
223        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
224        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
225        {
226          "timestamp": 1697620454.980079,
227          "start_timestamp": 1697620454.98,
228          "exclusive_time": 0.0788,
229          "op": "default",
230          "span_id": "e342abb1214ca181",
231          "parent_span_id": "0c7a7dea069bf5a6",
232          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
233          "is_remote": false,
234          "status": "ok",
235          "description": "middleware - fastify -> @fastify/multipart",
236          "data": {
237            "sentry.name": "middleware - fastify -> @fastify/multipart"
238          },
239          "links": [],
240          "kind": "internal"
241        }
242        "#);
243    }
244
245    #[test]
246    fn parse_span_with_db_attributes() {
247        let json = r#"{
248            "traceId": "89143b0763095bd9c9955e8175d1fb23",
249            "spanId": "e342abb1214ca181",
250            "parentSpanId": "0c7a7dea069bf5a6",
251            "name": "database query",
252            "kind": 3,
253            "startTimeUnixNano": "1697620454980000000",
254            "endTimeUnixNano": "1697620454980078800",
255            "attributes": [
256                {
257                    "key" : "db.system",
258                    "value": {
259                        "stringValue": "mysql"
260                    }
261                },
262                {
263                    "key" : "db.name",
264                    "value": {
265                        "stringValue": "database"
266                    }
267                },
268                {
269                    "key" : "db.type",
270                    "value": {
271                        "stringValue": "sql"
272                    }
273                },
274                {
275                    "key" : "db.statement",
276                    "value": {
277                        "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
278                    }
279                }
280            ]
281        }"#;
282        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
283        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
284        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
285        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
286        {
287          "timestamp": 1697620454.980079,
288          "start_timestamp": 1697620454.98,
289          "exclusive_time": 0.0788,
290          "op": "db",
291          "span_id": "e342abb1214ca181",
292          "parent_span_id": "0c7a7dea069bf5a6",
293          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
294          "is_remote": false,
295          "status": "ok",
296          "description": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
297          "data": {
298            "db.system": "mysql",
299            "sentry.name": "database query",
300            "db.name": "database",
301            "db.statement": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
302            "db.type": "sql"
303          },
304          "links": [],
305          "kind": "client"
306        }
307        "#);
308    }
309
310    #[test]
311    fn parse_span_with_db_attributes_and_description() {
312        let json = r#"{
313            "traceId": "89143b0763095bd9c9955e8175d1fb23",
314            "spanId": "e342abb1214ca181",
315            "parentSpanId": "0c7a7dea069bf5a6",
316            "name": "database query",
317            "kind": 3,
318            "startTimeUnixNano": "1697620454980000000",
319            "endTimeUnixNano": "1697620454980078800",
320            "attributes": [
321                {
322                    "key" : "db.name",
323                    "value": {
324                        "stringValue": "database"
325                    }
326                },
327                {
328                    "key" : "db.type",
329                    "value": {
330                        "stringValue": "sql"
331                    }
332                },
333                {
334                    "key" : "db.statement",
335                    "value": {
336                        "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
337                    }
338                },
339                {
340                    "key": "sentry.description",
341                    "value": {
342                        "stringValue": "index view query"
343                    }
344                }
345            ]
346        }"#;
347        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
348        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
349        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
350        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
351        {
352          "timestamp": 1697620454.980079,
353          "start_timestamp": 1697620454.98,
354          "exclusive_time": 0.0788,
355          "op": "default",
356          "span_id": "e342abb1214ca181",
357          "parent_span_id": "0c7a7dea069bf5a6",
358          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
359          "is_remote": false,
360          "status": "ok",
361          "description": "index view query",
362          "data": {
363            "sentry.name": "database query",
364            "db.name": "database",
365            "db.statement": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
366            "db.type": "sql"
367          },
368          "links": [],
369          "kind": "client"
370        }
371        "#);
372    }
373
374    #[test]
375    fn parse_span_with_http_attributes() {
376        let json = r#"{
377            "traceId": "89143b0763095bd9c9955e8175d1fb23",
378            "spanId": "e342abb1214ca181",
379            "parentSpanId": "0c7a7dea069bf5a6",
380            "name": "http client request",
381            "kind": 2,
382            "startTimeUnixNano": "1697620454980000000",
383            "endTimeUnixNano": "1697620454980078800",
384            "attributes": [
385                {
386                    "key" : "http.request.method",
387                    "value": {
388                        "stringValue": "GET"
389                    }
390                },
391                {
392                    "key" : "url.path",
393                    "value": {
394                        "stringValue": "/api/search?q=foobar"
395                    }
396                }
397            ]
398        }"#;
399        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
400        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
401        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
402        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
403        {
404          "timestamp": 1697620454.980079,
405          "start_timestamp": 1697620454.98,
406          "exclusive_time": 0.0788,
407          "op": "http.server",
408          "span_id": "e342abb1214ca181",
409          "parent_span_id": "0c7a7dea069bf5a6",
410          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
411          "is_remote": false,
412          "status": "ok",
413          "description": "GET /api/search?q=foobar",
414          "data": {
415            "sentry.name": "http client request",
416            "http.request.method": "GET",
417            "url.path": "/api/search?q=foobar"
418          },
419          "links": [],
420          "kind": "server"
421        }
422        "#);
423    }
424
425    #[test]
426    fn parse_array_attribute() {
427        let json = r#"{
428            "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
429            "spanId": "fa90fdead5f74052",
430            "parentSpanId": "fa90fdead5f74051",
431            "startTimeUnixNano": "123000000000",
432            "endTimeUnixNano": "123500000000",
433            "name": "cmd.run",
434            "status": {"code": 0},
435            "attributes": [
436                {
437                    "key": "process.args",
438                    "value": {
439                        "arrayValue": {
440                            "values": [
441                                {"stringValue": "node"},
442                                {"stringValue": "--require"},
443                                {"stringValue": "preflight.cjs"}
444                            ]
445                        }
446                    }
447                },
448                {
449                    "key": "process.info",
450                    "value": {
451                        "arrayValue": {
452                            "values": [
453                                {"intValue": 41},
454                                {
455                                    "arrayValue": {
456                                        "values": [
457                                            {"intValue": 42}
458                                    ]}
459                                }
460                            ]
461                        }
462                    }
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
470        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
471        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
472        {
473          "timestamp": 123.5,
474          "start_timestamp": 123.0,
475          "exclusive_time": 500.0,
476          "op": "default",
477          "span_id": "fa90fdead5f74052",
478          "parent_span_id": "fa90fdead5f74051",
479          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
480          "is_remote": false,
481          "status": "ok",
482          "description": "cmd.run",
483          "data": {
484            "sentry.name": "cmd.run",
485            "process.args": "[\"node\",\"--require\",\"preflight.cjs\"]",
486            "process.info": "[41]"
487          },
488          "links": []
489        }
490        "#);
491    }
492
493    /// Intended to be synced with `relay-event-schema::protocol::span::convert::tests::roundtrip`.
494    #[test]
495    fn parse_sentry_attributes() {
496        let json = r#"{
497            "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
498            "spanId": "fa90fdead5f74052",
499            "parentSpanId": "fa90fdead5f74051",
500            "startTimeUnixNano": "123000000000",
501            "endTimeUnixNano": "123500000000",
502            "name": "myname",
503            "status": {"code": 0, "message": "foo"},
504            "attributes": [
505                {
506                    "key" : "browser.name",
507                    "value": {
508                        "stringValue": "Chrome"
509                    }
510                },
511                {
512                    "key" : "sentry.description",
513                    "value": {
514                        "stringValue": "mydescription"
515                    }
516                },
517                {
518                    "key" : "sentry.environment",
519                    "value": {
520                        "stringValue": "prod"
521                    }
522                },
523                {
524                    "key" : "sentry.op",
525                    "value": {
526                        "stringValue": "myop"
527                    }
528                },
529                {
530                    "key" : "sentry.platform",
531                    "value": {
532                        "stringValue": "php"
533                    }
534                },
535                {
536                    "key" : "sentry.profile_id",
537                    "value": {
538                        "stringValue": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"
539                    }
540                },
541                {
542                    "key" : "sentry.release",
543                    "value": {
544                        "stringValue": "myapp@1.0.0"
545                    }
546                },
547                {
548                    "key" : "sentry.sdk.name",
549                    "value": {
550                        "stringValue": "sentry.php"
551                    }
552                },
553                {
554                    "key" : "sentry.segment.id",
555                    "value": {
556                        "stringValue": "FA90FDEAD5F74052"
557                    }
558                },
559                {
560                    "key" : "sentry.segment.name",
561                    "value": {
562                        "stringValue": "my 1st transaction"
563                    }
564                },
565                {
566                    "key": "sentry.metrics_summary.some_metric",
567                    "value": {
568                        "arrayValue": {
569                            "values": [
570                                {
571                                    "kvlistValue": {
572                                        "values": [
573                                            {
574                                                "key": "min",
575                                                "value": {
576                                                    "doubleValue": 1.0
577                                                }
578                                            },
579                                            {
580                                                "key": "max",
581                                                "value": {
582                                                    "doubleValue": 2.0
583                                                }
584                                            },
585                                            {
586                                                "key": "sum",
587                                                "value": {
588                                                    "doubleValue": 3.0
589                                                }
590                                            },
591                                            {
592                                                "key": "count",
593                                                "value": {
594                                                    "intValue": "2"
595                                                }
596                                            },
597                                            {
598                                                "key": "tags",
599                                                "value": {
600                                                    "kvlistValue": {
601                                                        "values": [
602                                                            {
603                                                                "key": "environment",
604                                                                "value": {
605                                                                    "stringValue": "test"
606                                                                }
607                                                            }
608                                                        ]
609                                                    }
610                                                }
611                                            }
612                                        ]
613                                    }
614                                }
615                            ]
616                        }
617                    }
618                }
619            ]
620        }"#;
621
622        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
623        let event_span = otel_to_sentry_span(otel_span, None, None);
624
625        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
626        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
627        {
628          "timestamp": 123.5,
629          "start_timestamp": 123.0,
630          "exclusive_time": 500.0,
631          "op": "myop",
632          "span_id": "fa90fdead5f74052",
633          "parent_span_id": "fa90fdead5f74051",
634          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
635          "segment_id": "fa90fdead5f74052",
636          "is_remote": false,
637          "status": "ok",
638          "description": "mydescription",
639          "profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
640          "data": {
641            "browser.name": "Chrome",
642            "sentry.environment": "prod",
643            "sentry.release": "myapp@1.0.0",
644            "sentry.segment.name": "my 1st transaction",
645            "sentry.sdk.name": "sentry.php",
646            "sentry.name": "myname",
647            "sentry.metrics_summary.some_metric": "[]",
648            "sentry.status.message": "foo"
649          },
650          "links": [],
651          "platform": "php"
652        }
653        "#);
654    }
655
656    #[test]
657    fn parse_span_is_remote() {
658        let json = r#"{
659            "traceId": "89143b0763095bd9c9955e8175d1fb23",
660            "spanId": "e342abb1214ca181",
661            "parentSpanId": "0c7a7dea069bf5a6",
662            "startTimeUnixNano": "123000000000",
663            "endTimeUnixNano": "123500000000",
664            "flags": 768
665        }"#;
666        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
667        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
668        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
669        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
670        {
671          "timestamp": 123.5,
672          "start_timestamp": 123.0,
673          "exclusive_time": 500.0,
674          "op": "default",
675          "span_id": "e342abb1214ca181",
676          "parent_span_id": "0c7a7dea069bf5a6",
677          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
678          "is_remote": true,
679          "status": "ok",
680          "data": {},
681          "links": []
682        }
683        "#);
684    }
685
686    #[test]
687    fn parse_span_is_not_remote() {
688        let json = r#"{
689            "traceId": "89143b0763095bd9c9955e8175d1fb23",
690            "spanId": "e342abb1214ca181",
691            "parentSpanId": "0c7a7dea069bf5a6",
692            "startTimeUnixNano": "123000000000",
693            "endTimeUnixNano": "123500000000",
694            "flags": 256
695        }"#;
696        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
697        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
698        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
699        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
700        {
701          "timestamp": 123.5,
702          "start_timestamp": 123.0,
703          "exclusive_time": 500.0,
704          "op": "default",
705          "span_id": "e342abb1214ca181",
706          "parent_span_id": "0c7a7dea069bf5a6",
707          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
708          "is_remote": false,
709          "status": "ok",
710          "data": {},
711          "links": []
712        }
713        "#);
714    }
715
716    #[test]
717    fn extract_span_kind() {
718        let json = r#"{
719            "traceId": "89143b0763095bd9c9955e8175d1fb23",
720            "spanId": "e342abb1214ca181",
721            "parentSpanId": "0c7a7dea069bf5a6",
722            "startTimeUnixNano": "123000000000",
723            "endTimeUnixNano": "123500000000",
724            "kind": 3
725        }"#;
726        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
727        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
728        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
729        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
730        {
731          "timestamp": 123.5,
732          "start_timestamp": 123.0,
733          "exclusive_time": 500.0,
734          "op": "default",
735          "span_id": "e342abb1214ca181",
736          "parent_span_id": "0c7a7dea069bf5a6",
737          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
738          "is_remote": false,
739          "status": "ok",
740          "data": {},
741          "links": [],
742          "kind": "client"
743        }
744        "#);
745    }
746
747    #[test]
748    fn parse_link() {
749        let json = r#"{
750            "traceId": "3c79f60c11214eb38604f4ae0781bfb2",
751            "spanId": "e342abb1214ca181",
752            "links": [
753                {
754                    "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
755                    "spanId": "fa90fdead5f74052",
756                    "attributes": [
757                        {
758                            "key": "str_key",
759                            "value": {
760                                "stringValue": "str_value"
761                            }
762                        },
763                        {
764                            "key": "bool_key",
765                            "value": {
766                                "boolValue": true
767                            }
768                        },
769                        {
770                            "key": "int_key",
771                            "value": {
772                                "intValue": "123"
773                            }
774                        },
775                        {
776                            "key": "double_key",
777                            "value": {
778                                "doubleValue": 1.23
779                            }
780                        }
781                    ],
782                    "flags": 1
783                }
784            ]
785        }"#;
786        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
787        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
788        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
789
790        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
791        {
792          "timestamp": 0.0,
793          "start_timestamp": 0.0,
794          "exclusive_time": 0.0,
795          "op": "default",
796          "span_id": "e342abb1214ca181",
797          "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
798          "is_remote": false,
799          "status": "ok",
800          "data": {},
801          "links": [
802            {
803              "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
804              "span_id": "fa90fdead5f74052",
805              "sampled": true,
806              "attributes": {
807                "bool_key": true,
808                "double_key": 1.23,
809                "int_key": 123,
810                "str_key": "str_value"
811              }
812            }
813          ]
814        }
815        "#);
816    }
817
818    #[test]
819    fn parse_span_error_status() {
820        let json = r#"{
821          "traceId": "89143b0763095bd9c9955e8175d1fb23",
822          "spanId": "e342abb1214ca181",
823          "status": {
824            "code": 2,
825            "message": "2 is the error status code"
826          }
827        }"#;
828        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
829        let event_span = otel_to_sentry_span(otel_span, None, None);
830        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
831        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
832        {
833          "timestamp": 0.0,
834          "start_timestamp": 0.0,
835          "exclusive_time": 0.0,
836          "op": "default",
837          "span_id": "e342abb1214ca181",
838          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
839          "is_remote": false,
840          "status": "internal_error",
841          "data": {
842            "sentry.status.message": "2 is the error status code"
843          },
844          "links": []
845        }
846        "#);
847    }
848}