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_segment": 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.origin": "auto.otlp.spans",
158            "sentry.parentSampled": true,
159            "sentry.sample_rate": 1,
160            "sentry.status.message": "test"
161          },
162          "links": [],
163          "kind": "server"
164        }
165        "#);
166    }
167
168    #[test]
169    fn parse_span_with_exclusive_time_attribute() {
170        let json = r#"{
171            "traceId": "89143b0763095bd9c9955e8175d1fb23",
172            "spanId": "e342abb1214ca181",
173            "parentSpanId": "0c7a7dea069bf5a6",
174            "name": "middleware - fastify -> @fastify/multipart",
175            "kind": 1,
176            "startTimeUnixNano": "1697620454980000000",
177            "endTimeUnixNano": "1697620454980078800",
178            "attributes": [
179                {
180                    "key": "sentry.exclusive_time",
181                    "value": {
182                        "doubleValue": 3200.0
183                    }
184                }
185            ]
186        }"#;
187        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
188        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
189        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
190        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
191        {
192          "timestamp": 1697620454.980079,
193          "start_timestamp": 1697620454.98,
194          "exclusive_time": 3200.0,
195          "op": "default",
196          "span_id": "e342abb1214ca181",
197          "parent_span_id": "0c7a7dea069bf5a6",
198          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
199          "is_segment": false,
200          "status": "ok",
201          "description": "middleware - fastify -> @fastify/multipart",
202          "data": {
203            "sentry.name": "middleware - fastify -> @fastify/multipart",
204            "sentry.origin": "auto.otlp.spans"
205          },
206          "links": [],
207          "kind": "internal"
208        }
209        "#);
210    }
211
212    #[test]
213    fn parse_span_no_exclusive_time_attribute() {
214        let json = r#"{
215            "traceId": "89143b0763095bd9c9955e8175d1fb23",
216            "spanId": "e342abb1214ca181",
217            "parentSpanId": "0c7a7dea069bf5a6",
218            "name": "middleware - fastify -> @fastify/multipart",
219            "kind": 1,
220            "startTimeUnixNano": "1697620454980000000",
221            "endTimeUnixNano": "1697620454980078800"
222        }"#;
223        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
224        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
225        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
226        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
227        {
228          "timestamp": 1697620454.980079,
229          "start_timestamp": 1697620454.98,
230          "exclusive_time": 0.0788,
231          "op": "default",
232          "span_id": "e342abb1214ca181",
233          "parent_span_id": "0c7a7dea069bf5a6",
234          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
235          "is_segment": false,
236          "status": "ok",
237          "description": "middleware - fastify -> @fastify/multipart",
238          "data": {
239            "sentry.name": "middleware - fastify -> @fastify/multipart",
240            "sentry.origin": "auto.otlp.spans"
241          },
242          "links": [],
243          "kind": "internal"
244        }
245        "#);
246    }
247
248    #[test]
249    fn parse_span_with_db_attributes() {
250        let json = r#"{
251            "traceId": "89143b0763095bd9c9955e8175d1fb23",
252            "spanId": "e342abb1214ca181",
253            "parentSpanId": "0c7a7dea069bf5a6",
254            "name": "database query",
255            "kind": 3,
256            "startTimeUnixNano": "1697620454980000000",
257            "endTimeUnixNano": "1697620454980078800",
258            "attributes": [
259                {
260                    "key" : "db.system",
261                    "value": {
262                        "stringValue": "mysql"
263                    }
264                },
265                {
266                    "key" : "db.name",
267                    "value": {
268                        "stringValue": "database"
269                    }
270                },
271                {
272                    "key" : "db.type",
273                    "value": {
274                        "stringValue": "sql"
275                    }
276                },
277                {
278                    "key" : "db.statement",
279                    "value": {
280                        "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
281                    }
282                }
283            ]
284        }"#;
285        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
286        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
287        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
288        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
289        {
290          "timestamp": 1697620454.980079,
291          "start_timestamp": 1697620454.98,
292          "exclusive_time": 0.0788,
293          "op": "db",
294          "span_id": "e342abb1214ca181",
295          "parent_span_id": "0c7a7dea069bf5a6",
296          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
297          "is_segment": false,
298          "status": "ok",
299          "description": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
300          "data": {
301            "db.system": "mysql",
302            "sentry.name": "database query",
303            "db.name": "database",
304            "db.statement": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
305            "db.type": "sql",
306            "sentry.origin": "auto.otlp.spans"
307          },
308          "links": [],
309          "kind": "client"
310        }
311        "#);
312    }
313
314    #[test]
315    fn parse_span_with_db_attributes_and_description() {
316        let json = r#"{
317            "traceId": "89143b0763095bd9c9955e8175d1fb23",
318            "spanId": "e342abb1214ca181",
319            "parentSpanId": "0c7a7dea069bf5a6",
320            "name": "database query",
321            "kind": 3,
322            "startTimeUnixNano": "1697620454980000000",
323            "endTimeUnixNano": "1697620454980078800",
324            "attributes": [
325                {
326                    "key" : "db.name",
327                    "value": {
328                        "stringValue": "database"
329                    }
330                },
331                {
332                    "key" : "db.type",
333                    "value": {
334                        "stringValue": "sql"
335                    }
336                },
337                {
338                    "key" : "db.statement",
339                    "value": {
340                        "stringValue": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s"
341                    }
342                },
343                {
344                    "key": "sentry.description",
345                    "value": {
346                        "stringValue": "index view query"
347                    }
348                }
349            ]
350        }"#;
351        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
352        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
353        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
354        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
355        {
356          "timestamp": 1697620454.980079,
357          "start_timestamp": 1697620454.98,
358          "exclusive_time": 0.0788,
359          "op": "default",
360          "span_id": "e342abb1214ca181",
361          "parent_span_id": "0c7a7dea069bf5a6",
362          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
363          "is_segment": false,
364          "status": "ok",
365          "description": "index view query",
366          "data": {
367            "sentry.name": "database query",
368            "db.name": "database",
369            "db.statement": "SELECT \"table\".\"col\" FROM \"table\" WHERE \"table\".\"col\" = %s",
370            "db.type": "sql",
371            "sentry.origin": "auto.otlp.spans"
372          },
373          "links": [],
374          "kind": "client"
375        }
376        "#);
377    }
378
379    #[test]
380    fn parse_span_with_http_attributes() {
381        let json = r#"{
382            "traceId": "89143b0763095bd9c9955e8175d1fb23",
383            "spanId": "e342abb1214ca181",
384            "parentSpanId": "0c7a7dea069bf5a6",
385            "name": "http client request",
386            "kind": 2,
387            "startTimeUnixNano": "1697620454980000000",
388            "endTimeUnixNano": "1697620454980078800",
389            "attributes": [
390                {
391                    "key" : "http.request.method",
392                    "value": {
393                        "stringValue": "GET"
394                    }
395                },
396                {
397                    "key" : "url.path",
398                    "value": {
399                        "stringValue": "/api/search?q=foobar"
400                    }
401                }
402            ]
403        }"#;
404        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
405        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
406        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
407        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
408        {
409          "timestamp": 1697620454.980079,
410          "start_timestamp": 1697620454.98,
411          "exclusive_time": 0.0788,
412          "op": "http.server",
413          "span_id": "e342abb1214ca181",
414          "parent_span_id": "0c7a7dea069bf5a6",
415          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
416          "is_segment": false,
417          "status": "ok",
418          "description": "GET /api/search?q=foobar",
419          "data": {
420            "sentry.name": "http client request",
421            "http.request.method": "GET",
422            "sentry.origin": "auto.otlp.spans",
423            "url.path": "/api/search?q=foobar"
424          },
425          "links": [],
426          "kind": "server"
427        }
428        "#);
429    }
430
431    #[test]
432    fn parse_array_attribute() {
433        let json = r#"{
434            "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
435            "spanId": "fa90fdead5f74052",
436            "parentSpanId": "fa90fdead5f74051",
437            "startTimeUnixNano": "123000000000",
438            "endTimeUnixNano": "123500000000",
439            "name": "cmd.run",
440            "status": {"code": 0},
441            "attributes": [
442                {
443                    "key": "process.args",
444                    "value": {
445                        "arrayValue": {
446                            "values": [
447                                {"stringValue": "node"},
448                                {"stringValue": "--require"},
449                                {"stringValue": "preflight.cjs"}
450                            ]
451                        }
452                    }
453                },
454                {
455                    "key": "process.info",
456                    "value": {
457                        "arrayValue": {
458                            "values": [
459                                {"intValue": 41},
460                                {
461                                    "arrayValue": {
462                                        "values": [
463                                            {"intValue": 42}
464                                    ]}
465                                }
466                            ]
467                        }
468                    }
469                }
470            ]
471        }"#;
472
473        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
474        let event_span = otel_to_sentry_span(otel_span, None, None);
475
476        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
477        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
478        {
479          "timestamp": 123.5,
480          "start_timestamp": 123.0,
481          "exclusive_time": 500.0,
482          "op": "default",
483          "span_id": "fa90fdead5f74052",
484          "parent_span_id": "fa90fdead5f74051",
485          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
486          "is_segment": false,
487          "status": "ok",
488          "description": "cmd.run",
489          "data": {
490            "sentry.name": "cmd.run",
491            "process.args": [
492              "node",
493              "--require",
494              "preflight.cjs"
495            ],
496            "process.info": [
497              41
498            ],
499            "sentry.origin": "auto.otlp.spans"
500          },
501          "links": []
502        }
503        "#);
504    }
505
506    /// Intended to be synced with `relay-event-schema::protocol::span::convert::tests::roundtrip`.
507    #[test]
508    fn parse_sentry_attributes() {
509        let json = r#"{
510            "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
511            "spanId": "fa90fdead5f74052",
512            "parentSpanId": "fa90fdead5f74051",
513            "startTimeUnixNano": "123000000000",
514            "endTimeUnixNano": "123500000000",
515            "name": "myname",
516            "status": {"code": 0, "message": "foo"},
517            "attributes": [
518                {
519                    "key" : "browser.name",
520                    "value": {
521                        "stringValue": "Chrome"
522                    }
523                },
524                {
525                    "key" : "sentry.description",
526                    "value": {
527                        "stringValue": "mydescription"
528                    }
529                },
530                {
531                    "key" : "sentry.environment",
532                    "value": {
533                        "stringValue": "prod"
534                    }
535                },
536                {
537                    "key" : "sentry.op",
538                    "value": {
539                        "stringValue": "myop"
540                    }
541                },
542                {
543                    "key" : "sentry.platform",
544                    "value": {
545                        "stringValue": "php"
546                    }
547                },
548                {
549                    "key" : "sentry.profile_id",
550                    "value": {
551                        "stringValue": "a0aaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab"
552                    }
553                },
554                {
555                    "key" : "sentry.release",
556                    "value": {
557                        "stringValue": "myapp@1.0.0"
558                    }
559                },
560                {
561                    "key" : "sentry.sdk.name",
562                    "value": {
563                        "stringValue": "sentry.php"
564                    }
565                },
566                {
567                    "key" : "sentry.segment.id",
568                    "value": {
569                        "stringValue": "FA90FDEAD5F74052"
570                    }
571                },
572                {
573                    "key" : "sentry.segment.name",
574                    "value": {
575                        "stringValue": "my 1st transaction"
576                    }
577                },
578                {
579                    "key": "sentry.metrics_summary.some_metric",
580                    "value": {
581                        "arrayValue": {
582                            "values": [
583                                {
584                                    "kvlistValue": {
585                                        "values": [
586                                            {
587                                                "key": "min",
588                                                "value": {
589                                                    "doubleValue": 1.0
590                                                }
591                                            },
592                                            {
593                                                "key": "max",
594                                                "value": {
595                                                    "doubleValue": 2.0
596                                                }
597                                            },
598                                            {
599                                                "key": "sum",
600                                                "value": {
601                                                    "doubleValue": 3.0
602                                                }
603                                            },
604                                            {
605                                                "key": "count",
606                                                "value": {
607                                                    "intValue": "2"
608                                                }
609                                            },
610                                            {
611                                                "key": "tags",
612                                                "value": {
613                                                    "kvlistValue": {
614                                                        "values": [
615                                                            {
616                                                                "key": "environment",
617                                                                "value": {
618                                                                    "stringValue": "test"
619                                                                }
620                                                            }
621                                                        ]
622                                                    }
623                                                }
624                                            }
625                                        ]
626                                    }
627                                }
628                            ]
629                        }
630                    }
631                }
632            ]
633        }"#;
634
635        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
636        let event_span = otel_to_sentry_span(otel_span, None, None);
637
638        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
639        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
640        {
641          "timestamp": 123.5,
642          "start_timestamp": 123.0,
643          "exclusive_time": 500.0,
644          "op": "myop",
645          "span_id": "fa90fdead5f74052",
646          "parent_span_id": "fa90fdead5f74051",
647          "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
648          "segment_id": "fa90fdead5f74052",
649          "is_segment": false,
650          "status": "ok",
651          "description": "mydescription",
652          "profile_id": "a0aaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
653          "data": {
654            "browser.name": "Chrome",
655            "sentry.environment": "prod",
656            "sentry.release": "myapp@1.0.0",
657            "sentry.segment.name": "my 1st transaction",
658            "sentry.sdk.name": "sentry.php",
659            "sentry.name": "myname",
660            "sentry.metrics_summary.some_metric": [],
661            "sentry.origin": "auto.otlp.spans",
662            "sentry.status.message": "foo"
663          },
664          "links": [],
665          "platform": "php"
666        }
667        "#);
668    }
669
670    #[test]
671    fn parse_span_is_remote() {
672        let json = r#"{
673            "traceId": "89143b0763095bd9c9955e8175d1fb23",
674            "spanId": "e342abb1214ca181",
675            "parentSpanId": "0c7a7dea069bf5a6",
676            "startTimeUnixNano": "123000000000",
677            "endTimeUnixNano": "123500000000",
678            "flags": 768
679        }"#;
680        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
681        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
682        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
683        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
684        {
685          "timestamp": 123.5,
686          "start_timestamp": 123.0,
687          "exclusive_time": 500.0,
688          "op": "default",
689          "span_id": "e342abb1214ca181",
690          "parent_span_id": "0c7a7dea069bf5a6",
691          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
692          "is_segment": true,
693          "status": "ok",
694          "data": {
695            "sentry.is_remote": true,
696            "sentry.origin": "auto.otlp.spans"
697          },
698          "links": []
699        }
700        "#);
701    }
702
703    #[test]
704    fn parse_span_is_not_remote() {
705        let json = r#"{
706            "traceId": "89143b0763095bd9c9955e8175d1fb23",
707            "spanId": "e342abb1214ca181",
708            "parentSpanId": "0c7a7dea069bf5a6",
709            "startTimeUnixNano": "123000000000",
710            "endTimeUnixNano": "123500000000",
711            "flags": 256
712        }"#;
713        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
714        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
715        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
716        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
717        {
718          "timestamp": 123.5,
719          "start_timestamp": 123.0,
720          "exclusive_time": 500.0,
721          "op": "default",
722          "span_id": "e342abb1214ca181",
723          "parent_span_id": "0c7a7dea069bf5a6",
724          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
725          "is_segment": false,
726          "status": "ok",
727          "data": {
728            "sentry.is_remote": false,
729            "sentry.origin": "auto.otlp.spans"
730          },
731          "links": []
732        }
733        "#);
734    }
735
736    #[test]
737    fn extract_span_kind() {
738        let json = r#"{
739            "traceId": "89143b0763095bd9c9955e8175d1fb23",
740            "spanId": "e342abb1214ca181",
741            "parentSpanId": "0c7a7dea069bf5a6",
742            "startTimeUnixNano": "123000000000",
743            "endTimeUnixNano": "123500000000",
744            "kind": 3
745        }"#;
746        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
747        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
748        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
749        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
750        {
751          "timestamp": 123.5,
752          "start_timestamp": 123.0,
753          "exclusive_time": 500.0,
754          "op": "default",
755          "span_id": "e342abb1214ca181",
756          "parent_span_id": "0c7a7dea069bf5a6",
757          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
758          "is_segment": false,
759          "status": "ok",
760          "data": {
761            "sentry.origin": "auto.otlp.spans"
762          },
763          "links": [],
764          "kind": "client"
765        }
766        "#);
767    }
768
769    #[test]
770    fn parse_link() {
771        let json = r#"{
772            "traceId": "3c79f60c11214eb38604f4ae0781bfb2",
773            "spanId": "e342abb1214ca181",
774            "links": [
775                {
776                    "traceId": "4c79f60c11214eb38604f4ae0781bfb2",
777                    "spanId": "fa90fdead5f74052",
778                    "attributes": [
779                        {
780                            "key": "str_key",
781                            "value": {
782                                "stringValue": "str_value"
783                            }
784                        },
785                        {
786                            "key": "bool_key",
787                            "value": {
788                                "boolValue": true
789                            }
790                        },
791                        {
792                            "key": "int_key",
793                            "value": {
794                                "intValue": "123"
795                            }
796                        },
797                        {
798                            "key": "double_key",
799                            "value": {
800                                "doubleValue": 1.23
801                            }
802                        }
803                    ],
804                    "flags": 1
805                }
806            ]
807        }"#;
808        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
809        let event_span: EventSpan = otel_to_sentry_span(otel_span, None, None);
810        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
811
812        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
813        {
814          "timestamp": 0.0,
815          "start_timestamp": 0.0,
816          "exclusive_time": 0.0,
817          "op": "default",
818          "span_id": "e342abb1214ca181",
819          "trace_id": "3c79f60c11214eb38604f4ae0781bfb2",
820          "is_segment": true,
821          "status": "ok",
822          "data": {
823            "sentry.origin": "auto.otlp.spans"
824          },
825          "links": [
826            {
827              "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
828              "span_id": "fa90fdead5f74052",
829              "sampled": true,
830              "attributes": {
831                "bool_key": true,
832                "double_key": 1.23,
833                "int_key": 123,
834                "str_key": "str_value"
835              }
836            }
837          ]
838        }
839        "#);
840    }
841
842    #[test]
843    fn parse_span_error_status() {
844        let json = r#"{
845          "traceId": "89143b0763095bd9c9955e8175d1fb23",
846          "spanId": "e342abb1214ca181",
847          "status": {
848            "code": 2,
849            "message": "2 is the error status code"
850          }
851        }"#;
852        let otel_span: OtelSpan = serde_json::from_str(json).unwrap();
853        let event_span = otel_to_sentry_span(otel_span, None, None);
854        let annotated_span: Annotated<EventSpan> = Annotated::new(event_span);
855        insta::assert_json_snapshot!(SerializableAnnotated(&annotated_span), @r#"
856        {
857          "timestamp": 0.0,
858          "start_timestamp": 0.0,
859          "exclusive_time": 0.0,
860          "op": "default",
861          "span_id": "e342abb1214ca181",
862          "trace_id": "89143b0763095bd9c9955e8175d1fb23",
863          "is_segment": true,
864          "status": "internal_error",
865          "data": {
866            "sentry.origin": "auto.otlp.spans",
867            "sentry.status.message": "2 is the error status code"
868          },
869          "links": []
870        }
871        "#);
872    }
873}