1use super::json_types::{
7 self, AnyValue, ExportTraceServiceRequest, InstrumentationScope, KeyValue, OtlpSpan,
8 OtlpSpanEvent, OtlpSpanLink, Resource, ResourceSpans, ScopeSpans, Status,
9};
10use super::OtlpResourceInfo;
11use crate::span::v04::{Span, SpanEvent, SpanLink};
12use crate::span::TraceData;
13use std::borrow::Borrow;
14
15const MAX_ATTRIBUTES_PER_SPAN: usize = 128;
17
18pub fn map_traces_to_otlp<T: TraceData>(
31 trace_chunks: Vec<Vec<Span<T>>>,
32 resource_info: &OtlpResourceInfo,
33) -> ExportTraceServiceRequest {
34 let resource = build_resource(resource_info);
35 let mut all_spans: Vec<OtlpSpan> = Vec::new();
36 for chunk in &trace_chunks {
37 let chunk_trace_id_high: u64 = chunk
41 .iter()
42 .find_map(|s| {
43 let high = (s.trace_id >> 64) as u64;
44 if high != 0 {
45 return Some(high);
46 }
47 s.meta
48 .get("_dd.p.tid")
49 .and_then(|v| u64::from_str_radix(v.borrow(), 16).ok())
50 })
51 .unwrap_or(0);
52 for span in chunk {
53 all_spans.push(map_span(span, &resource_info.service, chunk_trace_id_high));
54 }
55 }
56 let scope_spans = ScopeSpans {
57 scope: Some(InstrumentationScope::default()),
58 spans: all_spans,
59 schema_url: None,
60 };
61 let resource_spans = ResourceSpans {
62 resource: Some(resource),
63 scope_spans: vec![scope_spans],
64 };
65 ExportTraceServiceRequest {
66 resource_spans: vec![resource_spans],
67 }
68}
69
70fn build_resource(resource_info: &OtlpResourceInfo) -> Resource {
71 let mut attributes: Vec<KeyValue> = Vec::new();
72 if !resource_info.service.is_empty() {
73 attributes.push(KeyValue {
74 key: "service.name".to_string(),
75 value: AnyValue::StringValue(resource_info.service.clone()),
76 });
77 }
78 if !resource_info.env.is_empty() {
79 attributes.push(KeyValue {
80 key: "deployment.environment.name".to_string(),
81 value: AnyValue::StringValue(resource_info.env.clone()),
82 });
83 }
84 if !resource_info.app_version.is_empty() {
85 attributes.push(KeyValue {
86 key: "service.version".to_string(),
87 value: AnyValue::StringValue(resource_info.app_version.clone()),
88 });
89 }
90 attributes.push(KeyValue {
91 key: "telemetry.sdk.name".to_string(),
92 value: AnyValue::StringValue("datadog".to_string()),
93 });
94 if !resource_info.language.is_empty() {
95 attributes.push(KeyValue {
96 key: "telemetry.sdk.language".to_string(),
97 value: AnyValue::StringValue(resource_info.language.clone()),
98 });
99 }
100 if !resource_info.tracer_version.is_empty() {
101 attributes.push(KeyValue {
102 key: "telemetry.sdk.version".to_string(),
103 value: AnyValue::StringValue(resource_info.tracer_version.clone()),
104 });
105 }
106 if !resource_info.runtime_id.is_empty() {
107 attributes.push(KeyValue {
108 key: "runtime-id".to_string(),
109 value: AnyValue::StringValue(resource_info.runtime_id.clone()),
110 });
111 }
112 Resource { attributes }
113}
114
115fn map_span<T: TraceData>(
116 span: &Span<T>,
117 resource_service: &str,
118 chunk_trace_id_high: u64,
119) -> OtlpSpan {
120 let trace_id_128 = ((chunk_trace_id_high as u128) << 64) | (span.trace_id as u64 as u128);
124 let trace_id_hex = format!("{:032x}", trace_id_128);
125 let span_id_hex = format!("{:016x}", span.span_id);
126 let parent_span_id = if span.parent_id != 0 {
127 Some(format!("{:016x}", span.parent_id))
128 } else {
129 None
130 };
131 let start_nano = span.start;
132 let end_nano = span.start + span.duration;
133 let start_time_unix_nano = start_nano.to_string();
134 let end_time_unix_nano = end_nano.to_string();
135 let kind = span
138 .meta
139 .get("span.kind")
140 .map(|v| tag_to_otlp_kind(v.borrow()))
141 .unwrap_or_else(|| dd_type_to_otlp_kind(span.r#type.borrow()));
142 let (attributes, dropped_attributes_count) = map_attributes(span, resource_service);
143 let error_msg = span.meta.get("error.msg").map(|v| v.borrow().to_string());
144 let status = if span.error != 0 {
145 Status {
146 message: error_msg,
147 code: json_types::status_code::ERROR,
148 }
149 } else {
150 Status {
151 message: None,
152 code: json_types::status_code::UNSET,
153 }
154 };
155 let flags = span
157 .metrics
158 .get("_sampling_priority_v1")
159 .map(|p| if *p >= 1.0 { 1u32 } else { 0u32 });
160 let trace_state = span
161 .meta
162 .get("tracestate")
163 .map(|v| v.borrow().to_string())
164 .filter(|s| !s.is_empty());
165 let links = span.span_links.iter().map(map_span_link).collect();
166 let (events, dropped_events_count) = map_span_events(&span.span_events);
167 OtlpSpan {
168 trace_id: trace_id_hex,
169 span_id: span_id_hex,
170 parent_span_id,
171 trace_state,
172 name: span.resource.borrow().to_string(),
173 kind,
174 start_time_unix_nano,
175 end_time_unix_nano,
176 attributes,
177 status,
178 links,
179 events,
180 dropped_attributes_count: if dropped_attributes_count > 0 {
181 Some(dropped_attributes_count as u32)
182 } else {
183 None
184 },
185 dropped_events_count: if dropped_events_count > 0 {
186 Some(dropped_events_count as u32)
187 } else {
188 None
189 },
190 flags,
191 }
192}
193
194fn map_span_link<T: TraceData>(link: &SpanLink<T>) -> OtlpSpanLink {
195 let trace_id_128 = ((link.trace_id_high as u128) << 64) | (link.trace_id as u128);
196 let trace_id_hex = format!("{:032x}", trace_id_128);
197 let span_id_hex = format!("{:016x}", link.span_id);
198 let trace_state = if link.tracestate.borrow().is_empty() {
199 None
200 } else {
201 Some(link.tracestate.borrow().to_string())
202 };
203 let attributes: Vec<KeyValue> = link
204 .attributes
205 .iter()
206 .map(|(k, v)| KeyValue {
207 key: k.borrow().to_string(),
208 value: AnyValue::StringValue(v.borrow().to_string()),
209 })
210 .collect();
211 OtlpSpanLink {
212 trace_id: trace_id_hex,
213 span_id: span_id_hex,
214 trace_state,
215 attributes,
216 dropped_attributes_count: None,
217 }
218}
219
220fn map_span_events<T: TraceData>(events: &[SpanEvent<T>]) -> (Vec<OtlpSpanEvent>, usize) {
221 const MAX_EVENTS_PER_SPAN: usize = 128;
222 let mut otlp_events = Vec::with_capacity(events.len().min(MAX_EVENTS_PER_SPAN));
223 for ev in events.iter().take(MAX_EVENTS_PER_SPAN) {
224 let attributes: Vec<KeyValue> = ev
225 .attributes
226 .iter()
227 .map(|(k, v)| event_attr_to_key_value(k, v))
228 .collect();
229 otlp_events.push(OtlpSpanEvent {
230 time_unix_nano: ev.time_unix_nano.to_string(),
231 name: ev.name.borrow().to_string(),
232 attributes,
233 dropped_attributes_count: None,
234 });
235 }
236 let dropped = events.len().saturating_sub(otlp_events.len());
237 (otlp_events, dropped)
238}
239
240fn event_attr_to_key_value<T: TraceData>(
241 k: &T::Text,
242 v: &crate::span::v04::AttributeAnyValue<T>,
243) -> KeyValue {
244 use crate::span::v04::AttributeArrayValue;
245 let value = match v {
246 crate::span::v04::AttributeAnyValue::SingleValue(av) => match av {
247 AttributeArrayValue::String(s) => AnyValue::StringValue(s.borrow().to_string()),
248 AttributeArrayValue::Boolean(b) => AnyValue::BoolValue(*b),
249 AttributeArrayValue::Integer(i) => AnyValue::IntValue(*i),
250 AttributeArrayValue::Double(d) => AnyValue::DoubleValue(*d),
251 },
252 crate::span::v04::AttributeAnyValue::Array(items) => {
253 let values = items
254 .iter()
255 .map(|item| match item {
256 AttributeArrayValue::String(s) => AnyValue::StringValue(s.borrow().to_string()),
257 AttributeArrayValue::Boolean(b) => AnyValue::BoolValue(*b),
258 AttributeArrayValue::Integer(i) => AnyValue::IntValue(*i),
259 AttributeArrayValue::Double(d) => AnyValue::DoubleValue(*d),
260 })
261 .collect();
262 AnyValue::ArrayValue(crate::otlp_encoder::json_types::ArrayValue { values })
263 }
264 };
265 KeyValue {
266 key: k.borrow().to_string(),
267 value,
268 }
269}
270
271fn tag_to_otlp_kind(t: &str) -> i32 {
273 match t.to_lowercase().as_str() {
274 "server" => json_types::span_kind::SERVER,
275 "client" => json_types::span_kind::CLIENT,
276 "producer" => json_types::span_kind::PRODUCER,
277 "consumer" => json_types::span_kind::CONSUMER,
278 "internal" => json_types::span_kind::INTERNAL,
279 _ => json_types::span_kind::UNSPECIFIED,
280 }
281}
282
283fn dd_type_to_otlp_kind(t: &str) -> i32 {
285 match t.to_lowercase().as_str() {
286 "server" | "web" | "http" => json_types::span_kind::SERVER,
287 "client" => json_types::span_kind::CLIENT,
288 "producer" => json_types::span_kind::PRODUCER,
289 "consumer" => json_types::span_kind::CONSUMER,
290 _ => json_types::span_kind::INTERNAL,
291 }
292}
293
294fn map_attributes<T: TraceData>(span: &Span<T>, resource_service: &str) -> (Vec<KeyValue>, usize) {
295 let mut attrs: Vec<KeyValue> = Vec::new();
296 let span_service = span.service.borrow();
298 let has_per_span_service = !span_service.is_empty() && span_service != resource_service;
299 if has_per_span_service {
300 attrs.push(KeyValue {
301 key: "service.name".to_string(),
302 value: AnyValue::StringValue(span_service.to_string()),
303 });
304 }
305 let operation_name = span.name.borrow();
306 let has_operation_name = !operation_name.is_empty();
307 if has_operation_name {
308 attrs.push(KeyValue {
309 key: "operation.name".to_string(),
310 value: AnyValue::StringValue(operation_name.to_string()),
311 });
312 }
313 let span_type = span.r#type.borrow();
314 let has_span_type = !span_type.is_empty();
315 if has_span_type {
316 attrs.push(KeyValue {
317 key: "span.type".to_string(),
318 value: AnyValue::StringValue(span_type.to_string()),
319 });
320 }
321 let resource_name = span.resource.borrow();
322 let has_resource_name = !resource_name.is_empty();
323 if has_resource_name {
324 attrs.push(KeyValue {
325 key: "resource.name".to_string(),
326 value: AnyValue::StringValue(resource_name.to_string()),
327 });
328 }
329 for (k, v) in span.meta.iter() {
330 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
331 break;
332 }
333 attrs.push(KeyValue {
334 key: k.borrow().to_string(),
335 value: AnyValue::StringValue(v.borrow().to_string()),
336 });
337 }
338 for (k, v) in span.metrics.iter() {
339 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
340 break;
341 }
342 let value = if v.fract() == 0.0 && (*v >= i64::MIN as f64 && *v <= i64::MAX as f64) {
343 AnyValue::IntValue(*v as i64)
344 } else {
345 AnyValue::DoubleValue(*v)
346 };
347 attrs.push(KeyValue {
348 key: k.borrow().to_string(),
349 value,
350 });
351 }
352 for (k, v) in span.meta_struct.iter() {
353 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
354 break;
355 }
356 attrs.push(KeyValue {
357 key: k.borrow().to_string(),
358 value: AnyValue::BytesValue(v.borrow().to_vec()),
359 });
360 }
361 let total = (if has_per_span_service { 1 } else { 0 })
362 + (if has_operation_name { 1 } else { 0 })
363 + (if has_span_type { 1 } else { 0 })
364 + (if has_resource_name { 1 } else { 0 })
365 + span.meta.len()
366 + span.metrics.len()
367 + span.meta_struct.len();
368 let dropped = total.saturating_sub(attrs.len());
369 (attrs, dropped)
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::otlp_encoder::OtlpResourceInfo;
376 use crate::span::BytesData;
377
378 #[test]
379 fn test_trace_id_span_id_format() {
380 let resource_info = OtlpResourceInfo::default();
381 let span: Span<BytesData> = Span {
382 trace_id: 0xD269B633813FC60C_u128, span_id: 0xEEE19B7EC3C1B174,
384 parent_id: 0xEEE19B7EC3C1B173,
385 name: libdd_tinybytes::BytesString::from_static("test"),
386 service: libdd_tinybytes::BytesString::from_static("svc"),
387 resource: libdd_tinybytes::BytesString::from_static("res"),
388 r#type: libdd_tinybytes::BytesString::from_static("web"),
389 start: 1544712660000000000,
390 duration: 1000000000,
391 error: 0,
392 ..Default::default()
393 };
394 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
395 let rs = &req.resource_spans[0];
396 let otlp_span = &rs.scope_spans[0].spans[0];
397 assert_eq!(otlp_span.trace_id, "0000000000000000d269b633813fc60c");
398 assert_eq!(otlp_span.span_id, "eee19b7ec3c1b174");
399 assert_eq!(
400 otlp_span.parent_span_id.as_deref(),
401 Some("eee19b7ec3c1b173")
402 );
403 assert_eq!(otlp_span.kind, json_types::span_kind::SERVER);
404 assert_eq!(otlp_span.start_time_unix_nano, "1544712660000000000");
405 assert_eq!(otlp_span.end_time_unix_nano, "1544712661000000000");
406 assert_eq!(rs.scope_spans[0].scope.as_ref().unwrap().name, None);
407 }
408
409 #[test]
410 fn test_status_error_message_from_meta() {
411 let resource_info = OtlpResourceInfo::default();
412 let mut span: Span<BytesData> = Span {
413 trace_id: 1,
414 span_id: 2,
415 name: libdd_tinybytes::BytesString::from_static("err_span"),
416 start: 0,
417 duration: 1,
418 error: 1,
419 ..Default::default()
420 };
421 span.meta.insert(
422 libdd_tinybytes::BytesString::from_static("error.msg"),
423 libdd_tinybytes::BytesString::from_static("something broke"),
424 );
425 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
426 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
427 let status = &otlp_span.status;
428 assert_eq!(status.code, json_types::status_code::ERROR);
429 assert_eq!(status.message.as_deref(), Some("something broke"));
430 }
431
432 #[test]
433 fn test_metrics_as_int_or_double() {
434 let resource_info = OtlpResourceInfo::default();
435 let mut span: Span<BytesData> = Span {
436 trace_id: 1,
437 span_id: 2,
438 name: libdd_tinybytes::BytesString::from_static("m"),
439 start: 0,
440 duration: 1,
441 ..Default::default()
442 };
443 span.metrics
444 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
445 span.metrics.insert(
446 libdd_tinybytes::BytesString::from_static("rate"),
447 std::f64::consts::PI,
448 );
449 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
450 let json = serde_json::to_value(&req).unwrap();
451 let attrs = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"];
452 let count_kv = attrs
453 .as_array()
454 .unwrap()
455 .iter()
456 .find(|a| a["key"] == "count")
457 .unwrap();
458 assert_eq!(count_kv["value"]["intValue"], "42");
459 let rate_kv = attrs
460 .as_array()
461 .unwrap()
462 .iter()
463 .find(|a| a["key"] == "rate")
464 .unwrap();
465 let rate = rate_kv["value"]["doubleValue"].as_f64().unwrap();
466 assert!((rate - std::f64::consts::PI).abs() < 1e-9);
467 }
468
469 #[test]
470 fn test_128bit_trace_id_from_dd_p_tid() {
471 let resource_info = OtlpResourceInfo::default();
474 let mut span: Span<BytesData> = Span {
475 trace_id: 0xD269B633813FC60C_u128, span_id: 1,
477 name: libdd_tinybytes::BytesString::from_static("s"),
478 start: 0,
479 duration: 1,
480 ..Default::default()
481 };
482 span.meta.insert(
483 "_dd.p.tid".into(),
484 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
485 );
486 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
487 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
488 assert_eq!(otlp_span.trace_id, "5b8efff798038103d269b633813fc60c");
489 }
490
491 #[test]
492 fn test_128bit_trace_id_from_native_span_field() {
493 let resource_info = OtlpResourceInfo::default();
497 let full: u128 = 0x5b8efff798038103_d269b633813fc60c_u128;
498 let root: Span<BytesData> = Span {
499 trace_id: full,
500 span_id: 1,
501 name: libdd_tinybytes::BytesString::from_static("root"),
502 start: 0,
503 duration: 1,
504 ..Default::default()
505 };
506 let child: Span<BytesData> = Span {
508 trace_id: 0xD269B633813FC60C_u128,
509 span_id: 2,
510 parent_id: 1,
511 name: libdd_tinybytes::BytesString::from_static("child"),
512 start: 0,
513 duration: 1,
514 ..Default::default()
515 };
516 let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info);
517 let spans = &req.resource_spans[0].scope_spans[0].spans;
518 let expected = "5b8efff798038103d269b633813fc60c";
519 assert_eq!(spans[0].trace_id, expected);
520 assert_eq!(spans[1].trace_id, expected);
521 }
522
523 #[test]
524 fn test_128bit_trace_id_without_dd_p_tid() {
525 let resource_info = OtlpResourceInfo::default();
528 let span: Span<BytesData> = Span {
529 trace_id: 0xD269B633813FC60C_u128,
530 span_id: 1,
531 name: libdd_tinybytes::BytesString::from_static("s"),
532 start: 0,
533 duration: 1,
534 ..Default::default()
535 };
536 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
537 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
538 assert_eq!(otlp_span.trace_id, "0000000000000000d269b633813fc60c");
539 }
540
541 #[test]
542 fn test_128bit_trace_id_propagated_to_chunk_children() {
543 let resource_info = OtlpResourceInfo::default();
547 let low: u128 = 0xD269B633813FC60C_u128;
548 let mut root: Span<BytesData> = Span {
549 trace_id: low,
550 span_id: 1,
551 name: libdd_tinybytes::BytesString::from_static("root"),
552 start: 0,
553 duration: 1,
554 ..Default::default()
555 };
556 root.meta.insert(
557 "_dd.p.tid".into(),
558 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
559 );
560 let child_a: Span<BytesData> = Span {
561 trace_id: low,
562 span_id: 2,
563 parent_id: 1,
564 name: libdd_tinybytes::BytesString::from_static("child_a"),
565 start: 0,
566 duration: 1,
567 ..Default::default()
568 };
569 let child_b: Span<BytesData> = Span {
570 trace_id: low,
571 span_id: 3,
572 parent_id: 1,
573 name: libdd_tinybytes::BytesString::from_static("child_b"),
574 start: 0,
575 duration: 1,
576 ..Default::default()
577 };
578 let req = map_traces_to_otlp(vec![vec![root, child_a, child_b]], &resource_info);
579 let spans = &req.resource_spans[0].scope_spans[0].spans;
580 assert_eq!(spans.len(), 3);
581 let expected = "5b8efff798038103d269b633813fc60c";
582 for s in spans {
583 assert_eq!(s.trace_id, expected, "span {} mismatched", s.span_id);
584 }
585 }
586
587 #[test]
588 fn test_128bit_trace_id_isolation_across_chunks() {
589 let resource_info = OtlpResourceInfo::default();
592 let low_a: u128 = 0x1111111111111111_u128;
593 let low_b: u128 = 0x2222222222222222_u128;
594 let mut root_a: Span<BytesData> = Span {
595 trace_id: low_a,
596 span_id: 1,
597 name: libdd_tinybytes::BytesString::from_static("root_a"),
598 start: 0,
599 duration: 1,
600 ..Default::default()
601 };
602 root_a.meta.insert(
603 "_dd.p.tid".into(),
604 libdd_tinybytes::BytesString::from_static("aaaaaaaaaaaaaaaa"),
605 );
606 let child_a: Span<BytesData> = Span {
607 trace_id: low_a,
608 span_id: 2,
609 parent_id: 1,
610 name: libdd_tinybytes::BytesString::from_static("child_a"),
611 start: 0,
612 duration: 1,
613 ..Default::default()
614 };
615 let mut root_b: Span<BytesData> = Span {
616 trace_id: low_b,
617 span_id: 3,
618 name: libdd_tinybytes::BytesString::from_static("root_b"),
619 start: 0,
620 duration: 1,
621 ..Default::default()
622 };
623 root_b.meta.insert(
624 "_dd.p.tid".into(),
625 libdd_tinybytes::BytesString::from_static("bbbbbbbbbbbbbbbb"),
626 );
627 let child_b: Span<BytesData> = Span {
628 trace_id: low_b,
629 span_id: 4,
630 parent_id: 3,
631 name: libdd_tinybytes::BytesString::from_static("child_b"),
632 start: 0,
633 duration: 1,
634 ..Default::default()
635 };
636 let req = map_traces_to_otlp(
637 vec![vec![root_a, child_a], vec![root_b, child_b]],
638 &resource_info,
639 );
640 let spans = &req.resource_spans[0].scope_spans[0].spans;
641 assert_eq!(spans.len(), 4);
642 let expect_a = "aaaaaaaaaaaaaaaa1111111111111111";
644 let expect_b = "bbbbbbbbbbbbbbbb2222222222222222";
645 assert_eq!(spans[0].trace_id, expect_a);
646 assert_eq!(spans[1].trace_id, expect_a);
647 assert_eq!(spans[2].trace_id, expect_b);
648 assert_eq!(spans[3].trace_id, expect_b);
649 }
650
651 #[test]
652 fn test_chunk_with_malformed_dd_p_tid_on_root_falls_back() {
653 let resource_info = OtlpResourceInfo::default();
657 let low: u128 = 0xD269B633813FC60C_u128;
658 let mut root: Span<BytesData> = Span {
659 trace_id: low,
660 span_id: 1,
661 name: libdd_tinybytes::BytesString::from_static("root"),
662 start: 0,
663 duration: 1,
664 ..Default::default()
665 };
666 root.meta.insert(
667 "_dd.p.tid".into(),
668 libdd_tinybytes::BytesString::from_static("not-hex"),
669 );
670 let child_no_tag: Span<BytesData> = Span {
671 trace_id: low,
672 span_id: 2,
673 parent_id: 1,
674 name: libdd_tinybytes::BytesString::from_static("child_no_tag"),
675 start: 0,
676 duration: 1,
677 ..Default::default()
678 };
679 let mut child_valid: Span<BytesData> = Span {
680 trace_id: low,
681 span_id: 3,
682 parent_id: 1,
683 name: libdd_tinybytes::BytesString::from_static("child_valid"),
684 start: 0,
685 duration: 1,
686 ..Default::default()
687 };
688 child_valid.meta.insert(
689 "_dd.p.tid".into(),
690 libdd_tinybytes::BytesString::from_static("dddddddddddddddd"),
691 );
692 let req = map_traces_to_otlp(vec![vec![root, child_no_tag, child_valid]], &resource_info);
693 let spans = &req.resource_spans[0].scope_spans[0].spans;
694 let expected = "ddddddddddddddddd269b633813fc60c";
697 assert_eq!(spans[0].trace_id, expected);
698 assert_eq!(spans[1].trace_id, expected);
699 assert_eq!(spans[2].trace_id, expected);
700 }
701
702 #[test]
703 fn test_empty_chunk_does_not_panic() {
704 let resource_info = OtlpResourceInfo::default();
706 let empty: Vec<Vec<Span<BytesData>>> = vec![vec![]];
707 let req = map_traces_to_otlp(empty, &resource_info);
708 let spans = &req.resource_spans[0].scope_spans[0].spans;
709 assert!(spans.is_empty());
710 }
711
712 #[test]
713 fn test_tracestate_from_meta() {
714 let resource_info = OtlpResourceInfo::default();
715 let mut span: Span<BytesData> = Span {
716 trace_id: 1,
717 span_id: 2,
718 name: libdd_tinybytes::BytesString::from_static("s"),
719 start: 0,
720 duration: 1,
721 ..Default::default()
722 };
723 span.meta.insert(
724 "tracestate".into(),
725 libdd_tinybytes::BytesString::from_static("vendor1=abc,rojo=00f067"),
726 );
727 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
728 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
729 assert_eq!(
730 otlp_span.trace_state.as_deref(),
731 Some("vendor1=abc,rojo=00f067")
732 );
733 }
734
735 #[test]
736 fn test_meta_struct_as_bytes_value() {
737 use libdd_tinybytes::Bytes;
738 let resource_info = OtlpResourceInfo::default();
739 let mut span: Span<BytesData> = Span {
740 trace_id: 1,
741 span_id: 2,
742 name: libdd_tinybytes::BytesString::from_static("s"),
743 start: 0,
744 duration: 1,
745 ..Default::default()
746 };
747 span.meta_struct
748 .insert("my_key".into(), Bytes::from(vec![1u8, 2, 3]));
749 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
750 let json = serde_json::to_value(&req).unwrap();
751 let attrs = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"];
752 let kv = attrs
753 .as_array()
754 .unwrap()
755 .iter()
756 .find(|a| a["key"] == "my_key")
757 .expect("my_key attribute not found");
758 assert_eq!(kv["value"]["bytesValue"], "AQID");
760 }
761
762 #[test]
763 fn test_operation_name_attribute() {
764 let resource_info = OtlpResourceInfo::default();
765 let span: Span<BytesData> = Span {
766 trace_id: 1,
767 span_id: 2,
768 name: libdd_tinybytes::BytesString::from_static("my.operation"),
769 start: 0,
770 duration: 1,
771 ..Default::default()
772 };
773 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
774 let json = serde_json::to_value(&req).unwrap();
775 let attrs = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"];
776 let kv = attrs
777 .as_array()
778 .unwrap()
779 .iter()
780 .find(|a| a["key"] == "operation.name")
781 .expect("operation.name attribute not found");
782 assert_eq!(kv["value"]["stringValue"], "my.operation");
783 }
784
785 #[test]
786 fn test_span_type_attribute() {
787 let resource_info = OtlpResourceInfo::default();
788 let span: Span<BytesData> = Span {
789 trace_id: 1,
790 span_id: 2,
791 name: libdd_tinybytes::BytesString::from_static("s"),
792 r#type: libdd_tinybytes::BytesString::from_static("grpc"),
793 start: 0,
794 duration: 1,
795 ..Default::default()
796 };
797 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
798 let json = serde_json::to_value(&req).unwrap();
799 let attrs = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"];
800 let kv = attrs
801 .as_array()
802 .unwrap()
803 .iter()
804 .find(|a| a["key"] == "span.type")
805 .expect("span.type attribute not found");
806 assert_eq!(kv["value"]["stringValue"], "grpc");
807 }
808
809 #[test]
810 fn test_resource_name_attribute() {
811 let resource_info = OtlpResourceInfo::default();
812 let span: Span<BytesData> = Span {
813 trace_id: 1,
814 span_id: 2,
815 name: libdd_tinybytes::BytesString::from_static("s"),
816 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
817 start: 0,
818 duration: 1,
819 ..Default::default()
820 };
821 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
822 let json = serde_json::to_value(&req).unwrap();
823 let otlp_span = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0];
824 assert_eq!(otlp_span["name"], "GET /api/users");
826 let kv = otlp_span["attributes"]
828 .as_array()
829 .unwrap()
830 .iter()
831 .find(|a| a["key"] == "resource.name")
832 .expect("resource.name attribute not found");
833 assert_eq!(kv["value"]["stringValue"], "GET /api/users");
834 }
835
836 #[test]
837 fn test_empty_resource_name_not_emitted() {
838 let resource_info = OtlpResourceInfo::default();
842 let span: Span<BytesData> = Span {
843 trace_id: 1,
844 span_id: 2,
845 name: libdd_tinybytes::BytesString::from_static("s"),
846 start: 0,
848 duration: 1,
849 ..Default::default()
850 };
851 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
852 let json = serde_json::to_value(&req).unwrap();
853 let attrs = json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"]
854 .as_array()
855 .unwrap();
856 assert!(
857 !attrs.iter().any(|a| a["key"] == "resource.name"),
858 "resource.name should not be emitted when resource is empty"
859 );
860 }
861
862 #[test]
863 fn test_per_span_service_name_attribute() {
864 let resource_info = OtlpResourceInfo {
867 service: "resource-svc".to_string(),
868 ..Default::default()
869 };
870 let span: Span<BytesData> = Span {
871 trace_id: 1,
872 span_id: 2,
873 name: libdd_tinybytes::BytesString::from_static("s"),
874 service: libdd_tinybytes::BytesString::from_static("span-svc"),
875 start: 0,
876 duration: 1,
877 ..Default::default()
878 };
879 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
880 let json = serde_json::to_value(&req).unwrap();
881 let attrs = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"];
882 let kv = attrs
883 .as_array()
884 .unwrap()
885 .iter()
886 .find(|a| a["key"] == "service.name")
887 .expect("service.name attribute not found");
888 assert_eq!(kv["value"]["stringValue"], "span-svc");
889 }
890
891 #[test]
892 fn test_unsampled_span_flags_zero() {
893 let resource_info = OtlpResourceInfo::default();
895 let mut span: Span<BytesData> = Span {
896 trace_id: 1,
897 span_id: 2,
898 name: libdd_tinybytes::BytesString::from_static("s"),
899 start: 0,
900 duration: 1,
901 ..Default::default()
902 };
903 span.metrics.insert("_sampling_priority_v1".into(), 0.0);
904 let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
905 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
906 assert_eq!(otlp_span.flags, Some(0));
907 }
908}