1use super::OtlpResourceInfo;
13use crate::span::v04::{Span, SpanEvent, SpanLink};
14use crate::span::TraceData;
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: String::new(),
330 version: String::new(),
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,
474 }
475}
476
477fn map_span_events<T: TraceData>(events: &[SpanEvent<T>]) -> (Vec<ProtoEvent>, usize) {
478 const MAX_EVENTS_PER_SPAN: usize = 128;
479 let mut out = Vec::with_capacity(events.len().min(MAX_EVENTS_PER_SPAN));
480 for ev in events.iter().take(MAX_EVENTS_PER_SPAN) {
481 out.push(ProtoEvent {
482 time_unix_nano: ev.time_unix_nano,
483 name: ev.name.borrow().to_string(),
484 attributes: collect_event_attributes(ev),
485 dropped_attributes_count: 0,
486 });
487 }
488 let dropped = events.len().saturating_sub(out.len());
489 (out, dropped)
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::otlp_encoder::OtlpResourceInfo;
496 use crate::span::BytesData;
497
498 #[test]
499 fn maps_native_span_to_prost_ir() {
500 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
501 let resource_info = OtlpResourceInfo::default();
502 let mut span: Span<BytesData> = Span {
503 trace_id: 0xD269B633813FC60C_u128,
504 span_id: 0xEEE19B7EC3C1B174,
505 parent_id: 0xEEE19B7EC3C1B173,
506 name: libdd_tinybytes::BytesString::from_static("op"),
507 resource: libdd_tinybytes::BytesString::from_static("res"),
508 r#type: libdd_tinybytes::BytesString::from_static("web"),
509 start: 1544712660000000000,
510 duration: 1000000000,
511 error: 1,
512 ..Default::default()
513 };
514 span.meta.insert(
515 "error.msg".into(),
516 libdd_tinybytes::BytesString::from_static("boom"),
517 );
518 span.metrics
519 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
520 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
521 let s = &req.resource_spans[0].scope_spans[0].spans[0];
522 assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
523 assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
524 assert_eq!(
525 s.parent_span_id,
526 0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
527 );
528 assert_eq!(s.name, "res");
529 assert_eq!(s.kind, 2); assert_eq!(s.start_time_unix_nano, 1544712660000000000);
531 assert_eq!(s.end_time_unix_nano, 1544712661000000000);
532 let st = s.status.as_ref().unwrap();
533 assert_eq!(st.code, 2);
534 assert_eq!(st.message, "boom");
535 let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
536 assert!(matches!(
537 count.value.as_ref().unwrap().value,
538 Some(PV::IntValue(42))
539 ));
540 }
541
542 #[test]
543 fn proto_span_uses_raw_id_bytes_and_native_timestamps() {
544 let resource_info = OtlpResourceInfo {
545 service: "svc".to_string(),
546 ..Default::default()
547 };
548 let span: Span<BytesData> = Span {
549 trace_id: 0x5b8efff798038103_d269b633813fc60c_u128,
550 span_id: 0xEEE19B7EC3C1B174,
551 parent_id: 0xEEE19B7EC3C1B173,
552 name: libdd_tinybytes::BytesString::from_static("op"),
553 resource: libdd_tinybytes::BytesString::from_static("res"),
554 r#type: libdd_tinybytes::BytesString::from_static("web"),
555 start: 1544712660000000000,
556 duration: 1000000000,
557 ..Default::default()
558 };
559 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
560 let s = &req.resource_spans[0].scope_spans[0].spans[0];
561 assert_eq!(
562 s.trace_id,
563 0x5b8efff798038103_d269b633813fc60c_u128
564 .to_be_bytes()
565 .to_vec()
566 );
567 assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
568 assert_eq!(
569 s.parent_span_id,
570 0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
571 );
572 assert_eq!(s.start_time_unix_nano, 1544712660000000000);
573 assert_eq!(s.end_time_unix_nano, 1544712661000000000);
574 assert_eq!(s.name, "res");
575 assert_eq!(s.kind, span_kind::SERVER);
576 }
577
578 #[test]
579 fn negative_start_clamps_to_zero() {
580 let resource_info = OtlpResourceInfo {
584 service: "svc".to_string(),
585 ..Default::default()
586 };
587 let span: Span<BytesData> = Span {
588 trace_id: 1,
589 span_id: 1,
590 start: -1,
591 duration: 0,
592 ..Default::default()
593 };
594 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
595 let s = &req.resource_spans[0].scope_spans[0].spans[0];
596 assert_eq!(
597 s.start_time_unix_nano, 0,
598 "negative start must clamp to 0, not wrap"
599 );
600 assert_eq!(
601 s.end_time_unix_nano, 0,
602 "negative start+duration must clamp to 0, not wrap"
603 );
604 }
605
606 #[test]
607 fn status_error_message_from_meta() {
608 let resource_info = OtlpResourceInfo::default();
609 let mut span: Span<BytesData> = Span {
610 trace_id: 1,
611 span_id: 2,
612 name: libdd_tinybytes::BytesString::from_static("err_span"),
613 start: 0,
614 duration: 1,
615 error: 1,
616 ..Default::default()
617 };
618 span.meta.insert(
619 libdd_tinybytes::BytesString::from_static("error.msg"),
620 libdd_tinybytes::BytesString::from_static("something broke"),
621 );
622 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
623 let s = &req.resource_spans[0].scope_spans[0].spans[0];
624 let status = s.status.as_ref().unwrap();
625 assert_eq!(status.code, status_code::ERROR);
626 assert_eq!(status.message, "something broke");
627 }
628
629 #[test]
630 fn metrics_as_int_or_double() {
631 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
632 let resource_info = OtlpResourceInfo::default();
633 let mut span: Span<BytesData> = Span {
634 trace_id: 1,
635 span_id: 2,
636 name: libdd_tinybytes::BytesString::from_static("m"),
637 start: 0,
638 duration: 1,
639 ..Default::default()
640 };
641 span.metrics
642 .insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
643 span.metrics.insert(
644 libdd_tinybytes::BytesString::from_static("rate"),
645 std::f64::consts::PI,
646 );
647 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
648 let s = &req.resource_spans[0].scope_spans[0].spans[0];
649 let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
650 assert!(matches!(
651 count.value.as_ref().unwrap().value,
652 Some(PV::IntValue(42))
653 ));
654 let rate = s.attributes.iter().find(|a| a.key == "rate").unwrap();
655 match rate.value.as_ref().unwrap().value {
656 Some(PV::DoubleValue(d)) => assert!((d - std::f64::consts::PI).abs() < 1e-9),
657 ref other => panic!("expected double, got {other:?}"),
658 }
659 }
660
661 #[test]
662 fn trace_id_128_from_dd_p_tid() {
663 let resource_info = OtlpResourceInfo::default();
666 let mut span: Span<BytesData> = Span {
667 trace_id: 0xD269B633813FC60C_u128, span_id: 1,
669 name: libdd_tinybytes::BytesString::from_static("s"),
670 start: 0,
671 duration: 1,
672 ..Default::default()
673 };
674 span.meta.insert(
675 "_dd.p.tid".into(),
676 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
677 );
678 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
679 let s = &req.resource_spans[0].scope_spans[0].spans[0];
680 assert_eq!(
681 s.trace_id,
682 0x5b8efff798038103_d269b633813fc60c_u128
683 .to_be_bytes()
684 .to_vec()
685 );
686 }
687
688 #[test]
689 fn trace_id_128_from_native_span_field() {
690 let resource_info = OtlpResourceInfo::default();
694 let full: u128 = 0x5b8efff798038103_d269b633813fc60c_u128;
695 let root: Span<BytesData> = Span {
696 trace_id: full,
697 span_id: 1,
698 name: libdd_tinybytes::BytesString::from_static("root"),
699 start: 0,
700 duration: 1,
701 ..Default::default()
702 };
703 let child: Span<BytesData> = Span {
705 trace_id: 0xD269B633813FC60C_u128,
706 span_id: 2,
707 parent_id: 1,
708 name: libdd_tinybytes::BytesString::from_static("child"),
709 start: 0,
710 duration: 1,
711 ..Default::default()
712 };
713 let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, false);
714 let spans = &req.resource_spans[0].scope_spans[0].spans;
715 let expected = full.to_be_bytes().to_vec();
716 assert_eq!(spans[0].trace_id, expected);
717 assert_eq!(spans[1].trace_id, expected);
718 }
719
720 #[test]
721 fn trace_id_128_without_dd_p_tid_defaults_high_to_zero() {
722 let resource_info = OtlpResourceInfo::default();
725 let span: Span<BytesData> = Span {
726 trace_id: 0xD269B633813FC60C_u128,
727 span_id: 1,
728 name: libdd_tinybytes::BytesString::from_static("s"),
729 start: 0,
730 duration: 1,
731 ..Default::default()
732 };
733 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
734 let s = &req.resource_spans[0].scope_spans[0].spans[0];
735 assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
736 }
737
738 #[test]
739 fn trace_id_128_propagated_to_chunk_children() {
740 let resource_info = OtlpResourceInfo::default();
744 let low: u128 = 0xD269B633813FC60C_u128;
745 let mut root: Span<BytesData> = Span {
746 trace_id: low,
747 span_id: 1,
748 name: libdd_tinybytes::BytesString::from_static("root"),
749 start: 0,
750 duration: 1,
751 ..Default::default()
752 };
753 root.meta.insert(
754 "_dd.p.tid".into(),
755 libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
756 );
757 let child_a: Span<BytesData> = Span {
758 trace_id: low,
759 span_id: 2,
760 parent_id: 1,
761 name: libdd_tinybytes::BytesString::from_static("child_a"),
762 start: 0,
763 duration: 1,
764 ..Default::default()
765 };
766 let child_b: Span<BytesData> = Span {
767 trace_id: low,
768 span_id: 3,
769 parent_id: 1,
770 name: libdd_tinybytes::BytesString::from_static("child_b"),
771 start: 0,
772 duration: 1,
773 ..Default::default()
774 };
775 let req = map_traces_to_otlp(vec![vec![root, child_a, child_b]], &resource_info, false);
776 let spans = &req.resource_spans[0].scope_spans[0].spans;
777 assert_eq!(spans.len(), 3);
778 let expected = 0x5b8efff798038103_d269b633813fc60c_u128
779 .to_be_bytes()
780 .to_vec();
781 for s in spans {
782 assert_eq!(s.trace_id, expected);
783 }
784 }
785
786 #[test]
787 fn trace_id_128_isolation_across_chunks() {
788 let resource_info = OtlpResourceInfo::default();
791 let low_a: u128 = 0x1111111111111111_u128;
792 let low_b: u128 = 0x2222222222222222_u128;
793 let mut root_a: Span<BytesData> = Span {
794 trace_id: low_a,
795 span_id: 1,
796 name: libdd_tinybytes::BytesString::from_static("root_a"),
797 start: 0,
798 duration: 1,
799 ..Default::default()
800 };
801 root_a.meta.insert(
802 "_dd.p.tid".into(),
803 libdd_tinybytes::BytesString::from_static("aaaaaaaaaaaaaaaa"),
804 );
805 let child_a: Span<BytesData> = Span {
806 trace_id: low_a,
807 span_id: 2,
808 parent_id: 1,
809 name: libdd_tinybytes::BytesString::from_static("child_a"),
810 start: 0,
811 duration: 1,
812 ..Default::default()
813 };
814 let mut root_b: Span<BytesData> = Span {
815 trace_id: low_b,
816 span_id: 3,
817 name: libdd_tinybytes::BytesString::from_static("root_b"),
818 start: 0,
819 duration: 1,
820 ..Default::default()
821 };
822 root_b.meta.insert(
823 "_dd.p.tid".into(),
824 libdd_tinybytes::BytesString::from_static("bbbbbbbbbbbbbbbb"),
825 );
826 let child_b: Span<BytesData> = Span {
827 trace_id: low_b,
828 span_id: 4,
829 parent_id: 3,
830 name: libdd_tinybytes::BytesString::from_static("child_b"),
831 start: 0,
832 duration: 1,
833 ..Default::default()
834 };
835 let req = map_traces_to_otlp(
836 vec![vec![root_a, child_a], vec![root_b, child_b]],
837 &resource_info,
838 false,
839 );
840 let spans = &req.resource_spans[0].scope_spans[0].spans;
841 assert_eq!(spans.len(), 4);
842 let expect_a = 0xaaaaaaaaaaaaaaaa_1111111111111111_u128
843 .to_be_bytes()
844 .to_vec();
845 let expect_b = 0xbbbbbbbbbbbbbbbb_2222222222222222_u128
846 .to_be_bytes()
847 .to_vec();
848 assert_eq!(spans[0].trace_id, expect_a);
849 assert_eq!(spans[1].trace_id, expect_a);
850 assert_eq!(spans[2].trace_id, expect_b);
851 assert_eq!(spans[3].trace_id, expect_b);
852 }
853
854 #[test]
855 fn chunk_with_malformed_dd_p_tid_on_root_falls_back() {
856 let resource_info = OtlpResourceInfo::default();
860 let low: u128 = 0xD269B633813FC60C_u128;
861 let mut root: Span<BytesData> = Span {
862 trace_id: low,
863 span_id: 1,
864 name: libdd_tinybytes::BytesString::from_static("root"),
865 start: 0,
866 duration: 1,
867 ..Default::default()
868 };
869 root.meta.insert(
870 "_dd.p.tid".into(),
871 libdd_tinybytes::BytesString::from_static("not-hex"),
872 );
873 let child_no_tag: Span<BytesData> = Span {
874 trace_id: low,
875 span_id: 2,
876 parent_id: 1,
877 name: libdd_tinybytes::BytesString::from_static("child_no_tag"),
878 start: 0,
879 duration: 1,
880 ..Default::default()
881 };
882 let mut child_valid: Span<BytesData> = Span {
883 trace_id: low,
884 span_id: 3,
885 parent_id: 1,
886 name: libdd_tinybytes::BytesString::from_static("child_valid"),
887 start: 0,
888 duration: 1,
889 ..Default::default()
890 };
891 child_valid.meta.insert(
892 "_dd.p.tid".into(),
893 libdd_tinybytes::BytesString::from_static("dddddddddddddddd"),
894 );
895 let req = map_traces_to_otlp(
896 vec![vec![root, child_no_tag, child_valid]],
897 &resource_info,
898 false,
899 );
900 let spans = &req.resource_spans[0].scope_spans[0].spans;
901 let expected = 0xdddddddddddddddd_d269b633813fc60c_u128
904 .to_be_bytes()
905 .to_vec();
906 assert_eq!(spans[0].trace_id, expected);
907 assert_eq!(spans[1].trace_id, expected);
908 assert_eq!(spans[2].trace_id, expected);
909 }
910
911 #[test]
912 fn test_stats_computed_resource_attr_set_when_enabled() {
913 let resource_info = OtlpResourceInfo {
914 client_computed_stats: true,
915 ..Default::default()
916 };
917 let span: Span<BytesData> = Span {
918 trace_id: 1,
919 span_id: 2,
920 name: libdd_tinybytes::BytesString::from_static("s"),
921 start: 0,
922 duration: 1,
923 ..Default::default()
924 };
925 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
926 let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
927 let kv = resource_attrs
928 .iter()
929 .find(|a| a.key == "_dd.stats_computed")
930 .expect("_dd.stats_computed must be present when client_computed_stats=true");
931 let val = match kv.value.as_ref().and_then(|v| v.value.as_ref()) {
932 Some(ProtoValue::StringValue(s)) => s.as_str(),
933 other => panic!("expected stringValue, got {other:?}"),
934 };
935 assert_eq!(val, "true");
936 }
937
938 #[test]
939 fn test_stats_computed_resource_attr_absent_when_disabled() {
940 let resource_info = OtlpResourceInfo {
941 client_computed_stats: false,
942 ..Default::default()
943 };
944 let span: Span<BytesData> = Span {
945 trace_id: 1,
946 span_id: 2,
947 name: libdd_tinybytes::BytesString::from_static("s"),
948 start: 0,
949 duration: 1,
950 ..Default::default()
951 };
952 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
953 let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
954 assert!(
955 !resource_attrs.iter().any(|a| a.key == "_dd.stats_computed"),
956 "_dd.stats_computed must not be emitted when client_computed_stats=false"
957 );
958 }
959
960 #[test]
961 fn span_link_flags_are_carried() {
962 let mut span: Span<BytesData> = Span {
965 trace_id: 1,
966 span_id: 2,
967 name: libdd_tinybytes::BytesString::from_static("s"),
968 start: 0,
969 duration: 1,
970 ..Default::default()
971 };
972 span.span_links.push(SpanLink {
973 trace_id: 0x11,
974 span_id: 0x22,
975 flags: 1,
976 ..Default::default()
977 });
978 let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
979 let link = &req.resource_spans[0].scope_spans[0].spans[0].links[0];
980 assert_eq!(
981 link.flags, 1,
982 "OTLP Link.flags must carry the span link's flags"
983 );
984 }
985
986 #[test]
987 fn test_otel_trace_semantics_enabled() {
988 let resource_info = OtlpResourceInfo {
992 service: "resource-svc".to_string(),
993 ..Default::default()
994 };
995 let mut span: Span<BytesData> = Span {
996 trace_id: 1,
997 span_id: 2,
998 name: libdd_tinybytes::BytesString::from_static("http.request"),
999 service: libdd_tinybytes::BytesString::from_static("span-svc"),
1000 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
1001 r#type: libdd_tinybytes::BytesString::from_static("web"),
1002 start: 0,
1003 duration: 1,
1004 ..Default::default()
1005 };
1006 span.meta.insert(
1007 libdd_tinybytes::BytesString::from_static("span.kind"),
1008 libdd_tinybytes::BytesString::from_static("client"),
1009 );
1010 span.meta.insert(
1011 libdd_tinybytes::BytesString::from_static("http.method"),
1012 libdd_tinybytes::BytesString::from_static("GET"),
1013 );
1014 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
1015 let attrs = &req.resource_spans[0].scope_spans[0].spans[0].attributes;
1016 let keys: Vec<&str> = attrs.iter().map(|kv| kv.key.as_str()).collect();
1017 for omitted in [
1018 "service.name",
1019 "operation.name",
1020 "resource.name",
1021 "span.type",
1022 "span.kind",
1023 ] {
1024 assert!(
1025 !keys.contains(&omitted),
1026 "OTel-semantics must omit {omitted}"
1027 );
1028 }
1029 assert!(
1030 keys.contains(&"http.method"),
1031 "OTel-standard meta tags must remain"
1032 );
1033 }
1034
1035 #[test]
1036 fn error_message_promoted_to_status_under_otel_semantics() {
1037 let resource_info = OtlpResourceInfo::default();
1041 let mut span: Span<BytesData> = Span {
1042 trace_id: 1,
1043 span_id: 2,
1044 name: libdd_tinybytes::BytesString::from_static("op"),
1045 start: 0,
1046 duration: 1,
1047 error: 1,
1048 ..Default::default()
1049 };
1050 span.meta.insert(
1051 libdd_tinybytes::BytesString::from_static("error.message"),
1052 libdd_tinybytes::BytesString::from_static("boom"),
1053 );
1054 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
1055 let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
1056 let status = otlp_span
1057 .status
1058 .as_ref()
1059 .expect("status present on error span");
1060 assert_eq!(status.code, status_code::ERROR);
1061 assert_eq!(
1062 status.message, "boom",
1063 "error.message must be promoted to the OTLP Status message"
1064 );
1065 assert!(
1066 !otlp_span
1067 .attributes
1068 .iter()
1069 .any(|kv| kv.key == "error.message"),
1070 "error.message compat attr must be omitted under OTel-semantics"
1071 );
1072 }
1073
1074 #[test]
1075 fn empty_chunk_does_not_panic() {
1076 let resource_info = OtlpResourceInfo::default();
1078 let empty: Vec<Vec<Span<BytesData>>> = vec![vec![]];
1079 let req = map_traces_to_otlp(empty, &resource_info, false);
1080 let spans = &req.resource_spans[0].scope_spans[0].spans;
1081 assert!(spans.is_empty());
1082 }
1083
1084 #[test]
1085 fn tracestate_from_meta() {
1086 let resource_info = OtlpResourceInfo::default();
1087 let mut span: Span<BytesData> = Span {
1088 trace_id: 1,
1089 span_id: 2,
1090 name: libdd_tinybytes::BytesString::from_static("s"),
1091 start: 0,
1092 duration: 1,
1093 ..Default::default()
1094 };
1095 span.meta.insert(
1096 "tracestate".into(),
1097 libdd_tinybytes::BytesString::from_static("vendor1=abc,rojo=00f067"),
1098 );
1099 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1100 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1101 assert_eq!(s.trace_state, "vendor1=abc,rojo=00f067");
1102 }
1103
1104 #[test]
1105 fn meta_struct_as_bytes_value() {
1106 use libdd_tinybytes::Bytes;
1107 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1108 let resource_info = OtlpResourceInfo::default();
1109 let mut span: Span<BytesData> = Span {
1110 trace_id: 1,
1111 span_id: 2,
1112 name: libdd_tinybytes::BytesString::from_static("s"),
1113 start: 0,
1114 duration: 1,
1115 ..Default::default()
1116 };
1117 span.meta_struct
1118 .insert("my_key".into(), Bytes::from(vec![1u8, 2, 3]));
1119 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1120 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1121 let kv = s
1122 .attributes
1123 .iter()
1124 .find(|a| a.key == "my_key")
1125 .expect("my_key attribute not found");
1126 match kv.value.as_ref().unwrap().value {
1127 Some(PV::BytesValue(ref b)) => assert_eq!(b, &vec![1u8, 2, 3]),
1128 ref other => panic!("expected bytes, got {other:?}"),
1129 }
1130 }
1131
1132 #[test]
1133 fn operation_name_attribute() {
1134 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1135 let resource_info = OtlpResourceInfo::default();
1136 let span: Span<BytesData> = Span {
1137 trace_id: 1,
1138 span_id: 2,
1139 name: libdd_tinybytes::BytesString::from_static("my.operation"),
1140 start: 0,
1141 duration: 1,
1142 ..Default::default()
1143 };
1144 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1145 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1146 let kv = s
1147 .attributes
1148 .iter()
1149 .find(|a| a.key == "operation.name")
1150 .expect("operation.name attribute not found");
1151 match kv.value.as_ref().unwrap().value {
1152 Some(PV::StringValue(ref v)) => assert_eq!(v, "my.operation"),
1153 ref other => panic!("expected string, got {other:?}"),
1154 }
1155 }
1156
1157 #[test]
1158 fn span_type_attribute() {
1159 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1160 let resource_info = OtlpResourceInfo::default();
1161 let span: Span<BytesData> = Span {
1162 trace_id: 1,
1163 span_id: 2,
1164 name: libdd_tinybytes::BytesString::from_static("s"),
1165 r#type: libdd_tinybytes::BytesString::from_static("grpc"),
1166 start: 0,
1167 duration: 1,
1168 ..Default::default()
1169 };
1170 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1171 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1172 let kv = s
1173 .attributes
1174 .iter()
1175 .find(|a| a.key == "span.type")
1176 .expect("span.type attribute not found");
1177 match kv.value.as_ref().unwrap().value {
1178 Some(PV::StringValue(ref v)) => assert_eq!(v, "grpc"),
1179 ref other => panic!("expected string, got {other:?}"),
1180 }
1181 }
1182
1183 #[test]
1184 fn resource_name_attribute_and_span_name() {
1185 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1186 let resource_info = OtlpResourceInfo::default();
1187 let span: Span<BytesData> = Span {
1188 trace_id: 1,
1189 span_id: 2,
1190 name: libdd_tinybytes::BytesString::from_static("s"),
1191 resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
1192 start: 0,
1193 duration: 1,
1194 ..Default::default()
1195 };
1196 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1197 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1198 assert_eq!(s.name, "GET /api/users");
1200 let kv = s
1202 .attributes
1203 .iter()
1204 .find(|a| a.key == "resource.name")
1205 .expect("resource.name attribute not found");
1206 match kv.value.as_ref().unwrap().value {
1207 Some(PV::StringValue(ref v)) => assert_eq!(v, "GET /api/users"),
1208 ref other => panic!("expected string, got {other:?}"),
1209 }
1210 }
1211
1212 #[test]
1213 fn empty_resource_name_not_emitted() {
1214 let resource_info = OtlpResourceInfo::default();
1216 let span: Span<BytesData> = Span {
1217 trace_id: 1,
1218 span_id: 2,
1219 name: libdd_tinybytes::BytesString::from_static("s"),
1220 start: 0,
1222 duration: 1,
1223 ..Default::default()
1224 };
1225 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1226 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1227 assert!(
1228 !s.attributes.iter().any(|a| a.key == "resource.name"),
1229 "resource.name should not be emitted when resource is empty"
1230 );
1231 }
1232
1233 #[test]
1234 fn per_span_service_name_attribute() {
1235 use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
1236 let resource_info = OtlpResourceInfo {
1239 service: "resource-svc".to_string(),
1240 ..Default::default()
1241 };
1242 let span: Span<BytesData> = Span {
1243 trace_id: 1,
1244 span_id: 2,
1245 name: libdd_tinybytes::BytesString::from_static("s"),
1246 service: libdd_tinybytes::BytesString::from_static("span-svc"),
1247 start: 0,
1248 duration: 1,
1249 ..Default::default()
1250 };
1251 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1252 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1253 let kv = s
1254 .attributes
1255 .iter()
1256 .find(|a| a.key == "service.name")
1257 .expect("service.name attribute not found");
1258 match kv.value.as_ref().unwrap().value {
1259 Some(PV::StringValue(ref v)) => assert_eq!(v, "span-svc"),
1260 ref other => panic!("expected string, got {other:?}"),
1261 }
1262 }
1263
1264 #[test]
1265 fn unsampled_span_flags_zero() {
1266 let resource_info = OtlpResourceInfo::default();
1268 let mut span: Span<BytesData> = Span {
1269 trace_id: 1,
1270 span_id: 2,
1271 name: libdd_tinybytes::BytesString::from_static("s"),
1272 start: 0,
1273 duration: 1,
1274 ..Default::default()
1275 };
1276 span.metrics.insert("_sampling_priority_v1".into(), 0.0);
1277 let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
1278 let s = &req.resource_spans[0].scope_spans[0].spans[0];
1279 assert_eq!(s.flags, 0);
1280 }
1281}