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