1use super::OtlpResourceInfo;
13use crate::span::v04::{Span, SpanEvent, SpanLink};
14use crate::span::{TraceData, SPAN_LINK_FLAGS_SET_SENTINEL};
15use std::borrow::Borrow;
16
17use libdd_trace_protobuf::opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest as ProtoReq;
18use libdd_trace_protobuf::opentelemetry::proto::common::v1::{
19 any_value::Value as ProtoValue, AnyValue as ProtoAnyValue, ArrayValue as ProtoArrayValue,
20 InstrumentationScope as ProtoScope, KeyValue as ProtoKeyValue,
21};
22use libdd_trace_protobuf::opentelemetry::proto::resource::v1::Resource as ProtoResource;
23use libdd_trace_protobuf::opentelemetry::proto::trace::v1::{
24 span::{Event as ProtoEvent, Link as ProtoLink},
25 ResourceSpans as ProtoResourceSpans, ScopeSpans as ProtoScopeSpans, Span as ProtoSpan,
26 Status as ProtoStatus,
27};
28
29pub(crate) const MAX_ATTRIBUTES_PER_SPAN: usize = 128;
31
32mod span_kind {
34 pub const UNSPECIFIED: i32 = 0;
35 pub const INTERNAL: i32 = 1;
36 pub const SERVER: i32 = 2;
37 pub const CLIENT: i32 = 3;
38 pub const PRODUCER: i32 = 4;
39 pub const CONSUMER: i32 = 5;
40}
41
42pub mod status_code {
45 pub const UNSET: i32 = 0;
46 pub const ERROR: i32 = 2;
47}
48
49fn span_status<T: TraceData>(span: &Span<T>) -> (i32, Option<String>) {
57 if span.error != 0 {
58 let message = span
59 .meta
60 .get("error.msg")
61 .or_else(|| span.meta.get("error.message"))
62 .map(|v| v.borrow().to_string());
63 (status_code::ERROR, message)
64 } else {
65 (status_code::UNSET, None)
66 }
67}
68
69fn span_kind<T: TraceData>(span: &Span<T>) -> i32 {
71 span.meta
72 .get("span.kind")
73 .map(|v| tag_to_otlp_kind(v.borrow()))
74 .unwrap_or_else(|| dd_type_to_otlp_kind(span.r#type.borrow()))
75}
76
77fn chunk_trace_id_high<T: TraceData>(chunk: &[Span<T>]) -> u64 {
79 chunk
80 .iter()
81 .find_map(|s| {
82 let high = (s.trace_id >> 64) as u64;
83 if high != 0 {
84 return Some(high);
85 }
86 s.meta
87 .get("_dd.p.tid")
88 .and_then(|v| u64::from_str_radix(v.borrow(), 16).ok())
89 })
90 .unwrap_or(0)
91}
92
93fn span_sampling_flags<T: TraceData>(span: &Span<T>) -> u32 {
94 span.metrics
95 .get("_sampling_priority_v1")
96 .is_some_and(|priority| *priority >= 1.0)
97 .into()
98}
99
100fn chunk_sampling_flags<T: TraceData>(chunk: &[Span<T>]) -> u32 {
105 chunk
106 .iter()
107 .find_map(|span| span.metrics.get("_sampling_priority_v1"))
108 .is_some_and(|priority| *priority > 0.0)
109 .into()
110}
111
112fn tag_to_otlp_kind(t: &str) -> i32 {
114 if t.eq_ignore_ascii_case("server") {
118 span_kind::SERVER
119 } else if t.eq_ignore_ascii_case("client") {
120 span_kind::CLIENT
121 } else if t.eq_ignore_ascii_case("producer") {
122 span_kind::PRODUCER
123 } else if t.eq_ignore_ascii_case("consumer") {
124 span_kind::CONSUMER
125 } else if t.eq_ignore_ascii_case("internal") {
126 span_kind::INTERNAL
127 } else {
128 span_kind::UNSPECIFIED
129 }
130}
131
132fn dd_type_to_otlp_kind(t: &str) -> i32 {
134 if t.eq_ignore_ascii_case("server")
136 || t.eq_ignore_ascii_case("web")
137 || t.eq_ignore_ascii_case("http")
138 {
139 span_kind::SERVER
140 } else if t.eq_ignore_ascii_case("client") {
141 span_kind::CLIENT
142 } else if t.eq_ignore_ascii_case("producer") {
143 span_kind::PRODUCER
144 } else if t.eq_ignore_ascii_case("consumer") {
145 span_kind::CONSUMER
146 } else {
147 span_kind::INTERNAL
148 }
149}
150
151fn proto_kv(key: String, value: ProtoValue) -> ProtoKeyValue {
156 ProtoKeyValue {
157 key,
158 value: Some(ProtoAnyValue { value: Some(value) }),
159 key_ref: 0,
160 }
161}
162
163fn collect_span_attributes<T: TraceData>(
168 span: &Span<T>,
169 resource_service: &str,
170 otel_trace_semantics_enabled: bool,
171) -> (Vec<ProtoKeyValue>, usize) {
172 let capacity = (4 + span.meta.len() + span.metrics.len() + span.meta_struct.len())
175 .min(MAX_ATTRIBUTES_PER_SPAN);
176 let mut attrs: Vec<ProtoKeyValue> = Vec::with_capacity(capacity);
177 let span_service = span.service.borrow();
181 let has_per_span_service = !span_service.is_empty() && span_service != resource_service;
182 if has_per_span_service && !otel_trace_semantics_enabled {
183 attrs.push(proto_kv(
184 "service.name".to_string(),
185 ProtoValue::StringValue(span_service.to_string()),
186 ));
187 }
188 let operation_name = span.name.borrow();
189 let has_operation_name = !operation_name.is_empty();
190 if has_operation_name && !otel_trace_semantics_enabled {
191 attrs.push(proto_kv(
192 "operation.name".to_string(),
193 ProtoValue::StringValue(operation_name.to_string()),
194 ));
195 }
196 let span_type = span.r#type.borrow();
197 let has_span_type = !span_type.is_empty();
198 if has_span_type && !otel_trace_semantics_enabled {
199 attrs.push(proto_kv(
200 "span.type".to_string(),
201 ProtoValue::StringValue(span_type.to_string()),
202 ));
203 }
204 let resource_name = span.resource.borrow();
205 let has_resource_name = !resource_name.is_empty();
206 if has_resource_name && !otel_trace_semantics_enabled {
207 attrs.push(proto_kv(
208 "resource.name".to_string(),
209 ProtoValue::StringValue(resource_name.to_string()),
210 ));
211 }
212 for (k, v) in span.meta.iter() {
213 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
214 break;
215 }
216 let key = k.borrow();
217 if otel_trace_semantics_enabled
218 && (key == "error.msg" || key == "error.message" || key == "span.kind")
219 {
220 continue;
221 }
222 attrs.push(proto_kv(
223 key.to_string(),
224 ProtoValue::StringValue(v.borrow().to_string()),
225 ));
226 }
227 for (k, v) in span.metrics.iter() {
228 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
229 break;
230 }
231 let value = if v.fract() == 0.0 && (*v >= i64::MIN as f64 && *v <= i64::MAX as f64) {
232 ProtoValue::IntValue(*v as i64)
233 } else {
234 ProtoValue::DoubleValue(*v)
235 };
236 attrs.push(proto_kv(k.borrow().to_string(), value));
237 }
238 for (k, v) in span.meta_struct.iter() {
239 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
240 break;
241 }
242 attrs.push(proto_kv(
243 k.borrow().to_string(),
244 ProtoValue::BytesValue(v.borrow().to_vec()),
245 ));
246 }
247 let excluded_compat_tags = if otel_trace_semantics_enabled {
251 span.meta.contains_key("error.msg") as usize
252 + span.meta.contains_key("error.message") as usize
253 + span.meta.contains_key("span.kind") as usize
254 } else {
255 0
256 };
257 let promoted = if otel_trace_semantics_enabled {
258 0
259 } else {
260 (has_per_span_service as usize)
261 + (has_operation_name as usize)
262 + (has_span_type as usize)
263 + (has_resource_name as usize)
264 };
265 let total = promoted
266 + (span.meta.len() - excluded_compat_tags)
267 + span.metrics.len()
268 + span.meta_struct.len();
269 let dropped = total.saturating_sub(attrs.len());
270 (attrs, dropped)
271}
272
273fn event_attr_value<T: TraceData>(av: &crate::span::v04::AttributeArrayValue<T>) -> ProtoValue {
275 use crate::span::v04::AttributeArrayValue;
276 match av {
277 AttributeArrayValue::String(s) => ProtoValue::StringValue(s.borrow().to_string()),
278 AttributeArrayValue::Boolean(b) => ProtoValue::BoolValue(*b),
279 AttributeArrayValue::Integer(i) => ProtoValue::IntValue(*i),
280 AttributeArrayValue::Double(d) => ProtoValue::DoubleValue(*d),
281 }
282}
283
284fn collect_event_attributes<T: TraceData>(ev: &SpanEvent<T>) -> Vec<ProtoKeyValue> {
285 use crate::span::v04::AttributeAnyValue;
286 ev.attributes
287 .iter()
288 .map(|(k, v)| {
289 let value = match v {
290 AttributeAnyValue::SingleValue(av) => event_attr_value(av),
291 AttributeAnyValue::Array(items) => ProtoValue::ArrayValue(ProtoArrayValue {
292 values: items
293 .iter()
294 .map(|it| ProtoAnyValue {
295 value: Some(event_attr_value(it)),
296 })
297 .collect(),
298 }),
299 };
300 proto_kv(k.borrow().to_string(), value)
301 })
302 .collect()
303}
304
305pub fn map_traces_to_otlp<T: TraceData>(
321 trace_chunks: Vec<Vec<Span<T>>>,
322 resource_info: &OtlpResourceInfo,
323 otel_trace_semantics_enabled: bool,
324) -> ProtoReq {
325 let resource = build_resource(resource_info);
326 let total_spans: usize = trace_chunks.iter().map(|chunk| chunk.len()).sum();
328 let mut all_spans: Vec<ProtoSpan> = Vec::with_capacity(total_spans);
329 for chunk in &trace_chunks {
330 let high = chunk_trace_id_high(chunk);
334 let chunk_flags = otel_trace_semantics_enabled.then(|| chunk_sampling_flags(chunk));
337 for span in chunk {
338 let flags = chunk_flags.unwrap_or_else(|| span_sampling_flags(span));
339 all_spans.push(map_span(
340 span,
341 &resource_info.service,
342 high,
343 flags,
344 otel_trace_semantics_enabled,
345 ));
346 }
347 }
348 ProtoReq {
349 resource_spans: vec![ProtoResourceSpans {
350 resource: Some(resource),
351 scope_spans: vec![ProtoScopeSpans {
352 scope: Some(ProtoScope {
353 name: resource_info.instrumentation_scope_name.clone(),
354 version: resource_info.instrumentation_scope_version.clone(),
355 attributes: Vec::new(),
356 dropped_attributes_count: 0,
357 }),
358 spans: all_spans,
359 schema_url: String::new(),
360 }],
361 schema_url: String::new(),
362 }],
363 }
364}
365
366fn build_resource(resource_info: &OtlpResourceInfo) -> ProtoResource {
367 fn push_str_attr(attrs: &mut Vec<ProtoKeyValue>, k: &str, v: &str) {
368 if !v.is_empty() {
369 attrs.push(proto_kv(
370 k.to_string(),
371 ProtoValue::StringValue(v.to_string()),
372 ));
373 }
374 }
375 let mut attributes = Vec::new();
376 push_str_attr(&mut attributes, "service.name", &resource_info.service);
377 push_str_attr(
378 &mut attributes,
379 "deployment.environment.name",
380 &resource_info.env,
381 );
382 push_str_attr(
383 &mut attributes,
384 "service.version",
385 &resource_info.app_version,
386 );
387 attributes.push(proto_kv(
388 "telemetry.sdk.name".to_string(),
389 ProtoValue::StringValue("datadog".to_string()),
390 ));
391 push_str_attr(
392 &mut attributes,
393 "telemetry.sdk.language",
394 &resource_info.language,
395 );
396 push_str_attr(
397 &mut attributes,
398 "telemetry.sdk.version",
399 &resource_info.tracer_version,
400 );
401 push_str_attr(&mut attributes, "runtime-id", &resource_info.runtime_id);
402 if resource_info.client_computed_stats {
405 push_str_attr(&mut attributes, "_dd.stats_computed", "true");
406 }
407 ProtoResource {
409 attributes,
410 dropped_attributes_count: 0,
411 entity_refs: Vec::new(),
412 }
413}
414
415fn map_span<T: TraceData>(
416 span: &Span<T>,
417 resource_service: &str,
418 chunk_trace_id_high: u64,
419 flags: u32,
420 otel_trace_semantics_enabled: bool,
421) -> ProtoSpan {
422 let trace_id_128 = ((chunk_trace_id_high as u128) << 64) | (span.trace_id as u64 as u128);
426 let parent_span_id = if span.parent_id != 0 {
427 span.parent_id.to_be_bytes().to_vec()
428 } else {
429 Vec::new()
430 };
431 let (attributes, dropped_attributes_count) =
432 collect_span_attributes(span, resource_service, otel_trace_semantics_enabled);
433 let (code, message) = span_status(span);
434 let trace_state = span
435 .meta
436 .get("tracestate")
437 .map(|v| v.borrow().to_string())
438 .filter(|s| !s.is_empty())
439 .unwrap_or_default();
440 let links = span.span_links.iter().map(map_span_link).collect();
441 let (events, dropped_events_count) = map_span_events(&span.span_events);
442 ProtoSpan {
443 trace_id: trace_id_128.to_be_bytes().to_vec(),
444 span_id: span.span_id.to_be_bytes().to_vec(),
445 trace_state,
446 parent_span_id,
447 flags,
448 name: span.resource.borrow().to_string(),
449 kind: span_kind(span),
450 start_time_unix_nano: span.start.max(0) as u64,
452 end_time_unix_nano: (span.start + span.duration).max(0) as u64,
453 attributes,
454 dropped_attributes_count: dropped_attributes_count as u32,
455 events,
456 dropped_events_count: dropped_events_count as u32,
457 links,
458 dropped_links_count: 0,
460 status: Some(ProtoStatus {
461 message: message.unwrap_or_default(),
462 code,
463 }),
464 }
465}
466
467fn map_span_link<T: TraceData>(link: &SpanLink<T>) -> ProtoLink {
468 let trace_id_128 = ((link.trace_id_high as u128) << 64) | (link.trace_id as u128);
469 ProtoLink {
470 trace_id: trace_id_128.to_be_bytes().to_vec(),
471 span_id: link.span_id.to_be_bytes().to_vec(),
472 trace_state: {
473 let ts = link.tracestate.borrow();
474 if ts.is_empty() {
475 String::new()
476 } else {
477 ts.to_string()
478 }
479 },
480 attributes: link
481 .attributes
482 .iter()
483 .map(|(k, v)| {
484 proto_kv(
485 k.borrow().to_string(),
486 ProtoValue::StringValue(v.borrow().to_string()),
487 )
488 })
489 .collect(),
490 dropped_attributes_count: 0,
491 flags: link.flags & !SPAN_LINK_FLAGS_SET_SENTINEL,
495 }
496}
497
498fn map_span_events<T: TraceData>(events: &[SpanEvent<T>]) -> (Vec<ProtoEvent>, usize) {
499 const MAX_EVENTS_PER_SPAN: usize = 128;
500 let mut out = Vec::with_capacity(events.len().min(MAX_EVENTS_PER_SPAN));
501 for ev in events.iter().take(MAX_EVENTS_PER_SPAN) {
502 out.push(ProtoEvent {
503 time_unix_nano: ev.time_unix_nano,
504 name: ev.name.borrow().to_string(),
505 attributes: collect_event_attributes(ev),
506 dropped_attributes_count: 0,
507 });
508 }
509 let dropped = events.len().saturating_sub(out.len());
510 (out, dropped)
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use crate::otlp_encoder::OtlpResourceInfo;
517 use crate::span::BytesData;
518
519 #[test]
520 fn maps_native_span_to_prost_ir() {
521 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
522 let resource_info = OtlpResourceInfo::default();
523 let mut span: Span<BytesData> = Span {
524 trace_id: 0xD269B633813FC60C_u128,
525 span_id: 0xEEE19B7EC3C1B174,
526 parent_id: 0xEEE19B7EC3C1B173,
527 name: libdd_tinybytes::BytesString::from_static("op"),
528 resource: libdd_tinybytes::BytesString::from_static("res"),
529 r#type: libdd_tinybytes::BytesString::from_static("web"),
530 start: 1544712660000000000,
531 duration: 1000000000,
532 error: 1,
533 ..Default::default()
534 };
535 span.meta.insert(
536 "error.msg".into(),
537 libdd_tinybytes::BytesString::from_static("boom"),
538 );
539 span.metrics
540 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
541 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
542 let s = &req.resource_spans[0].scope_spans[0].spans[0];
543 assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
544 assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
545 assert_eq!(
546 s.parent_span_id,
547 0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
548 );
549 assert_eq!(s.name, "res");
550 assert_eq!(s.kind, 2); assert_eq!(s.start_time_unix_nano, 1544712660000000000);
552 assert_eq!(s.end_time_unix_nano, 1544712661000000000);
553 let st = s.status.as_ref().unwrap();
554 assert_eq!(st.code, 2);
555 assert_eq!(st.message, "boom");
556 let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
557 assert!(matches!(
558 count.value.as_ref().unwrap().value,
559 Some(PV::IntValue(42))
560 ));
561 }
562
563 #[test]
564 fn instrumentation_scope_from_resource_info() {
565 let resource_info = OtlpResourceInfo {
566 instrumentation_scope_name: "dd-trace-js".to_string(),
567 instrumentation_scope_version: "7.0.0-pre".to_string(),
568 ..Default::default()
569 };
570 let span: Span<BytesData> = Span {
571 trace_id: 1,
572 span_id: 2,
573 name: libdd_tinybytes::BytesString::from_static("s"),
574 start: 0,
575 duration: 1,
576 ..Default::default()
577 };
578
579 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
580 let scope = req.resource_spans[0].scope_spans[0].scope.as_ref().unwrap();
581 assert_eq!(scope.name, "dd-trace-js");
582 assert_eq!(scope.version, "7.0.0-pre");
583 }
584
585 #[test]
586 fn proto_span_uses_raw_id_bytes_and_native_timestamps() {
587 let resource_info = OtlpResourceInfo {
588 service: "svc".to_string(),
589 ..Default::default()
590 };
591 let span: Span<BytesData> = Span {
592 trace_id: 0x5b8efff798038103_d269b633813fc60c_u128,
593 span_id: 0xEEE19B7EC3C1B174,
594 parent_id: 0xEEE19B7EC3C1B173,
595 name: libdd_tinybytes::BytesString::from_static("op"),
596 resource: libdd_tinybytes::BytesString::from_static("res"),
597 r#type: libdd_tinybytes::BytesString::from_static("web"),
598 start: 1544712660000000000,
599 duration: 1000000000,
600 ..Default::default()
601 };
602 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
603 let s = &req.resource_spans[0].scope_spans[0].spans[0];
604 assert_eq!(
605 s.trace_id,
606 0x5b8efff798038103_d269b633813fc60c_u128
607 .to_be_bytes()
608 .to_vec()
609 );
610 assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
611 assert_eq!(
612 s.parent_span_id,
613 0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
614 );
615 assert_eq!(s.start_time_unix_nano, 1544712660000000000);
616 assert_eq!(s.end_time_unix_nano, 1544712661000000000);
617 assert_eq!(s.name, "res");
618 assert_eq!(s.kind, span_kind::SERVER);
619 }
620
621 #[test]
622 fn negative_start_clamps_to_zero() {
623 let resource_info = OtlpResourceInfo {
627 service: "svc".to_string(),
628 ..Default::default()
629 };
630 let span: Span<BytesData> = Span {
631 trace_id: 1,
632 span_id: 1,
633 start: -1,
634 duration: 0,
635 ..Default::default()
636 };
637 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
638 let s = &req.resource_spans[0].scope_spans[0].spans[0];
639 assert_eq!(
640 s.start_time_unix_nano, 0,
641 "negative start must clamp to 0, not wrap"
642 );
643 assert_eq!(
644 s.end_time_unix_nano, 0,
645 "negative start+duration must clamp to 0, not wrap"
646 );
647 }
648
649 #[test]
650 fn status_error_message_from_meta() {
651 let resource_info = OtlpResourceInfo::default();
652 let mut span: Span<BytesData> = Span {
653 trace_id: 1,
654 span_id: 2,
655 name: libdd_tinybytes::BytesString::from_static("err_span"),
656 start: 0,
657 duration: 1,
658 error: 1,
659 ..Default::default()
660 };
661 span.meta.insert(
662 libdd_tinybytes::BytesString::from_static("error.msg"),
663 libdd_tinybytes::BytesString::from_static("something broke"),
664 );
665 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
666 let s = &req.resource_spans[0].scope_spans[0].spans[0];
667 let status = s.status.as_ref().unwrap();
668 assert_eq!(status.code, status_code::ERROR);
669 assert_eq!(status.message, "something broke");
670 }
671
672 #[test]
673 fn metrics_as_int_or_double() {
674 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
675 let resource_info = OtlpResourceInfo::default();
676 let mut span: Span<BytesData> = Span {
677 trace_id: 1,
678 span_id: 2,
679 name: libdd_tinybytes::BytesString::from_static("m"),
680 start: 0,
681 duration: 1,
682 ..Default::default()
683 };
684 span.metrics
685 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
686 span.metrics.insert(
687 libdd_tinybytes::BytesString::from_static("rate"),
688 std::f64::consts::PI,
689 );
690 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
691 let s = &req.resource_spans[0].scope_spans[0].spans[0];
692 let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
693 assert!(matches!(
694 count.value.as_ref().unwrap().value,
695 Some(PV::IntValue(42))
696 ));
697 let rate = s.attributes.iter().find(|a| a.key == "rate").unwrap();
698 match rate.value.as_ref().unwrap().value {
699 Some(PV::DoubleValue(d)) => assert!((d - std::f64::consts::PI).abs() < 1e-9),
700 ref other => panic!("expected double, got {other:?}"),
701 }
702 }
703
704 #[test]
705 fn trace_id_128_from_dd_p_tid() {
706 let resource_info = OtlpResourceInfo::default();
709 let mut span: Span<BytesData> = Span {
710 trace_id: 0xD269B633813FC60C_u128, span_id: 1,
712 name: libdd_tinybytes::BytesString::from_static("s"),
713 start: 0,
714 duration: 1,
715 ..Default::default()
716 };
717 span.meta.insert(
718 "_dd.p.tid".into(),
719 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
720 );
721 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
722 let s = &req.resource_spans[0].scope_spans[0].spans[0];
723 assert_eq!(
724 s.trace_id,
725 0x5b8efff798038103_d269b633813fc60c_u128
726 .to_be_bytes()
727 .to_vec()
728 );
729 }
730
731 #[test]
732 fn trace_id_128_from_native_span_field() {
733 let resource_info = OtlpResourceInfo::default();
737 let full: u128 = 0x5b8efff798038103_d269b633813fc60c_u128;
738 let root: Span<BytesData> = Span {
739 trace_id: full,
740 span_id: 1,
741 name: libdd_tinybytes::BytesString::from_static("root"),
742 start: 0,
743 duration: 1,
744 ..Default::default()
745 };
746 let child: Span<BytesData> = Span {
748 trace_id: 0xD269B633813FC60C_u128,
749 span_id: 2,
750 parent_id: 1,
751 name: libdd_tinybytes::BytesString::from_static("child"),
752 start: 0,
753 duration: 1,
754 ..Default::default()
755 };
756 let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, false);
757 let spans = &req.resource_spans[0].scope_spans[0].spans;
758 let expected = full.to_be_bytes().to_vec();
759 assert_eq!(spans[0].trace_id, expected);
760 assert_eq!(spans[1].trace_id, expected);
761 }
762
763 #[test]
764 fn trace_id_128_without_dd_p_tid_defaults_high_to_zero() {
765 let resource_info = OtlpResourceInfo::default();
768 let span: Span<BytesData> = Span {
769 trace_id: 0xD269B633813FC60C_u128,
770 span_id: 1,
771 name: libdd_tinybytes::BytesString::from_static("s"),
772 start: 0,
773 duration: 1,
774 ..Default::default()
775 };
776 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
777 let s = &req.resource_spans[0].scope_spans[0].spans[0];
778 assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
779 }
780
781 #[test]
782 fn trace_id_128_propagated_to_chunk_children() {
783 let resource_info = OtlpResourceInfo::default();
787 let low: u128 = 0xD269B633813FC60C_u128;
788 let mut root: Span<BytesData> = Span {
789 trace_id: low,
790 span_id: 1,
791 name: libdd_tinybytes::BytesString::from_static("root"),
792 start: 0,
793 duration: 1,
794 ..Default::default()
795 };
796 root.meta.insert(
797 "_dd.p.tid".into(),
798 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
799 );
800 let child_a: Span<BytesData> = Span {
801 trace_id: low,
802 span_id: 2,
803 parent_id: 1,
804 name: libdd_tinybytes::BytesString::from_static("child_a"),
805 start: 0,
806 duration: 1,
807 ..Default::default()
808 };
809 let child_b: Span<BytesData> = Span {
810 trace_id: low,
811 span_id: 3,
812 parent_id: 1,
813 name: libdd_tinybytes::BytesString::from_static("child_b"),
814 start: 0,
815 duration: 1,
816 ..Default::default()
817 };
818 let req = map_traces_to_otlp(vec![vec![root, child_a, child_b]], &resource_info, false);
819 let spans = &req.resource_spans[0].scope_spans[0].spans;
820 assert_eq!(spans.len(), 3);
821 let expected = 0x5b8efff798038103_d269b633813fc60c_u128
822 .to_be_bytes()
823 .to_vec();
824 for s in spans {
825 assert_eq!(s.trace_id, expected);
826 }
827 }
828
829 #[test]
830 fn trace_id_128_isolation_across_chunks() {
831 let resource_info = OtlpResourceInfo::default();
834 let low_a: u128 = 0x1111111111111111_u128;
835 let low_b: u128 = 0x2222222222222222_u128;
836 let mut root_a: Span<BytesData> = Span {
837 trace_id: low_a,
838 span_id: 1,
839 name: libdd_tinybytes::BytesString::from_static("root_a"),
840 start: 0,
841 duration: 1,
842 ..Default::default()
843 };
844 root_a.meta.insert(
845 "_dd.p.tid".into(),
846 libdd_tinybytes::BytesString::from_static("aaaaaaaaaaaaaaaa"),
847 );
848 let child_a: Span<BytesData> = Span {
849 trace_id: low_a,
850 span_id: 2,
851 parent_id: 1,
852 name: libdd_tinybytes::BytesString::from_static("child_a"),
853 start: 0,
854 duration: 1,
855 ..Default::default()
856 };
857 let mut root_b: Span<BytesData> = Span {
858 trace_id: low_b,
859 span_id: 3,
860 name: libdd_tinybytes::BytesString::from_static("root_b"),
861 start: 0,
862 duration: 1,
863 ..Default::default()
864 };
865 root_b.meta.insert(
866 "_dd.p.tid".into(),
867 libdd_tinybytes::BytesString::from_static("bbbbbbbbbbbbbbbb"),
868 );
869 let child_b: Span<BytesData> = Span {
870 trace_id: low_b,
871 span_id: 4,
872 parent_id: 3,
873 name: libdd_tinybytes::BytesString::from_static("child_b"),
874 start: 0,
875 duration: 1,
876 ..Default::default()
877 };
878 let req = map_traces_to_otlp(
879 vec![vec![root_a, child_a], vec![root_b, child_b]],
880 &resource_info,
881 false,
882 );
883 let spans = &req.resource_spans[0].scope_spans[0].spans;
884 assert_eq!(spans.len(), 4);
885 let expect_a = 0xaaaaaaaaaaaaaaaa_1111111111111111_u128
886 .to_be_bytes()
887 .to_vec();
888 let expect_b = 0xbbbbbbbbbbbbbbbb_2222222222222222_u128
889 .to_be_bytes()
890 .to_vec();
891 assert_eq!(spans[0].trace_id, expect_a);
892 assert_eq!(spans[1].trace_id, expect_a);
893 assert_eq!(spans[2].trace_id, expect_b);
894 assert_eq!(spans[3].trace_id, expect_b);
895 }
896
897 #[test]
898 fn chunk_with_malformed_dd_p_tid_on_root_falls_back() {
899 let resource_info = OtlpResourceInfo::default();
903 let low: u128 = 0xD269B633813FC60C_u128;
904 let mut root: Span<BytesData> = Span {
905 trace_id: low,
906 span_id: 1,
907 name: libdd_tinybytes::BytesString::from_static("root"),
908 start: 0,
909 duration: 1,
910 ..Default::default()
911 };
912 root.meta.insert(
913 "_dd.p.tid".into(),
914 libdd_tinybytes::BytesString::from_static("not-hex"),
915 );
916 let child_no_tag: Span<BytesData> = Span {
917 trace_id: low,
918 span_id: 2,
919 parent_id: 1,
920 name: libdd_tinybytes::BytesString::from_static("child_no_tag"),
921 start: 0,
922 duration: 1,
923 ..Default::default()
924 };
925 let mut child_valid: Span<BytesData> = Span {
926 trace_id: low,
927 span_id: 3,
928 parent_id: 1,
929 name: libdd_tinybytes::BytesString::from_static("child_valid"),
930 start: 0,
931 duration: 1,
932 ..Default::default()
933 };
934 child_valid.meta.insert(
935 "_dd.p.tid".into(),
936 libdd_tinybytes::BytesString::from_static("dddddddddddddddd"),
937 );
938 let req = map_traces_to_otlp(
939 vec![vec![root, child_no_tag, child_valid]],
940 &resource_info,
941 false,
942 );
943 let spans = &req.resource_spans[0].scope_spans[0].spans;
944 let expected = 0xdddddddddddddddd_d269b633813fc60c_u128
947 .to_be_bytes()
948 .to_vec();
949 assert_eq!(spans[0].trace_id, expected);
950 assert_eq!(spans[1].trace_id, expected);
951 assert_eq!(spans[2].trace_id, expected);
952 }
953
954 #[test]
955 fn test_stats_computed_resource_attr_set_when_enabled() {
956 let resource_info = OtlpResourceInfo {
957 client_computed_stats: true,
958 ..Default::default()
959 };
960 let span: Span<BytesData> = Span {
961 trace_id: 1,
962 span_id: 2,
963 name: libdd_tinybytes::BytesString::from_static("s"),
964 start: 0,
965 duration: 1,
966 ..Default::default()
967 };
968 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
969 let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
970 let kv = resource_attrs
971 .iter()
972 .find(|a| a.key == "_dd.stats_computed")
973 .expect("_dd.stats_computed must be present when client_computed_stats=true");
974 let val = match kv.value.as_ref().and_then(|v| v.value.as_ref()) {
975 Some(ProtoValue::StringValue(s)) => s.as_str(),
976 other => panic!("expected stringValue, got {other:?}"),
977 };
978 assert_eq!(val, "true");
979 }
980
981 #[test]
982 fn test_stats_computed_resource_attr_absent_when_disabled() {
983 let resource_info = OtlpResourceInfo {
984 client_computed_stats: false,
985 ..Default::default()
986 };
987 let span: Span<BytesData> = Span {
988 trace_id: 1,
989 span_id: 2,
990 name: libdd_tinybytes::BytesString::from_static("s"),
991 start: 0,
992 duration: 1,
993 ..Default::default()
994 };
995 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
996 let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
997 assert!(
998 !resource_attrs.iter().any(|a| a.key == "_dd.stats_computed"),
999 "_dd.stats_computed must not be emitted when client_computed_stats=false"
1000 );
1001 }
1002
1003 #[test]
1004 fn span_link_flags_are_carried() {
1005 let mut span: Span<BytesData> = Span {
1008 trace_id: 1,
1009 span_id: 2,
1010 name: libdd_tinybytes::BytesString::from_static("s"),
1011 start: 0,
1012 duration: 1,
1013 ..Default::default()
1014 };
1015 span.span_links.push(SpanLink {
1016 trace_id: 0x11,
1017 span_id: 0x22,
1018 flags: 1,
1019 ..Default::default()
1020 });
1021 let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
1022 let link = &req.resource_spans[0].scope_spans[0].spans[0].links[0];
1023 assert_eq!(
1024 link.flags, 1,
1025 "OTLP Link.flags must carry the span link's flags"
1026 );
1027 }
1028
1029 #[test]
1030 fn span_link_flags_sentinel_bit_masked() {
1031 fn mapped_flags(flags: u32) -> u32 {
1035 let mut span: Span<BytesData> = Span {
1036 trace_id: 1,
1037 span_id: 2,
1038 name: libdd_tinybytes::BytesString::from_static("s"),
1039 start: 0,
1040 duration: 1,
1041 ..Default::default()
1042 };
1043 span.span_links.push(SpanLink {
1044 trace_id: 0x11,
1045 span_id: 0x22,
1046 flags,
1047 ..Default::default()
1048 });
1049 let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
1050 req.resource_spans[0].scope_spans[0].spans[0].links[0].flags
1051 }
1052
1053 assert_eq!(
1054 mapped_flags(0x8000_0001),
1055 1,
1056 "OTLP Link.flags must not carry the internal sentinel bit"
1057 );
1058 assert_eq!(
1059 mapped_flags(0x8000_0000),
1060 0,
1061 "an explicit drop decision must still map to flags: 0"
1062 );
1063 }
1064
1065 #[test]
1066 fn test_otel_trace_semantics_enabled() {
1067 let resource_info = OtlpResourceInfo {
1071 service: "resource-svc".to_string(),
1072 ..Default::default()
1073 };
1074 let mut span: Span<BytesData> = Span {
1075 trace_id: 1,
1076 span_id: 2,
1077 name: libdd_tinybytes::BytesString::from_static("http.request"),
1078 service: libdd_tinybytes::BytesString::from_static("span-svc"),
1079 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
1080 r#type: libdd_tinybytes::BytesString::from_static("web"),
1081 start: 0,
1082 duration: 1,
1083 ..Default::default()
1084 };
1085 span.meta.insert(
1086 libdd_tinybytes::BytesString::from_static("span.kind"),
1087 libdd_tinybytes::BytesString::from_static("client"),
1088 );
1089 span.meta.insert(
1090 libdd_tinybytes::BytesString::from_static("http.method"),
1091 libdd_tinybytes::BytesString::from_static("GET"),
1092 );
1093 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
1094 let attrs = &req.resource_spans[0].scope_spans[0].spans[0].attributes;
1095 let keys: Vec<&str> = attrs.iter().map(|kv| kv.key.as_str()).collect();
1096 for omitted in [
1097 "service.name",
1098 "operation.name",
1099 "resource.name",
1100 "span.type",
1101 "span.kind",
1102 ] {
1103 assert!(
1104 !keys.contains(&omitted),
1105 "OTel-semantics must omit {omitted}"
1106 );
1107 }
1108 assert!(
1109 keys.contains(&"http.method"),
1110 "OTel-standard meta tags must remain"
1111 );
1112 }
1113
1114 #[test]
1115 fn error_message_promoted_to_status_under_otel_semantics() {
1116 let resource_info = OtlpResourceInfo::default();
1120 let mut span: Span<BytesData> = Span {
1121 trace_id: 1,
1122 span_id: 2,
1123 name: libdd_tinybytes::BytesString::from_static("op"),
1124 start: 0,
1125 duration: 1,
1126 error: 1,
1127 ..Default::default()
1128 };
1129 span.meta.insert(
1130 libdd_tinybytes::BytesString::from_static("error.message"),
1131 libdd_tinybytes::BytesString::from_static("boom"),
1132 );
1133 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
1134 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
1135 let status = otlp_span
1136 .status
1137 .as_ref()
1138 .expect("status present on error span");
1139 assert_eq!(status.code, status_code::ERROR);
1140 assert_eq!(
1141 status.message, "boom",
1142 "error.message must be promoted to the OTLP Status message"
1143 );
1144 assert!(
1145 !otlp_span
1146 .attributes
1147 .iter()
1148 .any(|kv| kv.key == "error.message"),
1149 "error.message compat attr must be omitted under OTel-semantics"
1150 );
1151 }
1152
1153 #[test]
1154 fn empty_chunk_does_not_panic() {
1155 let resource_info = OtlpResourceInfo::default();
1157 let empty: Vec<Vec<Span<BytesData>>> = vec![vec![]];
1158 let req = map_traces_to_otlp(empty, &resource_info, false);
1159 let spans = &req.resource_spans[0].scope_spans[0].spans;
1160 assert!(spans.is_empty());
1161 }
1162
1163 #[test]
1164 fn tracestate_from_meta() {
1165 let resource_info = OtlpResourceInfo::default();
1166 let mut span: Span<BytesData> = Span {
1167 trace_id: 1,
1168 span_id: 2,
1169 name: libdd_tinybytes::BytesString::from_static("s"),
1170 start: 0,
1171 duration: 1,
1172 ..Default::default()
1173 };
1174 span.meta.insert(
1175 "tracestate".into(),
1176 libdd_tinybytes::BytesString::from_static("vendor1=abc,rojo=00f067"),
1177 );
1178 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1179 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1180 assert_eq!(s.trace_state, "vendor1=abc,rojo=00f067");
1181 }
1182
1183 #[test]
1184 fn meta_struct_as_bytes_value() {
1185 use libdd_tinybytes::Bytes;
1186 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1187 let resource_info = OtlpResourceInfo::default();
1188 let mut span: Span<BytesData> = Span {
1189 trace_id: 1,
1190 span_id: 2,
1191 name: libdd_tinybytes::BytesString::from_static("s"),
1192 start: 0,
1193 duration: 1,
1194 ..Default::default()
1195 };
1196 span.meta_struct
1197 .insert("my_key".into(), Bytes::from(vec![1u8, 2, 3]));
1198 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1199 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1200 let kv = s
1201 .attributes
1202 .iter()
1203 .find(|a| a.key == "my_key")
1204 .expect("my_key attribute not found");
1205 match kv.value.as_ref().unwrap().value {
1206 Some(PV::BytesValue(ref b)) => assert_eq!(b, &vec![1u8, 2, 3]),
1207 ref other => panic!("expected bytes, got {other:?}"),
1208 }
1209 }
1210
1211 #[test]
1212 fn operation_name_attribute() {
1213 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1214 let resource_info = OtlpResourceInfo::default();
1215 let span: Span<BytesData> = Span {
1216 trace_id: 1,
1217 span_id: 2,
1218 name: libdd_tinybytes::BytesString::from_static("my.operation"),
1219 start: 0,
1220 duration: 1,
1221 ..Default::default()
1222 };
1223 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1224 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1225 let kv = s
1226 .attributes
1227 .iter()
1228 .find(|a| a.key == "operation.name")
1229 .expect("operation.name attribute not found");
1230 match kv.value.as_ref().unwrap().value {
1231 Some(PV::StringValue(ref v)) => assert_eq!(v, "my.operation"),
1232 ref other => panic!("expected string, got {other:?}"),
1233 }
1234 }
1235
1236 #[test]
1237 fn span_type_attribute() {
1238 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1239 let resource_info = OtlpResourceInfo::default();
1240 let span: Span<BytesData> = Span {
1241 trace_id: 1,
1242 span_id: 2,
1243 name: libdd_tinybytes::BytesString::from_static("s"),
1244 r#type: libdd_tinybytes::BytesString::from_static("grpc"),
1245 start: 0,
1246 duration: 1,
1247 ..Default::default()
1248 };
1249 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1250 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1251 let kv = s
1252 .attributes
1253 .iter()
1254 .find(|a| a.key == "span.type")
1255 .expect("span.type attribute not found");
1256 match kv.value.as_ref().unwrap().value {
1257 Some(PV::StringValue(ref v)) => assert_eq!(v, "grpc"),
1258 ref other => panic!("expected string, got {other:?}"),
1259 }
1260 }
1261
1262 #[test]
1263 fn resource_name_attribute_and_span_name() {
1264 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1265 let resource_info = OtlpResourceInfo::default();
1266 let span: Span<BytesData> = Span {
1267 trace_id: 1,
1268 span_id: 2,
1269 name: libdd_tinybytes::BytesString::from_static("s"),
1270 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
1271 start: 0,
1272 duration: 1,
1273 ..Default::default()
1274 };
1275 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1276 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1277 assert_eq!(s.name, "GET /api/users");
1279 let kv = s
1281 .attributes
1282 .iter()
1283 .find(|a| a.key == "resource.name")
1284 .expect("resource.name attribute not found");
1285 match kv.value.as_ref().unwrap().value {
1286 Some(PV::StringValue(ref v)) => assert_eq!(v, "GET /api/users"),
1287 ref other => panic!("expected string, got {other:?}"),
1288 }
1289 }
1290
1291 #[test]
1292 fn empty_resource_name_not_emitted() {
1293 let resource_info = OtlpResourceInfo::default();
1295 let span: Span<BytesData> = Span {
1296 trace_id: 1,
1297 span_id: 2,
1298 name: libdd_tinybytes::BytesString::from_static("s"),
1299 start: 0,
1301 duration: 1,
1302 ..Default::default()
1303 };
1304 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1305 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1306 assert!(
1307 !s.attributes.iter().any(|a| a.key == "resource.name"),
1308 "resource.name should not be emitted when resource is empty"
1309 );
1310 }
1311
1312 #[test]
1313 fn per_span_service_name_attribute() {
1314 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1315 let resource_info = OtlpResourceInfo {
1318 service: "resource-svc".to_string(),
1319 ..Default::default()
1320 };
1321 let span: Span<BytesData> = Span {
1322 trace_id: 1,
1323 span_id: 2,
1324 name: libdd_tinybytes::BytesString::from_static("s"),
1325 service: libdd_tinybytes::BytesString::from_static("span-svc"),
1326 start: 0,
1327 duration: 1,
1328 ..Default::default()
1329 };
1330 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1331 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1332 let kv = s
1333 .attributes
1334 .iter()
1335 .find(|a| a.key == "service.name")
1336 .expect("service.name attribute not found");
1337 match kv.value.as_ref().unwrap().value {
1338 Some(PV::StringValue(ref v)) => assert_eq!(v, "span-svc"),
1339 ref other => panic!("expected string, got {other:?}"),
1340 }
1341 }
1342
1343 #[test]
1344 fn unsampled_span_flags_zero() {
1345 let resource_info = OtlpResourceInfo::default();
1347 let mut span: Span<BytesData> = Span {
1348 trace_id: 1,
1349 span_id: 2,
1350 name: libdd_tinybytes::BytesString::from_static("s"),
1351 start: 0,
1352 duration: 1,
1353 ..Default::default()
1354 };
1355 span.metrics.insert("_sampling_priority_v1".into(), 0.0);
1356 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1357 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1358 assert_eq!(s.flags, 0);
1359 }
1360
1361 #[test]
1362 fn trace_without_sampling_priority_leaves_sampled_flag_unset() {
1363 let resource_info = OtlpResourceInfo::default();
1364 let root: Span<BytesData> = Span {
1365 trace_id: 1,
1366 span_id: 2,
1367 name: libdd_tinybytes::BytesString::from_static("root"),
1368 start: 0,
1369 duration: 1,
1370 ..Default::default()
1371 };
1372 let child: Span<BytesData> = Span {
1373 trace_id: 1,
1374 span_id: 3,
1375 parent_id: 2,
1376 name: libdd_tinybytes::BytesString::from_static("child"),
1377 start: 0,
1378 duration: 1,
1379 ..Default::default()
1380 };
1381
1382 let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, true);
1383 let spans = &req.resource_spans[0].scope_spans[0].spans;
1384 assert!(spans.iter().all(|span| span.flags == 0));
1385 }
1386
1387 #[test]
1388 fn sampled_trace_sets_sampled_flag_on_every_span() {
1389 let resource_info = OtlpResourceInfo::default();
1390 let root: Span<BytesData> = Span {
1391 trace_id: 1,
1392 span_id: 2,
1393 name: libdd_tinybytes::BytesString::from_static("root"),
1394 start: 0,
1395 duration: 1,
1396 ..Default::default()
1397 };
1398 let mut child: Span<BytesData> = Span {
1399 trace_id: 1,
1400 span_id: 3,
1401 parent_id: 2,
1402 name: libdd_tinybytes::BytesString::from_static("child"),
1403 start: 0,
1404 duration: 1,
1405 ..Default::default()
1406 };
1407 child.metrics.insert("_sampling_priority_v1".into(), 1.0);
1408 let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, true);
1409 let spans = &req.resource_spans[0].scope_spans[0].spans;
1410 assert!(spans.iter().all(|span| span.flags == 1));
1411 }
1412
1413 #[test]
1414 fn first_priority_sets_sampled_flag_on_every_span() {
1415 let resource_info = OtlpResourceInfo::default();
1416 let mut root: Span<BytesData> = Span {
1417 trace_id: 1,
1418 span_id: 2,
1419 name: libdd_tinybytes::BytesString::from_static("root"),
1420 start: 0,
1421 duration: 1,
1422 ..Default::default()
1423 };
1424 root.metrics.insert("_sampling_priority_v1".into(), -1.0);
1425 let mut child: Span<BytesData> = Span {
1426 trace_id: 1,
1427 span_id: 3,
1428 parent_id: 2,
1429 name: libdd_tinybytes::BytesString::from_static("child"),
1430 start: 0,
1431 duration: 1,
1432 ..Default::default()
1433 };
1434 child.metrics.insert("_sampling_priority_v1".into(), 1.0);
1435
1436 let req = map_traces_to_otlp(vec![vec![child, root]], &resource_info, true);
1437 let spans = &req.resource_spans[0].scope_spans[0].spans;
1438 assert!(spans.iter().all(|span| span.flags == 1));
1439 }
1440
1441 #[test]
1442 fn first_unsampled_priority_clears_sampled_flag_on_every_span() {
1443 let resource_info = OtlpResourceInfo::default();
1444 let mut first: Span<BytesData> = Span {
1445 trace_id: 1,
1446 span_id: 2,
1447 name: libdd_tinybytes::BytesString::from_static("first"),
1448 start: 0,
1449 duration: 1,
1450 ..Default::default()
1451 };
1452 first.metrics.insert("_sampling_priority_v1".into(), -1.0);
1453 let mut second: Span<BytesData> = Span {
1454 trace_id: 1,
1455 span_id: 3,
1456 parent_id: 2,
1457 name: libdd_tinybytes::BytesString::from_static("second"),
1458 start: 0,
1459 duration: 1,
1460 ..Default::default()
1461 };
1462 second.metrics.insert("_sampling_priority_v1".into(), 1.0);
1463
1464 let req = map_traces_to_otlp(vec![vec![first, second]], &resource_info, true);
1465 let spans = &req.resource_spans[0].scope_spans[0].spans;
1466 assert!(spans.iter().all(|span| span.flags == 0));
1467 }
1468
1469 #[test]
1470 fn semantics_disabled_preserves_per_span_sampling_flags() {
1471 let resource_info = OtlpResourceInfo::default();
1472 let root: Span<BytesData> = Span {
1473 trace_id: 1,
1474 span_id: 2,
1475 name: libdd_tinybytes::BytesString::from_static("root"),
1476 start: 0,
1477 duration: 1,
1478 ..Default::default()
1479 };
1480 let mut child: Span<BytesData> = Span {
1481 trace_id: 1,
1482 span_id: 3,
1483 parent_id: 2,
1484 name: libdd_tinybytes::BytesString::from_static("child"),
1485 start: 0,
1486 duration: 1,
1487 ..Default::default()
1488 };
1489 child.metrics.insert("_sampling_priority_v1".into(), 1.0);
1490 let mut fractional: Span<BytesData> = Span {
1491 trace_id: 1,
1492 span_id: 4,
1493 parent_id: 2,
1494 name: libdd_tinybytes::BytesString::from_static("fractional"),
1495 start: 0,
1496 duration: 1,
1497 ..Default::default()
1498 };
1499 fractional
1500 .metrics
1501 .insert("_sampling_priority_v1".into(), 0.5);
1502
1503 let req = map_traces_to_otlp(vec![vec![root, child, fractional]], &resource_info, false);
1504 let spans = &req.resource_spans[0].scope_spans[0].spans;
1505 assert_eq!(spans[0].flags, 0);
1506 assert_eq!(spans[1].flags, 1);
1507 assert_eq!(spans[2].flags, 0);
1508 }
1509}