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 tag_to_otlp_kind(t: &str) -> i32 {
95 if t.eq_ignore_ascii_case("server") {
99 span_kind::SERVER
100 } else if t.eq_ignore_ascii_case("client") {
101 span_kind::CLIENT
102 } else if t.eq_ignore_ascii_case("producer") {
103 span_kind::PRODUCER
104 } else if t.eq_ignore_ascii_case("consumer") {
105 span_kind::CONSUMER
106 } else if t.eq_ignore_ascii_case("internal") {
107 span_kind::INTERNAL
108 } else {
109 span_kind::UNSPECIFIED
110 }
111}
112
113fn dd_type_to_otlp_kind(t: &str) -> i32 {
115 if t.eq_ignore_ascii_case("server")
117 || t.eq_ignore_ascii_case("web")
118 || t.eq_ignore_ascii_case("http")
119 {
120 span_kind::SERVER
121 } else if t.eq_ignore_ascii_case("client") {
122 span_kind::CLIENT
123 } else if t.eq_ignore_ascii_case("producer") {
124 span_kind::PRODUCER
125 } else if t.eq_ignore_ascii_case("consumer") {
126 span_kind::CONSUMER
127 } else {
128 span_kind::INTERNAL
129 }
130}
131
132fn proto_kv(key: String, value: ProtoValue) -> ProtoKeyValue {
137 ProtoKeyValue {
138 key,
139 value: Some(ProtoAnyValue { value: Some(value) }),
140 key_ref: 0,
141 }
142}
143
144fn collect_span_attributes<T: TraceData>(
149 span: &Span<T>,
150 resource_service: &str,
151 otel_trace_semantics_enabled: bool,
152) -> (Vec<ProtoKeyValue>, usize) {
153 let capacity = (4 + span.meta.len() + span.metrics.len() + span.meta_struct.len())
156 .min(MAX_ATTRIBUTES_PER_SPAN);
157 let mut attrs: Vec<ProtoKeyValue> = Vec::with_capacity(capacity);
158 let span_service = span.service.borrow();
162 let has_per_span_service = !span_service.is_empty() && span_service != resource_service;
163 if has_per_span_service && !otel_trace_semantics_enabled {
164 attrs.push(proto_kv(
165 "service.name".to_string(),
166 ProtoValue::StringValue(span_service.to_string()),
167 ));
168 }
169 let operation_name = span.name.borrow();
170 let has_operation_name = !operation_name.is_empty();
171 if has_operation_name && !otel_trace_semantics_enabled {
172 attrs.push(proto_kv(
173 "operation.name".to_string(),
174 ProtoValue::StringValue(operation_name.to_string()),
175 ));
176 }
177 let span_type = span.r#type.borrow();
178 let has_span_type = !span_type.is_empty();
179 if has_span_type && !otel_trace_semantics_enabled {
180 attrs.push(proto_kv(
181 "span.type".to_string(),
182 ProtoValue::StringValue(span_type.to_string()),
183 ));
184 }
185 let resource_name = span.resource.borrow();
186 let has_resource_name = !resource_name.is_empty();
187 if has_resource_name && !otel_trace_semantics_enabled {
188 attrs.push(proto_kv(
189 "resource.name".to_string(),
190 ProtoValue::StringValue(resource_name.to_string()),
191 ));
192 }
193 for (k, v) in span.meta.iter() {
194 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
195 break;
196 }
197 let key = k.borrow();
198 if otel_trace_semantics_enabled
199 && (key == "error.msg" || key == "error.message" || key == "span.kind")
200 {
201 continue;
202 }
203 attrs.push(proto_kv(
204 key.to_string(),
205 ProtoValue::StringValue(v.borrow().to_string()),
206 ));
207 }
208 for (k, v) in span.metrics.iter() {
209 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
210 break;
211 }
212 let value = if v.fract() == 0.0 && (*v >= i64::MIN as f64 && *v <= i64::MAX as f64) {
213 ProtoValue::IntValue(*v as i64)
214 } else {
215 ProtoValue::DoubleValue(*v)
216 };
217 attrs.push(proto_kv(k.borrow().to_string(), value));
218 }
219 for (k, v) in span.meta_struct.iter() {
220 if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
221 break;
222 }
223 attrs.push(proto_kv(
224 k.borrow().to_string(),
225 ProtoValue::BytesValue(v.borrow().to_vec()),
226 ));
227 }
228 let excluded_compat_tags = if otel_trace_semantics_enabled {
232 span.meta.contains_key("error.msg") as usize
233 + span.meta.contains_key("error.message") as usize
234 + span.meta.contains_key("span.kind") as usize
235 } else {
236 0
237 };
238 let promoted = if otel_trace_semantics_enabled {
239 0
240 } else {
241 (has_per_span_service as usize)
242 + (has_operation_name as usize)
243 + (has_span_type as usize)
244 + (has_resource_name as usize)
245 };
246 let total = promoted
247 + (span.meta.len() - excluded_compat_tags)
248 + span.metrics.len()
249 + span.meta_struct.len();
250 let dropped = total.saturating_sub(attrs.len());
251 (attrs, dropped)
252}
253
254fn event_attr_value<T: TraceData>(av: &crate::span::v04::AttributeArrayValue<T>) -> ProtoValue {
256 use crate::span::v04::AttributeArrayValue;
257 match av {
258 AttributeArrayValue::String(s) => ProtoValue::StringValue(s.borrow().to_string()),
259 AttributeArrayValue::Boolean(b) => ProtoValue::BoolValue(*b),
260 AttributeArrayValue::Integer(i) => ProtoValue::IntValue(*i),
261 AttributeArrayValue::Double(d) => ProtoValue::DoubleValue(*d),
262 }
263}
264
265fn collect_event_attributes<T: TraceData>(ev: &SpanEvent<T>) -> Vec<ProtoKeyValue> {
266 use crate::span::v04::AttributeAnyValue;
267 ev.attributes
268 .iter()
269 .map(|(k, v)| {
270 let value = match v {
271 AttributeAnyValue::SingleValue(av) => event_attr_value(av),
272 AttributeAnyValue::Array(items) => ProtoValue::ArrayValue(ProtoArrayValue {
273 values: items
274 .iter()
275 .map(|it| ProtoAnyValue {
276 value: Some(event_attr_value(it)),
277 })
278 .collect(),
279 }),
280 };
281 proto_kv(k.borrow().to_string(), value)
282 })
283 .collect()
284}
285
286pub fn map_traces_to_otlp<T: TraceData>(
302 trace_chunks: Vec<Vec<Span<T>>>,
303 resource_info: &OtlpResourceInfo,
304 otel_trace_semantics_enabled: bool,
305) -> ProtoReq {
306 let resource = build_resource(resource_info);
307 let total_spans: usize = trace_chunks.iter().map(|chunk| chunk.len()).sum();
309 let mut all_spans: Vec<ProtoSpan> = Vec::with_capacity(total_spans);
310 for chunk in &trace_chunks {
311 let high = chunk_trace_id_high(chunk);
315 for span in chunk {
316 all_spans.push(map_span(
317 span,
318 &resource_info.service,
319 high,
320 otel_trace_semantics_enabled,
321 ));
322 }
323 }
324 ProtoReq {
325 resource_spans: vec![ProtoResourceSpans {
326 resource: Some(resource),
327 scope_spans: vec![ProtoScopeSpans {
328 scope: Some(ProtoScope {
329 name: resource_info.instrumentation_scope_name.clone(),
330 version: resource_info.instrumentation_scope_version.clone(),
331 attributes: Vec::new(),
332 dropped_attributes_count: 0,
333 }),
334 spans: all_spans,
335 schema_url: String::new(),
336 }],
337 schema_url: String::new(),
338 }],
339 }
340}
341
342fn build_resource(resource_info: &OtlpResourceInfo) -> ProtoResource {
343 fn push_str_attr(attrs: &mut Vec<ProtoKeyValue>, k: &str, v: &str) {
344 if !v.is_empty() {
345 attrs.push(proto_kv(
346 k.to_string(),
347 ProtoValue::StringValue(v.to_string()),
348 ));
349 }
350 }
351 let mut attributes = Vec::new();
352 push_str_attr(&mut attributes, "service.name", &resource_info.service);
353 push_str_attr(
354 &mut attributes,
355 "deployment.environment.name",
356 &resource_info.env,
357 );
358 push_str_attr(
359 &mut attributes,
360 "service.version",
361 &resource_info.app_version,
362 );
363 attributes.push(proto_kv(
364 "telemetry.sdk.name".to_string(),
365 ProtoValue::StringValue("datadog".to_string()),
366 ));
367 push_str_attr(
368 &mut attributes,
369 "telemetry.sdk.language",
370 &resource_info.language,
371 );
372 push_str_attr(
373 &mut attributes,
374 "telemetry.sdk.version",
375 &resource_info.tracer_version,
376 );
377 push_str_attr(&mut attributes, "runtime-id", &resource_info.runtime_id);
378 if resource_info.client_computed_stats {
381 push_str_attr(&mut attributes, "_dd.stats_computed", "true");
382 }
383 ProtoResource {
385 attributes,
386 dropped_attributes_count: 0,
387 entity_refs: Vec::new(),
388 }
389}
390
391fn map_span<T: TraceData>(
392 span: &Span<T>,
393 resource_service: &str,
394 chunk_trace_id_high: u64,
395 otel_trace_semantics_enabled: bool,
396) -> ProtoSpan {
397 let trace_id_128 = ((chunk_trace_id_high as u128) << 64) | (span.trace_id as u64 as u128);
401 let parent_span_id = if span.parent_id != 0 {
402 span.parent_id.to_be_bytes().to_vec()
403 } else {
404 Vec::new()
405 };
406 let (attributes, dropped_attributes_count) =
407 collect_span_attributes(span, resource_service, otel_trace_semantics_enabled);
408 let (code, message) = span_status(span);
409 let flags = span
410 .metrics
411 .get("_sampling_priority_v1")
412 .map(|p| (*p >= 1.0) as u32)
413 .unwrap_or(0);
414 let trace_state = span
415 .meta
416 .get("tracestate")
417 .map(|v| v.borrow().to_string())
418 .filter(|s| !s.is_empty())
419 .unwrap_or_default();
420 let links = span.span_links.iter().map(map_span_link).collect();
421 let (events, dropped_events_count) = map_span_events(&span.span_events);
422 ProtoSpan {
423 trace_id: trace_id_128.to_be_bytes().to_vec(),
424 span_id: span.span_id.to_be_bytes().to_vec(),
425 trace_state,
426 parent_span_id,
427 flags,
428 name: span.resource.borrow().to_string(),
429 kind: span_kind(span),
430 start_time_unix_nano: span.start.max(0) as u64,
432 end_time_unix_nano: (span.start + span.duration).max(0) as u64,
433 attributes,
434 dropped_attributes_count: dropped_attributes_count as u32,
435 events,
436 dropped_events_count: dropped_events_count as u32,
437 links,
438 dropped_links_count: 0,
440 status: Some(ProtoStatus {
441 message: message.unwrap_or_default(),
442 code,
443 }),
444 }
445}
446
447fn map_span_link<T: TraceData>(link: &SpanLink<T>) -> ProtoLink {
448 let trace_id_128 = ((link.trace_id_high as u128) << 64) | (link.trace_id as u128);
449 ProtoLink {
450 trace_id: trace_id_128.to_be_bytes().to_vec(),
451 span_id: link.span_id.to_be_bytes().to_vec(),
452 trace_state: {
453 let ts = link.tracestate.borrow();
454 if ts.is_empty() {
455 String::new()
456 } else {
457 ts.to_string()
458 }
459 },
460 attributes: link
461 .attributes
462 .iter()
463 .map(|(k, v)| {
464 proto_kv(
465 k.borrow().to_string(),
466 ProtoValue::StringValue(v.borrow().to_string()),
467 )
468 })
469 .collect(),
470 dropped_attributes_count: 0,
471 flags: link.flags & !SPAN_LINK_FLAGS_SET_SENTINEL,
475 }
476}
477
478fn map_span_events<T: TraceData>(events: &[SpanEvent<T>]) -> (Vec<ProtoEvent>, usize) {
479 const MAX_EVENTS_PER_SPAN: usize = 128;
480 let mut out = Vec::with_capacity(events.len().min(MAX_EVENTS_PER_SPAN));
481 for ev in events.iter().take(MAX_EVENTS_PER_SPAN) {
482 out.push(ProtoEvent {
483 time_unix_nano: ev.time_unix_nano,
484 name: ev.name.borrow().to_string(),
485 attributes: collect_event_attributes(ev),
486 dropped_attributes_count: 0,
487 });
488 }
489 let dropped = events.len().saturating_sub(out.len());
490 (out, dropped)
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use crate::otlp_encoder::OtlpResourceInfo;
497 use crate::span::BytesData;
498
499 #[test]
500 fn maps_native_span_to_prost_ir() {
501 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
502 let resource_info = OtlpResourceInfo::default();
503 let mut span: Span<BytesData> = Span {
504 trace_id: 0xD269B633813FC60C_u128,
505 span_id: 0xEEE19B7EC3C1B174,
506 parent_id: 0xEEE19B7EC3C1B173,
507 name: libdd_tinybytes::BytesString::from_static("op"),
508 resource: libdd_tinybytes::BytesString::from_static("res"),
509 r#type: libdd_tinybytes::BytesString::from_static("web"),
510 start: 1544712660000000000,
511 duration: 1000000000,
512 error: 1,
513 ..Default::default()
514 };
515 span.meta.insert(
516 "error.msg".into(),
517 libdd_tinybytes::BytesString::from_static("boom"),
518 );
519 span.metrics
520 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
521 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
522 let s = &req.resource_spans[0].scope_spans[0].spans[0];
523 assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
524 assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
525 assert_eq!(
526 s.parent_span_id,
527 0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
528 );
529 assert_eq!(s.name, "res");
530 assert_eq!(s.kind, 2); assert_eq!(s.start_time_unix_nano, 1544712660000000000);
532 assert_eq!(s.end_time_unix_nano, 1544712661000000000);
533 let st = s.status.as_ref().unwrap();
534 assert_eq!(st.code, 2);
535 assert_eq!(st.message, "boom");
536 let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
537 assert!(matches!(
538 count.value.as_ref().unwrap().value,
539 Some(PV::IntValue(42))
540 ));
541 }
542
543 #[test]
544 fn instrumentation_scope_from_resource_info() {
545 let resource_info = OtlpResourceInfo {
546 instrumentation_scope_name: "dd-trace-js".to_string(),
547 instrumentation_scope_version: "7.0.0-pre".to_string(),
548 ..Default::default()
549 };
550 let span: Span<BytesData> = Span {
551 trace_id: 1,
552 span_id: 2,
553 name: libdd_tinybytes::BytesString::from_static("s"),
554 start: 0,
555 duration: 1,
556 ..Default::default()
557 };
558
559 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
560 let scope = req.resource_spans[0].scope_spans[0].scope.as_ref().unwrap();
561 assert_eq!(scope.name, "dd-trace-js");
562 assert_eq!(scope.version, "7.0.0-pre");
563 }
564
565 #[test]
566 fn proto_span_uses_raw_id_bytes_and_native_timestamps() {
567 let resource_info = OtlpResourceInfo {
568 service: "svc".to_string(),
569 ..Default::default()
570 };
571 let span: Span<BytesData> = Span {
572 trace_id: 0x5b8efff798038103_d269b633813fc60c_u128,
573 span_id: 0xEEE19B7EC3C1B174,
574 parent_id: 0xEEE19B7EC3C1B173,
575 name: libdd_tinybytes::BytesString::from_static("op"),
576 resource: libdd_tinybytes::BytesString::from_static("res"),
577 r#type: libdd_tinybytes::BytesString::from_static("web"),
578 start: 1544712660000000000,
579 duration: 1000000000,
580 ..Default::default()
581 };
582 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
583 let s = &req.resource_spans[0].scope_spans[0].spans[0];
584 assert_eq!(
585 s.trace_id,
586 0x5b8efff798038103_d269b633813fc60c_u128
587 .to_be_bytes()
588 .to_vec()
589 );
590 assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
591 assert_eq!(
592 s.parent_span_id,
593 0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
594 );
595 assert_eq!(s.start_time_unix_nano, 1544712660000000000);
596 assert_eq!(s.end_time_unix_nano, 1544712661000000000);
597 assert_eq!(s.name, "res");
598 assert_eq!(s.kind, span_kind::SERVER);
599 }
600
601 #[test]
602 fn negative_start_clamps_to_zero() {
603 let resource_info = OtlpResourceInfo {
607 service: "svc".to_string(),
608 ..Default::default()
609 };
610 let span: Span<BytesData> = Span {
611 trace_id: 1,
612 span_id: 1,
613 start: -1,
614 duration: 0,
615 ..Default::default()
616 };
617 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
618 let s = &req.resource_spans[0].scope_spans[0].spans[0];
619 assert_eq!(
620 s.start_time_unix_nano, 0,
621 "negative start must clamp to 0, not wrap"
622 );
623 assert_eq!(
624 s.end_time_unix_nano, 0,
625 "negative start+duration must clamp to 0, not wrap"
626 );
627 }
628
629 #[test]
630 fn status_error_message_from_meta() {
631 let resource_info = OtlpResourceInfo::default();
632 let mut span: Span<BytesData> = Span {
633 trace_id: 1,
634 span_id: 2,
635 name: libdd_tinybytes::BytesString::from_static("err_span"),
636 start: 0,
637 duration: 1,
638 error: 1,
639 ..Default::default()
640 };
641 span.meta.insert(
642 libdd_tinybytes::BytesString::from_static("error.msg"),
643 libdd_tinybytes::BytesString::from_static("something broke"),
644 );
645 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
646 let s = &req.resource_spans[0].scope_spans[0].spans[0];
647 let status = s.status.as_ref().unwrap();
648 assert_eq!(status.code, status_code::ERROR);
649 assert_eq!(status.message, "something broke");
650 }
651
652 #[test]
653 fn metrics_as_int_or_double() {
654 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
655 let resource_info = OtlpResourceInfo::default();
656 let mut span: Span<BytesData> = Span {
657 trace_id: 1,
658 span_id: 2,
659 name: libdd_tinybytes::BytesString::from_static("m"),
660 start: 0,
661 duration: 1,
662 ..Default::default()
663 };
664 span.metrics
665 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
666 span.metrics.insert(
667 libdd_tinybytes::BytesString::from_static("rate"),
668 std::f64::consts::PI,
669 );
670 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
671 let s = &req.resource_spans[0].scope_spans[0].spans[0];
672 let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
673 assert!(matches!(
674 count.value.as_ref().unwrap().value,
675 Some(PV::IntValue(42))
676 ));
677 let rate = s.attributes.iter().find(|a| a.key == "rate").unwrap();
678 match rate.value.as_ref().unwrap().value {
679 Some(PV::DoubleValue(d)) => assert!((d - std::f64::consts::PI).abs() < 1e-9),
680 ref other => panic!("expected double, got {other:?}"),
681 }
682 }
683
684 #[test]
685 fn trace_id_128_from_dd_p_tid() {
686 let resource_info = OtlpResourceInfo::default();
689 let mut span: Span<BytesData> = Span {
690 trace_id: 0xD269B633813FC60C_u128, span_id: 1,
692 name: libdd_tinybytes::BytesString::from_static("s"),
693 start: 0,
694 duration: 1,
695 ..Default::default()
696 };
697 span.meta.insert(
698 "_dd.p.tid".into(),
699 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
700 );
701 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
702 let s = &req.resource_spans[0].scope_spans[0].spans[0];
703 assert_eq!(
704 s.trace_id,
705 0x5b8efff798038103_d269b633813fc60c_u128
706 .to_be_bytes()
707 .to_vec()
708 );
709 }
710
711 #[test]
712 fn trace_id_128_from_native_span_field() {
713 let resource_info = OtlpResourceInfo::default();
717 let full: u128 = 0x5b8efff798038103_d269b633813fc60c_u128;
718 let root: Span<BytesData> = Span {
719 trace_id: full,
720 span_id: 1,
721 name: libdd_tinybytes::BytesString::from_static("root"),
722 start: 0,
723 duration: 1,
724 ..Default::default()
725 };
726 let child: Span<BytesData> = Span {
728 trace_id: 0xD269B633813FC60C_u128,
729 span_id: 2,
730 parent_id: 1,
731 name: libdd_tinybytes::BytesString::from_static("child"),
732 start: 0,
733 duration: 1,
734 ..Default::default()
735 };
736 let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, false);
737 let spans = &req.resource_spans[0].scope_spans[0].spans;
738 let expected = full.to_be_bytes().to_vec();
739 assert_eq!(spans[0].trace_id, expected);
740 assert_eq!(spans[1].trace_id, expected);
741 }
742
743 #[test]
744 fn trace_id_128_without_dd_p_tid_defaults_high_to_zero() {
745 let resource_info = OtlpResourceInfo::default();
748 let span: Span<BytesData> = Span {
749 trace_id: 0xD269B633813FC60C_u128,
750 span_id: 1,
751 name: libdd_tinybytes::BytesString::from_static("s"),
752 start: 0,
753 duration: 1,
754 ..Default::default()
755 };
756 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
757 let s = &req.resource_spans[0].scope_spans[0].spans[0];
758 assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
759 }
760
761 #[test]
762 fn trace_id_128_propagated_to_chunk_children() {
763 let resource_info = OtlpResourceInfo::default();
767 let low: u128 = 0xD269B633813FC60C_u128;
768 let mut root: Span<BytesData> = Span {
769 trace_id: low,
770 span_id: 1,
771 name: libdd_tinybytes::BytesString::from_static("root"),
772 start: 0,
773 duration: 1,
774 ..Default::default()
775 };
776 root.meta.insert(
777 "_dd.p.tid".into(),
778 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
779 );
780 let child_a: Span<BytesData> = Span {
781 trace_id: low,
782 span_id: 2,
783 parent_id: 1,
784 name: libdd_tinybytes::BytesString::from_static("child_a"),
785 start: 0,
786 duration: 1,
787 ..Default::default()
788 };
789 let child_b: Span<BytesData> = Span {
790 trace_id: low,
791 span_id: 3,
792 parent_id: 1,
793 name: libdd_tinybytes::BytesString::from_static("child_b"),
794 start: 0,
795 duration: 1,
796 ..Default::default()
797 };
798 let req = map_traces_to_otlp(vec![vec![root, child_a, child_b]], &resource_info, false);
799 let spans = &req.resource_spans[0].scope_spans[0].spans;
800 assert_eq!(spans.len(), 3);
801 let expected = 0x5b8efff798038103_d269b633813fc60c_u128
802 .to_be_bytes()
803 .to_vec();
804 for s in spans {
805 assert_eq!(s.trace_id, expected);
806 }
807 }
808
809 #[test]
810 fn trace_id_128_isolation_across_chunks() {
811 let resource_info = OtlpResourceInfo::default();
814 let low_a: u128 = 0x1111111111111111_u128;
815 let low_b: u128 = 0x2222222222222222_u128;
816 let mut root_a: Span<BytesData> = Span {
817 trace_id: low_a,
818 span_id: 1,
819 name: libdd_tinybytes::BytesString::from_static("root_a"),
820 start: 0,
821 duration: 1,
822 ..Default::default()
823 };
824 root_a.meta.insert(
825 "_dd.p.tid".into(),
826 libdd_tinybytes::BytesString::from_static("aaaaaaaaaaaaaaaa"),
827 );
828 let child_a: Span<BytesData> = Span {
829 trace_id: low_a,
830 span_id: 2,
831 parent_id: 1,
832 name: libdd_tinybytes::BytesString::from_static("child_a"),
833 start: 0,
834 duration: 1,
835 ..Default::default()
836 };
837 let mut root_b: Span<BytesData> = Span {
838 trace_id: low_b,
839 span_id: 3,
840 name: libdd_tinybytes::BytesString::from_static("root_b"),
841 start: 0,
842 duration: 1,
843 ..Default::default()
844 };
845 root_b.meta.insert(
846 "_dd.p.tid".into(),
847 libdd_tinybytes::BytesString::from_static("bbbbbbbbbbbbbbbb"),
848 );
849 let child_b: Span<BytesData> = Span {
850 trace_id: low_b,
851 span_id: 4,
852 parent_id: 3,
853 name: libdd_tinybytes::BytesString::from_static("child_b"),
854 start: 0,
855 duration: 1,
856 ..Default::default()
857 };
858 let req = map_traces_to_otlp(
859 vec![vec![root_a, child_a], vec![root_b, child_b]],
860 &resource_info,
861 false,
862 );
863 let spans = &req.resource_spans[0].scope_spans[0].spans;
864 assert_eq!(spans.len(), 4);
865 let expect_a = 0xaaaaaaaaaaaaaaaa_1111111111111111_u128
866 .to_be_bytes()
867 .to_vec();
868 let expect_b = 0xbbbbbbbbbbbbbbbb_2222222222222222_u128
869 .to_be_bytes()
870 .to_vec();
871 assert_eq!(spans[0].trace_id, expect_a);
872 assert_eq!(spans[1].trace_id, expect_a);
873 assert_eq!(spans[2].trace_id, expect_b);
874 assert_eq!(spans[3].trace_id, expect_b);
875 }
876
877 #[test]
878 fn chunk_with_malformed_dd_p_tid_on_root_falls_back() {
879 let resource_info = OtlpResourceInfo::default();
883 let low: u128 = 0xD269B633813FC60C_u128;
884 let mut root: Span<BytesData> = Span {
885 trace_id: low,
886 span_id: 1,
887 name: libdd_tinybytes::BytesString::from_static("root"),
888 start: 0,
889 duration: 1,
890 ..Default::default()
891 };
892 root.meta.insert(
893 "_dd.p.tid".into(),
894 libdd_tinybytes::BytesString::from_static("not-hex"),
895 );
896 let child_no_tag: Span<BytesData> = Span {
897 trace_id: low,
898 span_id: 2,
899 parent_id: 1,
900 name: libdd_tinybytes::BytesString::from_static("child_no_tag"),
901 start: 0,
902 duration: 1,
903 ..Default::default()
904 };
905 let mut child_valid: Span<BytesData> = Span {
906 trace_id: low,
907 span_id: 3,
908 parent_id: 1,
909 name: libdd_tinybytes::BytesString::from_static("child_valid"),
910 start: 0,
911 duration: 1,
912 ..Default::default()
913 };
914 child_valid.meta.insert(
915 "_dd.p.tid".into(),
916 libdd_tinybytes::BytesString::from_static("dddddddddddddddd"),
917 );
918 let req = map_traces_to_otlp(
919 vec![vec![root, child_no_tag, child_valid]],
920 &resource_info,
921 false,
922 );
923 let spans = &req.resource_spans[0].scope_spans[0].spans;
924 let expected = 0xdddddddddddddddd_d269b633813fc60c_u128
927 .to_be_bytes()
928 .to_vec();
929 assert_eq!(spans[0].trace_id, expected);
930 assert_eq!(spans[1].trace_id, expected);
931 assert_eq!(spans[2].trace_id, expected);
932 }
933
934 #[test]
935 fn test_stats_computed_resource_attr_set_when_enabled() {
936 let resource_info = OtlpResourceInfo {
937 client_computed_stats: true,
938 ..Default::default()
939 };
940 let span: Span<BytesData> = Span {
941 trace_id: 1,
942 span_id: 2,
943 name: libdd_tinybytes::BytesString::from_static("s"),
944 start: 0,
945 duration: 1,
946 ..Default::default()
947 };
948 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
949 let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
950 let kv = resource_attrs
951 .iter()
952 .find(|a| a.key == "_dd.stats_computed")
953 .expect("_dd.stats_computed must be present when client_computed_stats=true");
954 let val = match kv.value.as_ref().and_then(|v| v.value.as_ref()) {
955 Some(ProtoValue::StringValue(s)) => s.as_str(),
956 other => panic!("expected stringValue, got {other:?}"),
957 };
958 assert_eq!(val, "true");
959 }
960
961 #[test]
962 fn test_stats_computed_resource_attr_absent_when_disabled() {
963 let resource_info = OtlpResourceInfo {
964 client_computed_stats: false,
965 ..Default::default()
966 };
967 let span: Span<BytesData> = Span {
968 trace_id: 1,
969 span_id: 2,
970 name: libdd_tinybytes::BytesString::from_static("s"),
971 start: 0,
972 duration: 1,
973 ..Default::default()
974 };
975 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
976 let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
977 assert!(
978 !resource_attrs.iter().any(|a| a.key == "_dd.stats_computed"),
979 "_dd.stats_computed must not be emitted when client_computed_stats=false"
980 );
981 }
982
983 #[test]
984 fn span_link_flags_are_carried() {
985 let mut 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 span.span_links.push(SpanLink {
996 trace_id: 0x11,
997 span_id: 0x22,
998 flags: 1,
999 ..Default::default()
1000 });
1001 let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
1002 let link = &req.resource_spans[0].scope_spans[0].spans[0].links[0];
1003 assert_eq!(
1004 link.flags, 1,
1005 "OTLP Link.flags must carry the span link's flags"
1006 );
1007 }
1008
1009 #[test]
1010 fn span_link_flags_sentinel_bit_masked() {
1011 fn mapped_flags(flags: u32) -> u32 {
1015 let mut span: Span<BytesData> = Span {
1016 trace_id: 1,
1017 span_id: 2,
1018 name: libdd_tinybytes::BytesString::from_static("s"),
1019 start: 0,
1020 duration: 1,
1021 ..Default::default()
1022 };
1023 span.span_links.push(SpanLink {
1024 trace_id: 0x11,
1025 span_id: 0x22,
1026 flags,
1027 ..Default::default()
1028 });
1029 let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
1030 req.resource_spans[0].scope_spans[0].spans[0].links[0].flags
1031 }
1032
1033 assert_eq!(
1034 mapped_flags(0x8000_0001),
1035 1,
1036 "OTLP Link.flags must not carry the internal sentinel bit"
1037 );
1038 assert_eq!(
1039 mapped_flags(0x8000_0000),
1040 0,
1041 "an explicit drop decision must still map to flags: 0"
1042 );
1043 }
1044
1045 #[test]
1046 fn test_otel_trace_semantics_enabled() {
1047 let resource_info = OtlpResourceInfo {
1051 service: "resource-svc".to_string(),
1052 ..Default::default()
1053 };
1054 let mut span: Span<BytesData> = Span {
1055 trace_id: 1,
1056 span_id: 2,
1057 name: libdd_tinybytes::BytesString::from_static("http.request"),
1058 service: libdd_tinybytes::BytesString::from_static("span-svc"),
1059 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
1060 r#type: libdd_tinybytes::BytesString::from_static("web"),
1061 start: 0,
1062 duration: 1,
1063 ..Default::default()
1064 };
1065 span.meta.insert(
1066 libdd_tinybytes::BytesString::from_static("span.kind"),
1067 libdd_tinybytes::BytesString::from_static("client"),
1068 );
1069 span.meta.insert(
1070 libdd_tinybytes::BytesString::from_static("http.method"),
1071 libdd_tinybytes::BytesString::from_static("GET"),
1072 );
1073 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
1074 let attrs = &req.resource_spans[0].scope_spans[0].spans[0].attributes;
1075 let keys: Vec<&str> = attrs.iter().map(|kv| kv.key.as_str()).collect();
1076 for omitted in [
1077 "service.name",
1078 "operation.name",
1079 "resource.name",
1080 "span.type",
1081 "span.kind",
1082 ] {
1083 assert!(
1084 !keys.contains(&omitted),
1085 "OTel-semantics must omit {omitted}"
1086 );
1087 }
1088 assert!(
1089 keys.contains(&"http.method"),
1090 "OTel-standard meta tags must remain"
1091 );
1092 }
1093
1094 #[test]
1095 fn error_message_promoted_to_status_under_otel_semantics() {
1096 let resource_info = OtlpResourceInfo::default();
1100 let mut span: Span<BytesData> = Span {
1101 trace_id: 1,
1102 span_id: 2,
1103 name: libdd_tinybytes::BytesString::from_static("op"),
1104 start: 0,
1105 duration: 1,
1106 error: 1,
1107 ..Default::default()
1108 };
1109 span.meta.insert(
1110 libdd_tinybytes::BytesString::from_static("error.message"),
1111 libdd_tinybytes::BytesString::from_static("boom"),
1112 );
1113 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
1114 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
1115 let status = otlp_span
1116 .status
1117 .as_ref()
1118 .expect("status present on error span");
1119 assert_eq!(status.code, status_code::ERROR);
1120 assert_eq!(
1121 status.message, "boom",
1122 "error.message must be promoted to the OTLP Status message"
1123 );
1124 assert!(
1125 !otlp_span
1126 .attributes
1127 .iter()
1128 .any(|kv| kv.key == "error.message"),
1129 "error.message compat attr must be omitted under OTel-semantics"
1130 );
1131 }
1132
1133 #[test]
1134 fn empty_chunk_does_not_panic() {
1135 let resource_info = OtlpResourceInfo::default();
1137 let empty: Vec<Vec<Span<BytesData>>> = vec![vec![]];
1138 let req = map_traces_to_otlp(empty, &resource_info, false);
1139 let spans = &req.resource_spans[0].scope_spans[0].spans;
1140 assert!(spans.is_empty());
1141 }
1142
1143 #[test]
1144 fn tracestate_from_meta() {
1145 let resource_info = OtlpResourceInfo::default();
1146 let mut span: Span<BytesData> = Span {
1147 trace_id: 1,
1148 span_id: 2,
1149 name: libdd_tinybytes::BytesString::from_static("s"),
1150 start: 0,
1151 duration: 1,
1152 ..Default::default()
1153 };
1154 span.meta.insert(
1155 "tracestate".into(),
1156 libdd_tinybytes::BytesString::from_static("vendor1=abc,rojo=00f067"),
1157 );
1158 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1159 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1160 assert_eq!(s.trace_state, "vendor1=abc,rojo=00f067");
1161 }
1162
1163 #[test]
1164 fn meta_struct_as_bytes_value() {
1165 use libdd_tinybytes::Bytes;
1166 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1167 let resource_info = OtlpResourceInfo::default();
1168 let mut span: Span<BytesData> = Span {
1169 trace_id: 1,
1170 span_id: 2,
1171 name: libdd_tinybytes::BytesString::from_static("s"),
1172 start: 0,
1173 duration: 1,
1174 ..Default::default()
1175 };
1176 span.meta_struct
1177 .insert("my_key".into(), Bytes::from(vec![1u8, 2, 3]));
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 let kv = s
1181 .attributes
1182 .iter()
1183 .find(|a| a.key == "my_key")
1184 .expect("my_key attribute not found");
1185 match kv.value.as_ref().unwrap().value {
1186 Some(PV::BytesValue(ref b)) => assert_eq!(b, &vec![1u8, 2, 3]),
1187 ref other => panic!("expected bytes, got {other:?}"),
1188 }
1189 }
1190
1191 #[test]
1192 fn operation_name_attribute() {
1193 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1194 let resource_info = OtlpResourceInfo::default();
1195 let span: Span<BytesData> = Span {
1196 trace_id: 1,
1197 span_id: 2,
1198 name: libdd_tinybytes::BytesString::from_static("my.operation"),
1199 start: 0,
1200 duration: 1,
1201 ..Default::default()
1202 };
1203 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1204 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1205 let kv = s
1206 .attributes
1207 .iter()
1208 .find(|a| a.key == "operation.name")
1209 .expect("operation.name attribute not found");
1210 match kv.value.as_ref().unwrap().value {
1211 Some(PV::StringValue(ref v)) => assert_eq!(v, "my.operation"),
1212 ref other => panic!("expected string, got {other:?}"),
1213 }
1214 }
1215
1216 #[test]
1217 fn span_type_attribute() {
1218 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1219 let resource_info = OtlpResourceInfo::default();
1220 let span: Span<BytesData> = Span {
1221 trace_id: 1,
1222 span_id: 2,
1223 name: libdd_tinybytes::BytesString::from_static("s"),
1224 r#type: libdd_tinybytes::BytesString::from_static("grpc"),
1225 start: 0,
1226 duration: 1,
1227 ..Default::default()
1228 };
1229 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1230 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1231 let kv = s
1232 .attributes
1233 .iter()
1234 .find(|a| a.key == "span.type")
1235 .expect("span.type attribute not found");
1236 match kv.value.as_ref().unwrap().value {
1237 Some(PV::StringValue(ref v)) => assert_eq!(v, "grpc"),
1238 ref other => panic!("expected string, got {other:?}"),
1239 }
1240 }
1241
1242 #[test]
1243 fn resource_name_attribute_and_span_name() {
1244 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1245 let resource_info = OtlpResourceInfo::default();
1246 let span: Span<BytesData> = Span {
1247 trace_id: 1,
1248 span_id: 2,
1249 name: libdd_tinybytes::BytesString::from_static("s"),
1250 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
1251 start: 0,
1252 duration: 1,
1253 ..Default::default()
1254 };
1255 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1256 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1257 assert_eq!(s.name, "GET /api/users");
1259 let kv = s
1261 .attributes
1262 .iter()
1263 .find(|a| a.key == "resource.name")
1264 .expect("resource.name attribute not found");
1265 match kv.value.as_ref().unwrap().value {
1266 Some(PV::StringValue(ref v)) => assert_eq!(v, "GET /api/users"),
1267 ref other => panic!("expected string, got {other:?}"),
1268 }
1269 }
1270
1271 #[test]
1272 fn empty_resource_name_not_emitted() {
1273 let resource_info = OtlpResourceInfo::default();
1275 let span: Span<BytesData> = Span {
1276 trace_id: 1,
1277 span_id: 2,
1278 name: libdd_tinybytes::BytesString::from_static("s"),
1279 start: 0,
1281 duration: 1,
1282 ..Default::default()
1283 };
1284 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1285 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1286 assert!(
1287 !s.attributes.iter().any(|a| a.key == "resource.name"),
1288 "resource.name should not be emitted when resource is empty"
1289 );
1290 }
1291
1292 #[test]
1293 fn per_span_service_name_attribute() {
1294 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1295 let resource_info = OtlpResourceInfo {
1298 service: "resource-svc".to_string(),
1299 ..Default::default()
1300 };
1301 let span: Span<BytesData> = Span {
1302 trace_id: 1,
1303 span_id: 2,
1304 name: libdd_tinybytes::BytesString::from_static("s"),
1305 service: libdd_tinybytes::BytesString::from_static("span-svc"),
1306 start: 0,
1307 duration: 1,
1308 ..Default::default()
1309 };
1310 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1311 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1312 let kv = s
1313 .attributes
1314 .iter()
1315 .find(|a| a.key == "service.name")
1316 .expect("service.name attribute not found");
1317 match kv.value.as_ref().unwrap().value {
1318 Some(PV::StringValue(ref v)) => assert_eq!(v, "span-svc"),
1319 ref other => panic!("expected string, got {other:?}"),
1320 }
1321 }
1322
1323 #[test]
1324 fn unsampled_span_flags_zero() {
1325 let resource_info = OtlpResourceInfo::default();
1327 let mut span: Span<BytesData> = Span {
1328 trace_id: 1,
1329 span_id: 2,
1330 name: libdd_tinybytes::BytesString::from_static("s"),
1331 start: 0,
1332 duration: 1,
1333 ..Default::default()
1334 };
1335 span.metrics.insert("_sampling_priority_v1".into(), 0.0);
1336 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1337 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1338 assert_eq!(s.flags, 0);
1339 }
1340}