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
7pub 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 #[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}