Skip to main content

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