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