1mod span_v04;
5mod span_v1;
6
7use crate::span::v04::Span;
8use crate::span::v1::TracerPayload;
9use crate::span::TraceData;
10use crate::tracer_metadata::TracerMetadata;
11use libdd_common::ResultInfallibleExt;
12use rmp::encode::{
13 write_array_len, write_bin, write_map_len, write_sint, write_str, write_uint, write_uint8,
14 ByteBuf, RmpWrite, ValueWriteError,
15};
16use std::borrow::Borrow;
17use std::collections::HashMap;
18
19mod trace_key {
21 pub const CONTAINER_ID: u8 = 2;
22 pub const LANGUAGE_NAME: u8 = 3;
23 pub const LANGUAGE_VERSION: u8 = 4;
24 pub const TRACER_VERSION: u8 = 5;
25 pub const RUNTIME_ID: u8 = 6;
26 pub const ENV_REF: u8 = 7;
27 pub const HOSTNAME_REF: u8 = 8;
28 pub const APP_VERSION_REF: u8 = 9;
29 pub const ATTRIBUTES: u8 = 10;
31 pub const CHUNKS: u8 = 11;
32}
33
34mod chunk_key {
36 pub const PRIORITY: u8 = 1;
37 pub const ORIGIN: u8 = 2;
38 pub const ATTRIBUTES: u8 = 3;
39 pub const SPANS: u8 = 4;
40 pub const DROPPED_TRACE: u8 = 5;
41 pub const TRACE_ID: u8 = 6;
42 pub const SAMPLING_MECHANISM: u8 = 7;
44}
45
46#[repr(u8)]
48pub(super) enum SpanKey {
49 Service = 1,
50 Name = 2,
51 Resource = 3,
52 SpanId = 4,
53 ParentId = 5,
54 Start = 6,
55 Duration = 7,
56 Error = 8,
57 Attributes = 9,
58 Type = 10,
59 SpanLinks = 11,
60 SpanEvents = 12,
61 Env = 13,
62 Version = 14,
63 Component = 15,
64 Kind = 16,
65}
66
67#[repr(u8)]
69pub(super) enum SpanLinkKey {
70 TraceId = 1,
71 SpanId = 2,
72 Attributes = 3,
73 TraceState = 4,
74 Flags = 5,
75}
76
77#[repr(u8)]
79pub(super) enum SpanEventKey {
80 Time = 1,
81 Name = 2,
82 Attributes = 3,
83}
84
85#[repr(u8)]
88pub(super) enum AnyValueKey {
89 String = 1,
90 Bool = 2,
91 Double = 3,
92 Int64 = 4,
93 Bytes = 5,
94 Array = 6,
95 KeyValueList = 7,
96}
97
98pub(super) const TYPED_VALUE_STRIDE: u32 = 2;
101
102pub(super) const FLAT_ATTR_STRIDE: u32 = 3;
106
107pub(crate) struct StringTable {
117 seen: HashMap<String, u32>,
118}
119
120impl StringTable {
121 fn new() -> Self {
122 let mut seen = HashMap::new();
123 seen.insert(String::new(), 0);
124 Self { seen }
125 }
126
127 pub(crate) fn write_interned<W: RmpWrite, S: AsRef<str>>(
132 &mut self,
133 writer: &mut W,
134 s: S,
135 ) -> Result<(), ValueWriteError<W::Error>> {
136 let s = s.as_ref();
137 if let Some(&id) = self.seen.get(s) {
138 write_uint(writer, id as u64)?;
139 } else {
140 let id = self.seen.len() as u32;
141 self.seen.insert(s.to_string(), id);
142 write_str(writer, s)?;
143 }
144 Ok(())
145 }
146}
147
148pub(super) fn normalize_span_start(start: i64) -> u64 {
153 if start < 0 {
154 std::time::SystemTime::now()
155 .duration_since(std::time::UNIX_EPOCH)
156 .map(|d| d.as_nanos() as u64)
157 .unwrap_or(0)
158 } else {
159 start as u64
160 }
161}
162
163struct PayloadAttrs<'a> {
165 env: Option<&'a str>,
166 hostname: Option<&'a str>,
167 app_version: Option<&'a str>,
168 apm_mode: Option<&'a str>,
170 git_commit_sha: Option<&'a str>,
172}
173
174fn extract_payload_attrs<'a, T: TraceData + 'a, S: AsRef<[Span<T>]>>(
175 traces: &'a [S],
176 metadata: &'a TracerMetadata,
177) -> PayloadAttrs<'a>
178where
179 T::Text: 'a,
180{
181 let mut env = (!metadata.env.is_empty()).then_some(metadata.env.as_str());
185 let mut hostname = (!metadata.hostname.is_empty()).then_some(metadata.hostname.as_str());
186 let mut app_version =
187 (!metadata.app_version.is_empty()).then_some(metadata.app_version.as_str());
188 let mut git_commit_sha =
189 (!metadata.git_commit_sha.is_empty()).then_some(metadata.git_commit_sha.as_str());
190 let mut apm_mode = None;
191
192 'outer: for trace in traces {
193 for span in trace.as_ref() {
194 if env.is_none() {
195 env = span.meta.get("env").map(|v| v.borrow());
196 }
197 if hostname.is_none() {
198 hostname = span.meta.get("_dd.hostname").map(|v| v.borrow());
199 }
200 if app_version.is_none() {
201 app_version = span.meta.get("version").map(|v| v.borrow());
202 }
203 if apm_mode.is_none() {
204 apm_mode = span.meta.get("_dd.apm_mode").map(|v| v.borrow());
205 }
206 if git_commit_sha.is_none() {
207 git_commit_sha = span.meta.get("_dd.git.commit.sha").map(|v| v.borrow());
208 }
209 if env.is_some()
210 && hostname.is_some()
211 && app_version.is_some()
212 && apm_mode.is_some()
213 && git_commit_sha.is_some()
214 {
215 break 'outer;
216 }
217 }
218 }
219
220 PayloadAttrs {
221 env,
222 hostname,
223 app_version,
224 apm_mode,
225 git_commit_sha,
226 }
227}
228
229struct ChunkAttrs<'a> {
231 trace_id: u128,
233 sampling_priority: Option<i32>,
235 origin: Option<&'a str>,
237 sampling_mechanism: Option<u32>,
239}
240
241fn extract_chunk_attrs<'a, T: TraceData>(spans: &'a [Span<T>]) -> ChunkAttrs<'a>
242where
243 T::Text: 'a,
244{
245 let trace_id = spans
248 .first()
249 .map(|s| {
250 let high = s
251 .meta
252 .get("_dd.p.tid")
253 .and_then(|v| u64::from_str_radix(v.borrow(), 16).ok())
254 .unwrap_or(0);
255 ((high as u128) << 64) | s.trace_id
256 })
257 .unwrap_or(0);
258
259 let mut sampling_priority = None;
260 let mut origin = None;
261 let mut sampling_mechanism = None;
262
263 for span in spans {
264 let is_root =
266 span.parent_id == 0 || span.metrics.get("_dd.top_level").copied().unwrap_or(0.0) == 1.0;
267
268 if is_root {
269 sampling_priority = span.metrics.get("_sampling_priority_v1").map(|v| *v as i32);
272 origin = span.meta.get("_dd.origin").map(|v| v.borrow());
273 sampling_mechanism = span
276 .meta
277 .get("_dd.p.dm")
278 .and_then(|v| v.borrow().parse::<i32>().ok())
279 .map(|dm| dm.unsigned_abs());
280 break;
281 }
282
283 if sampling_priority.is_none() {
286 sampling_priority = span.metrics.get("_sampling_priority_v1").map(|v| *v as i32);
287 }
288 if origin.is_none() {
289 origin = span.meta.get("_dd.origin").map(|v| v.borrow());
290 }
291 if sampling_mechanism.is_none() {
292 sampling_mechanism = span
293 .meta
294 .get("_dd.p.dm")
295 .and_then(|v| v.borrow().parse::<i32>().ok())
296 .map(|dm| dm.unsigned_abs());
297 }
298 }
299
300 ChunkAttrs {
301 trace_id,
302 sampling_priority,
303 origin,
304 sampling_mechanism,
305 }
306}
307
308fn encode_payload_from_v04<W: RmpWrite, T: TraceData, S: AsRef<[Span<T>]>>(
322 writer: &mut W,
323 traces: &[S],
324 metadata: &TracerMetadata,
325) -> Result<(), ValueWriteError<W::Error>> {
326 let mut table = StringTable::new();
327 let payload_attrs = extract_payload_attrs(traces, metadata);
328
329 let attr_count =
330 payload_attrs.apm_mode.is_some() as u32 + payload_attrs.git_commit_sha.is_some() as u32;
331 let has_attributes = attr_count > 0;
332
333 let map_len = 1u32 + (!metadata.language.is_empty()) as u32
335 + (!metadata.language_version.is_empty()) as u32
336 + (!metadata.tracer_version.is_empty()) as u32
337 + (!metadata.runtime_id.is_empty()) as u32
338 + (!metadata.container_id.is_empty()) as u32
339 + payload_attrs.env.is_some() as u32
340 + payload_attrs.hostname.is_some() as u32
341 + payload_attrs.app_version.is_some() as u32
342 + has_attributes as u32;
343
344 write_map_len(writer, map_len)?;
345
346 write_uint8(writer, trace_key::CHUNKS)?;
347 write_array_len(writer, traces.len() as u32)?;
348 for trace in traces {
349 encode_chunk_from_v04(writer, trace.as_ref(), &mut table)?;
350 }
351
352 if !metadata.language.is_empty() {
353 write_uint8(writer, trace_key::LANGUAGE_NAME)?;
354 table.write_interned(writer, &metadata.language)?;
355 }
356
357 if !metadata.language_version.is_empty() {
358 write_uint8(writer, trace_key::LANGUAGE_VERSION)?;
359 table.write_interned(writer, &metadata.language_version)?;
360 }
361
362 if !metadata.tracer_version.is_empty() {
363 write_uint8(writer, trace_key::TRACER_VERSION)?;
364 table.write_interned(writer, &metadata.tracer_version)?;
365 }
366
367 if !metadata.runtime_id.is_empty() {
368 write_uint8(writer, trace_key::RUNTIME_ID)?;
369 table.write_interned(writer, &metadata.runtime_id)?;
370 }
371
372 if !metadata.container_id.is_empty() {
373 write_uint8(writer, trace_key::CONTAINER_ID)?;
374 table.write_interned(writer, &metadata.container_id)?;
375 }
376
377 if let Some(env) = payload_attrs.env {
378 write_uint8(writer, trace_key::ENV_REF)?;
379 table.write_interned(writer, env)?;
380 }
381
382 if let Some(hostname) = payload_attrs.hostname {
383 write_uint8(writer, trace_key::HOSTNAME_REF)?;
384 table.write_interned(writer, hostname)?;
385 }
386
387 if let Some(app_version) = payload_attrs.app_version {
388 write_uint8(writer, trace_key::APP_VERSION_REF)?;
389 table.write_interned(writer, app_version)?;
390 }
391
392 if has_attributes {
393 write_uint8(writer, trace_key::ATTRIBUTES)?;
396 write_array_len(writer, attr_count * FLAT_ATTR_STRIDE)?;
397 if let Some(v) = payload_attrs.apm_mode {
398 table.write_interned(writer, "_dd.apm_mode")?;
399 write_uint8(writer, AnyValueKey::String as u8)?;
400 table.write_interned(writer, v)?;
401 }
402 if let Some(v) = payload_attrs.git_commit_sha {
403 table.write_interned(writer, "_dd.git.commit.sha")?;
404 write_uint8(writer, AnyValueKey::String as u8)?;
405 table.write_interned(writer, v)?;
406 }
407 }
408
409 Ok(())
410}
411
412fn encode_chunk_from_v04<W: RmpWrite, T: TraceData>(
424 writer: &mut W,
425 spans: &[Span<T>],
426 table: &mut StringTable,
427) -> Result<(), ValueWriteError<W::Error>> {
428 let attrs = extract_chunk_attrs(spans);
429
430 let fields = 2u32 + attrs.origin.is_some() as u32
432 + attrs.sampling_priority.is_some() as u32
433 + attrs.sampling_mechanism.is_some() as u32;
434
435 write_map_len(writer, fields)?;
436
437 write_uint8(writer, chunk_key::TRACE_ID)?;
438 write_bin(writer, &attrs.trace_id.to_be_bytes())?;
439
440 write_uint8(writer, chunk_key::SPANS)?;
441 write_array_len(writer, spans.len() as u32)?;
442 for span in spans {
443 span_v04::encode_span(writer, span, table)?;
444 }
445
446 if let Some(origin) = attrs.origin {
447 write_uint8(writer, chunk_key::ORIGIN)?;
448 table.write_interned(writer, origin)?;
449 }
450
451 if let Some(priority) = attrs.sampling_priority {
452 write_uint8(writer, chunk_key::PRIORITY)?;
453 write_sint(writer, priority as i64)?;
454 }
455
456 if let Some(mechanism) = attrs.sampling_mechanism {
457 write_uint8(writer, chunk_key::SAMPLING_MECHANISM)?;
458 write_uint(writer, mechanism as u64)?;
459 }
460
461 Ok(())
462}
463
464pub fn write_to_slice_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
469 slice: &mut &mut [u8],
471 traces: &[S],
472 metadata: &TracerMetadata,
473) -> Result<(), ValueWriteError> {
474 encode_payload_from_v04(slice, traces, metadata)
475}
476
477pub fn to_vec_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
479 traces: &[S],
480 metadata: &TracerMetadata,
481) -> Vec<u8> {
482 to_vec_with_capacity_from_v04(traces, 0, metadata)
483}
484
485pub fn to_vec_with_capacity_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
487 traces: &[S],
488 capacity: u32,
489 metadata: &TracerMetadata,
490) -> Vec<u8> {
491 let mut buf = ByteBuf::with_capacity(capacity as usize);
492 encode_payload_from_v04(&mut buf, traces, metadata)
493 .map_err(super::flatten_value_write_infallible)
494 .unwrap_infallible();
495 buf.into_vec()
496}
497
498pub fn to_encoded_byte_len_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
500 traces: &[S],
501 metadata: &TracerMetadata,
502) -> u32 {
503 let mut counter = super::CountLength(0);
504 let _ = encode_payload_from_v04(&mut counter, traces, metadata);
510 counter.0
511}
512
513fn encode_payload_from_v1<W: RmpWrite, T: TraceData>(
515 writer: &mut W,
516 payload: &TracerPayload<T>,
517) -> Result<(), ValueWriteError<W::Error>> {
518 let mut table = StringTable::new();
519
520 let has_attributes = !payload.attributes.is_empty();
521
522 let map_len = 1u32 + (!payload.container_id.borrow().is_empty()) as u32
524 + (!payload.language_name.borrow().is_empty()) as u32
525 + (!payload.language_version.borrow().is_empty()) as u32
526 + (!payload.tracer_version.borrow().is_empty()) as u32
527 + (!payload.runtime_id.borrow().is_empty()) as u32
528 + (!payload.env.borrow().is_empty()) as u32
529 + (!payload.hostname.borrow().is_empty()) as u32
530 + (!payload.app_version.borrow().is_empty()) as u32
531 + has_attributes as u32;
532
533 write_map_len(writer, map_len)?;
534
535 write_uint8(writer, trace_key::CHUNKS)?;
536 write_array_len(writer, payload.chunks.len() as u32)?;
537 for chunk in &payload.chunks {
538 encode_chunk_from_v1(writer, chunk, &mut table)?;
539 }
540
541 if !payload.container_id.borrow().is_empty() {
542 write_uint8(writer, trace_key::CONTAINER_ID)?;
543 table.write_interned(writer, payload.container_id.borrow())?;
544 }
545
546 if !payload.language_name.borrow().is_empty() {
547 write_uint8(writer, trace_key::LANGUAGE_NAME)?;
548 table.write_interned(writer, payload.language_name.borrow())?;
549 }
550
551 if !payload.language_version.borrow().is_empty() {
552 write_uint8(writer, trace_key::LANGUAGE_VERSION)?;
553 table.write_interned(writer, payload.language_version.borrow())?;
554 }
555
556 if !payload.tracer_version.borrow().is_empty() {
557 write_uint8(writer, trace_key::TRACER_VERSION)?;
558 table.write_interned(writer, payload.tracer_version.borrow())?;
559 }
560
561 if !payload.runtime_id.borrow().is_empty() {
562 write_uint8(writer, trace_key::RUNTIME_ID)?;
563 table.write_interned(writer, payload.runtime_id.borrow())?;
564 }
565
566 if !payload.env.borrow().is_empty() {
567 write_uint8(writer, trace_key::ENV_REF)?;
568 table.write_interned(writer, payload.env.borrow())?;
569 }
570
571 if !payload.hostname.borrow().is_empty() {
572 write_uint8(writer, trace_key::HOSTNAME_REF)?;
573 table.write_interned(writer, payload.hostname.borrow())?;
574 }
575
576 if !payload.app_version.borrow().is_empty() {
577 write_uint8(writer, trace_key::APP_VERSION_REF)?;
578 table.write_interned(writer, payload.app_version.borrow())?;
579 }
580
581 if has_attributes {
582 write_uint8(writer, trace_key::ATTRIBUTES)?;
583 span_v1::encode_attributes_map(writer, &payload.attributes, &mut table)?;
584 }
585
586 Ok(())
587}
588
589fn encode_chunk_from_v1<W: RmpWrite, T: TraceData>(
591 writer: &mut W,
592 chunk: &crate::span::v1::TraceChunk<T>,
593 table: &mut StringTable,
594) -> Result<(), ValueWriteError<W::Error>> {
595 let origin = <T::Text as Borrow<str>>::borrow(&chunk.origin);
596 let has_attributes = !chunk.attributes.is_empty();
597 let has_dropped = chunk.dropped_trace;
598
599 let fields = 2u32 + !origin.is_empty() as u32
601 + chunk.priority.is_some() as u32
602 + chunk.sampling_mechanism.is_some() as u32
603 + has_attributes as u32
604 + has_dropped as u32;
605
606 write_map_len(writer, fields)?;
607
608 write_uint8(writer, chunk_key::TRACE_ID)?;
609 write_bin(writer, &chunk.trace_id)?;
610
611 write_uint8(writer, chunk_key::SPANS)?;
612 write_array_len(writer, chunk.spans.len() as u32)?;
613 for span in &chunk.spans {
614 span_v1::encode_span(writer, span, table)?;
615 }
616
617 if !origin.is_empty() {
618 write_uint8(writer, chunk_key::ORIGIN)?;
619 table.write_interned(writer, origin)?;
620 }
621
622 if let Some(priority) = chunk.priority {
623 write_uint8(writer, chunk_key::PRIORITY)?;
624 write_sint(writer, priority as i64)?;
625 }
626
627 if let Some(mechanism) = chunk.sampling_mechanism {
628 write_uint8(writer, chunk_key::SAMPLING_MECHANISM)?;
629 write_uint(writer, mechanism as u64)?;
630 }
631
632 if has_attributes {
633 write_uint8(writer, chunk_key::ATTRIBUTES)?;
634 span_v1::encode_attributes_map(writer, &chunk.attributes, table)?;
635 }
636
637 if has_dropped {
638 write_uint8(writer, chunk_key::DROPPED_TRACE)?;
639 rmp::encode::write_bool(writer, true).map_err(ValueWriteError::InvalidDataWrite)?;
640 }
641
642 Ok(())
643}
644
645pub fn to_vec_from_v1<T: TraceData>(payload: &TracerPayload<T>) -> Vec<u8> {
670 to_vec_with_capacity_from_v1(payload, 0)
671}
672
673pub fn to_vec_with_capacity_from_v1<T: TraceData>(
699 payload: &TracerPayload<T>,
700 capacity: u32,
701) -> Vec<u8> {
702 let mut buf = ByteBuf::with_capacity(capacity as usize);
703 encode_payload_from_v1(&mut buf, payload)
704 .map_err(super::flatten_value_write_infallible)
705 .unwrap_infallible();
706 buf.into_vec()
707}
708
709pub fn write_to_slice_from_v1<T: TraceData>(
740 slice: &mut &mut [u8],
741 payload: &TracerPayload<T>,
742) -> Result<(), ValueWriteError> {
743 encode_payload_from_v1(slice, payload)
744}
745
746pub fn to_encoded_byte_len_from_v1<T: TraceData>(payload: &TracerPayload<T>) -> u32 {
774 let mut counter = super::CountLength(0);
775 let _ = encode_payload_from_v1(&mut counter, payload);
776 counter.0
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use crate::span::v04::SpanBytes;
783 use libdd_tinybytes::BytesString;
784
785 fn make_span(
786 service: &str,
787 name: &str,
788 trace_id: u128,
789 span_id: u64,
790 parent_id: u64,
791 ) -> SpanBytes {
792 SpanBytes {
793 service: BytesString::from_slice(service.as_bytes()).unwrap(),
794 name: BytesString::from_slice(name.as_bytes()).unwrap(),
795 resource: BytesString::from_slice(b"res").unwrap(),
796 trace_id,
797 span_id,
798 parent_id,
799 start: 1_000_000,
800 duration: 500,
801 ..Default::default()
802 }
803 }
804
805 #[test]
806 fn test_to_vec_non_empty() {
807 let spans = vec![make_span("svc", "op", 42, 1, 0)];
808 let traces = vec![spans];
809 let encoded = to_vec_from_v04(&traces, &TracerMetadata::default());
810 assert!(!encoded.is_empty());
811 }
812
813 #[test]
814 fn test_to_vec_empty_traces() {
815 let traces: Vec<Vec<SpanBytes>> = vec![];
816 let encoded = to_vec_from_v04(&traces, &TracerMetadata::default());
817 assert!(!encoded.is_empty());
819 }
820
821 #[test]
822 fn test_string_interning_reduces_size() {
823 let s1 = make_span("my-service", "op1", 1, 1, 0);
825 let s2 = make_span("my-service", "op2", 2, 2, 0);
826 let traces_two = vec![vec![s1], vec![s2]];
827
828 let s_single = make_span("my-service", "op1", 1, 1, 0);
830 let traces_single = vec![vec![s_single]];
831
832 let encoded_two = to_vec_from_v04(&traces_two, &TracerMetadata::default());
833 let encoded_single = to_vec_from_v04(&traces_single, &TracerMetadata::default());
834
835 assert!(
838 encoded_two.len() < 2 * encoded_single.len(),
839 "Interning should reduce size: two={} single={}",
840 encoded_two.len(),
841 encoded_single.len()
842 );
843 }
844
845 #[test]
846 fn test_chunk_level_attrs_origin_and_priority() {
847 let meta = vec![(
848 BytesString::from_static("_dd.origin"),
849 BytesString::from_static("lambda"),
850 )]
851 .into();
852 let metrics = vec![(BytesString::from_static("_sampling_priority_v1"), 1.0f64)].into();
853
854 let root = SpanBytes {
855 service: BytesString::from_slice(b"svc").unwrap(),
856 name: BytesString::from_slice(b"op").unwrap(),
857 resource: BytesString::from_slice(b"res").unwrap(),
858 trace_id: 99,
859 span_id: 1,
860 parent_id: 0,
861 start: 1000,
862 duration: 100,
863 meta,
864 metrics,
865 ..Default::default()
866 };
867
868 let encoded = to_vec_from_v04(&[vec![root]], &TracerMetadata::default());
869 assert!(!encoded.is_empty());
870 let lambda_bytes = b"lambda";
872 assert!(
873 encoded
874 .windows(lambda_bytes.len())
875 .any(|w| w == lambda_bytes),
876 "origin 'lambda' should appear in payload"
877 );
878 }
879
880 #[test]
881 fn test_to_encoded_byte_len_matches_to_vec() {
882 let spans = vec![
883 make_span("svc", "op", 1, 1, 0),
884 make_span("svc", "child", 1, 2, 1),
885 ];
886 let traces = vec![spans];
887 let meta = TracerMetadata::default();
888 let encoded = to_vec_from_v04(&traces, &meta);
889 let len = to_encoded_byte_len_from_v04(&traces, &meta);
890 assert_eq!(encoded.len() as u32, len);
891 }
892
893 #[test]
894 fn test_remote_parent_root_span_top_level() {
895 let metrics = vec![
897 (BytesString::from_static("_dd.top_level"), 1.0f64),
898 (BytesString::from_static("_sampling_priority_v1"), 2.0f64),
899 ]
900 .into();
901
902 let root = SpanBytes {
903 service: BytesString::from_slice(b"svc").unwrap(),
904 name: BytesString::from_slice(b"op").unwrap(),
905 resource: BytesString::from_slice(b"res").unwrap(),
906 trace_id: 123,
907 span_id: 42,
908 parent_id: 999, start: 1000,
910 duration: 100,
911 metrics,
912 ..Default::default()
913 };
914
915 let encoded = to_vec_from_v04(&[vec![root]], &TracerMetadata::default());
916 assert!(!encoded.is_empty());
917 }
918
919 #[test]
920 fn test_payload_promoted_fields() {
921 let meta = vec![
922 (
923 BytesString::from_static("env"),
924 BytesString::from_static("prod"),
925 ),
926 (
927 BytesString::from_static("version"),
928 BytesString::from_static("1.2.3"),
929 ),
930 (
931 BytesString::from_static("_dd.hostname"),
932 BytesString::from_static("my-host"),
933 ),
934 ]
935 .into();
936
937 let span = SpanBytes {
938 service: BytesString::from_slice(b"svc").unwrap(),
939 name: BytesString::from_slice(b"op").unwrap(),
940 resource: BytesString::from_slice(b"res").unwrap(),
941 trace_id: 1,
942 span_id: 1,
943 parent_id: 0,
944 start: 1000,
945 duration: 100,
946 meta,
947 ..Default::default()
948 };
949
950 let encoded = to_vec_from_v04(&[vec![span]], &TracerMetadata::default());
951 let prod_bytes = b"prod";
952 assert!(
953 encoded.windows(prod_bytes.len()).any(|w| w == prod_bytes),
954 "env 'prod' should appear in payload"
955 );
956 let host_bytes = b"my-host";
957 assert!(
958 encoded.windows(host_bytes.len()).any(|w| w == host_bytes),
959 "hostname 'my-host' should appear in payload"
960 );
961 }
962
963 #[test]
964 fn test_payload_attributes_apm_mode_and_git_commit_sha() {
965 let meta = vec![
966 (
967 BytesString::from_static("_dd.apm_mode"),
968 BytesString::from_static("ssi"),
969 ),
970 (
971 BytesString::from_static("_dd.git.commit.sha"),
972 BytesString::from_static("abc123"),
973 ),
974 ]
975 .into();
976
977 let span = SpanBytes {
978 service: BytesString::from_slice(b"svc").unwrap(),
979 name: BytesString::from_slice(b"op").unwrap(),
980 resource: BytesString::from_slice(b"res").unwrap(),
981 trace_id: 1,
982 span_id: 1,
983 parent_id: 0,
984 start: 1000,
985 duration: 100,
986 meta,
987 ..Default::default()
988 };
989
990 let encoded = to_vec_from_v04(&[vec![span]], &TracerMetadata::default());
991
992 let ssi_bytes = b"ssi";
994 assert!(
995 encoded.windows(ssi_bytes.len()).any(|w| w == ssi_bytes),
996 "apm_mode 'ssi' should appear in payload"
997 );
998 let sha_bytes = b"abc123";
999 assert!(
1000 encoded.windows(sha_bytes.len()).any(|w| w == sha_bytes),
1001 "git commit sha 'abc123' should appear in payload"
1002 );
1003 let apm_key = b"_dd.apm_mode";
1005 assert!(
1006 encoded.windows(apm_key.len()).any(|w| w == apm_key),
1007 "_dd.apm_mode key should appear in payload"
1008 );
1009 let git_key = b"_dd.git.commit.sha";
1010 assert!(
1011 encoded.windows(git_key.len()).any(|w| w == git_key),
1012 "_dd.git.commit.sha key should appear in payload"
1013 );
1014 }
1015
1016 #[test]
1017 fn test_payload_attributes_absent_when_no_relevant_tags() {
1018 let span = make_span("svc", "op", 1, 1, 0);
1020 let encoded = to_vec_from_v04(&[vec![span]], &TracerMetadata::default());
1021 let apm_key = b"_dd.apm_mode";
1022 assert!(
1023 !encoded.windows(apm_key.len()).any(|w| w == apm_key),
1024 "key 10 should be absent when no relevant tags are set"
1025 );
1026 }
1027
1028 #[test]
1029 fn test_payload_metadata_fields_present() {
1030 let span = make_span("svc", "op", 1, 1, 0);
1031 let metadata = TracerMetadata {
1032 language: "python".to_string(),
1033 language_version: "3.11".to_string(),
1034 tracer_version: "2.0.0".to_string(),
1035 runtime_id: "abc-123-uuid".to_string(),
1036 ..Default::default()
1037 };
1038 let encoded = to_vec_from_v04(&[vec![span]], &metadata);
1039
1040 for s in &[b"python" as &[u8], b"3.11", b"2.0.0", b"abc-123-uuid"] {
1041 assert!(
1042 encoded.windows(s.len()).any(|w| w == *s),
1043 "{} should appear in payload",
1044 std::str::from_utf8(s).unwrap()
1045 );
1046 }
1047 }
1048
1049 #[test]
1050 fn test_payload_metadata_absent_when_empty() {
1051 let span = make_span("svc", "op", 1, 1, 0);
1052 let encoded_with = to_vec_from_v04(
1053 &[vec![span.clone()]],
1054 &TracerMetadata {
1055 language: "go".to_string(),
1056 ..Default::default()
1057 },
1058 );
1059 let encoded_without = to_vec_from_v04(&[vec![span]], &TracerMetadata::default());
1060 assert!(encoded_with.len() > encoded_without.len());
1062 }
1063
1064 #[test]
1065 fn test_128bit_trace_id_from_dd_p_tid() {
1066 let meta = vec![(
1067 BytesString::from_static("_dd.p.tid"),
1068 BytesString::from_static("640cfd5400000000"),
1069 )]
1070 .into();
1071 let span = SpanBytes {
1072 service: BytesString::from_slice(b"svc").unwrap(),
1073 name: BytesString::from_slice(b"op").unwrap(),
1074 resource: BytesString::from_slice(b"res").unwrap(),
1075 trace_id: 0x0123456789abcdef,
1076 span_id: 1,
1077 parent_id: 0,
1078 start: 1000,
1079 duration: 100,
1080 meta,
1081 ..Default::default()
1082 };
1083 let encoded = to_vec_from_v04(&[vec![span]], &TracerMetadata::default());
1084
1085 let expected = [
1087 0x64, 0x0c, 0xfd, 0x54, 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
1088 0xcd, 0xef,
1089 ];
1090 assert!(
1091 encoded.windows(16).any(|w| w == expected),
1092 "128-bit trace_id big-endian bytes should appear in payload"
1093 );
1094 let tid_key = b"_dd.p.tid";
1096 assert!(
1097 !encoded.windows(tid_key.len()).any(|w| w == tid_key),
1098 "_dd.p.tid should be consumed, not encoded as a span attribute"
1099 );
1100 }
1101
1102 #[test]
1103 fn test_128bit_trace_id_without_dd_p_tid() {
1104 let span = make_span("svc", "op", 0x0123456789abcdef, 1, 0);
1106 let encoded = to_vec_from_v04(&[vec![span]], &TracerMetadata::default());
1107 let expected = [
1108 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
1109 0xcd, 0xef,
1110 ];
1111 assert!(
1112 encoded.windows(16).any(|w| w == expected),
1113 "absent _dd.p.tid should yield zero high 64 bits"
1114 );
1115 }
1116
1117 #[test]
1118 fn test_sampling_mechanism_negative_value() {
1119 let meta = vec![(
1122 BytesString::from_static("_dd.p.dm"),
1123 BytesString::from_static("-4"),
1124 )]
1125 .into();
1126 let root = SpanBytes {
1127 service: BytesString::from_slice(b"svc").unwrap(),
1128 name: BytesString::from_slice(b"op").unwrap(),
1129 resource: BytesString::from_slice(b"res").unwrap(),
1130 trace_id: 1,
1131 span_id: 1,
1132 parent_id: 0,
1133 start: 1000,
1134 duration: 100,
1135 meta,
1136 ..Default::default()
1137 };
1138 let encoded = to_vec_from_v04(&[vec![root]], &TracerMetadata::default());
1139
1140 let expected = [chunk_key::SAMPLING_MECHANISM, 0x04];
1144 assert!(
1145 encoded.windows(2).any(|w| w == expected),
1146 "sampling_mechanism should be encoded as unsigned_abs(\"-4\") = 4"
1147 );
1148 }
1149
1150 #[test]
1151 fn test_chunk_attrs_fallback_no_root_span() {
1152 let meta1 = vec![(
1155 BytesString::from_static("_dd.origin"),
1156 BytesString::from_static("lambda"),
1157 )]
1158 .into();
1159 let metrics2 = vec![(BytesString::from_static("_sampling_priority_v1"), 2.0f64)].into();
1160 let meta3 = vec![(
1161 BytesString::from_static("_dd.p.dm"),
1162 BytesString::from_static("-3"),
1163 )]
1164 .into();
1165
1166 let s1 = SpanBytes {
1167 service: BytesString::from_slice(b"svc").unwrap(),
1168 name: BytesString::from_slice(b"op1").unwrap(),
1169 resource: BytesString::from_slice(b"res").unwrap(),
1170 trace_id: 1,
1171 span_id: 11,
1172 parent_id: 10, start: 1000,
1174 duration: 100,
1175 meta: meta1,
1176 ..Default::default()
1177 };
1178 let s2 = SpanBytes {
1179 service: BytesString::from_slice(b"svc").unwrap(),
1180 name: BytesString::from_slice(b"op2").unwrap(),
1181 resource: BytesString::from_slice(b"res").unwrap(),
1182 trace_id: 1,
1183 span_id: 12,
1184 parent_id: 11,
1185 start: 1000,
1186 duration: 100,
1187 metrics: metrics2,
1188 ..Default::default()
1189 };
1190 let s3 = SpanBytes {
1191 service: BytesString::from_slice(b"svc").unwrap(),
1192 name: BytesString::from_slice(b"op3").unwrap(),
1193 resource: BytesString::from_slice(b"res").unwrap(),
1194 trace_id: 1,
1195 span_id: 13,
1196 parent_id: 12,
1197 start: 1000,
1198 duration: 100,
1199 meta: meta3,
1200 ..Default::default()
1201 };
1202 let encoded = to_vec_from_v04(&[vec![s1, s2, s3]], &TracerMetadata::default());
1203
1204 let lambda = b"lambda";
1207 assert!(
1208 encoded.windows(lambda.len()).any(|w| w == lambda),
1209 "origin 'lambda' from span 1 should appear in payload"
1210 );
1211 let prio = [chunk_key::PRIORITY, 0x02];
1213 assert!(
1214 encoded.windows(2).any(|w| w == prio),
1215 "sampling_priority 2 from span 2 should appear"
1216 );
1217 let mech = [chunk_key::SAMPLING_MECHANISM, 0x03];
1219 assert!(
1220 encoded.windows(2).any(|w| w == mech),
1221 "sampling_mechanism 3 from span 3 should appear"
1222 );
1223 }
1224}
1225
1226#[cfg(test)]
1227mod v1_payload_tests {
1228 use super::*;
1235 use crate::span::v1::{
1236 AttributeValue, Span as V1Span, SpanBytes as V1SpanBytes, SpanKind, TraceChunkBytes,
1237 TracerPayloadBytes,
1238 };
1239 use crate::span::vec_map::VecMap;
1240 use libdd_tinybytes::BytesString;
1241
1242 fn bs(s: &str) -> BytesString {
1243 BytesString::from_slice(s.as_bytes()).expect("test string must fit in BytesString")
1244 }
1245
1246 fn make_span(service: &str, name: &str, span_id: u64) -> V1SpanBytes {
1247 V1Span {
1248 service: bs(service),
1249 name: bs(name),
1250 resource: bs("res"),
1251 span_id,
1252 start: 1_000_000,
1253 duration: 500,
1254 ..Default::default()
1255 }
1256 }
1257
1258 fn make_chunk(spans: Vec<V1SpanBytes>, trace_id: [u8; 16]) -> TraceChunkBytes {
1259 TraceChunkBytes {
1260 trace_id,
1261 spans,
1262 ..Default::default()
1263 }
1264 }
1265
1266 #[test]
1267 fn empty_payload_is_valid_msgpack_map() {
1268 let payload = TracerPayloadBytes::default();
1269 let encoded = to_vec_from_v1(&payload);
1270 assert_eq!(encoded, vec![0x81, 0x0b, 0x90]);
1273 }
1274
1275 #[test]
1276 fn payload_byte_len_matches_to_vec() {
1277 let chunk = make_chunk(vec![make_span("svc", "op", 1)], [0u8; 16]);
1278 let payload = TracerPayloadBytes {
1279 chunks: vec![chunk],
1280 ..Default::default()
1281 };
1282 let encoded = to_vec_from_v1(&payload);
1283 let len = to_encoded_byte_len_from_v1(&payload);
1284 assert_eq!(encoded.len() as u32, len);
1285 }
1286
1287 #[test]
1288 fn span_kind_is_always_emitted_as_uint() {
1289 let chunk = make_chunk(vec![make_span("svc", "op", 1)], [0u8; 16]);
1292 let payload = TracerPayloadBytes {
1293 chunks: vec![chunk],
1294 ..Default::default()
1295 };
1296 let encoded = to_vec_from_v1(&payload);
1297 let pat = [0x10u8, 0x01u8];
1298 assert!(
1299 encoded.windows(2).any(|w| w == pat),
1300 "Kind (key=16) Internal (=1) must be emitted"
1301 );
1302 }
1303
1304 #[test]
1305 fn typed_attributes_carry_correct_type_discriminants() {
1306 let mut attrs = VecMap::new();
1307 attrs.insert(bs("k_str"), AttributeValue::String(bs("v")));
1308 let span = V1Span {
1309 service: bs("svc"),
1310 name: bs("op"),
1311 resource: bs("res"),
1312 span_id: 1,
1313 start: 1,
1314 duration: 1,
1315 attributes: attrs,
1316 ..Default::default()
1317 };
1318 let chunk = make_chunk(vec![span], [0u8; 16]);
1319 let payload = TracerPayloadBytes {
1320 chunks: vec![chunk],
1321 ..Default::default()
1322 };
1323 let encoded = to_vec_from_v1(&payload);
1324 assert!(
1326 encoded.windows(b"k_str".len()).any(|w| w == b"k_str"),
1327 "attribute key must appear"
1328 );
1329 }
1330
1331 #[test]
1332 fn bytes_attribute_uses_bin_marker() {
1333 let mut attrs = VecMap::new();
1335 attrs.insert(
1336 bs("payload"),
1337 AttributeValue::Bytes(libdd_tinybytes::Bytes::copy_from_slice(b"\xde\xad")),
1338 );
1339 let span = V1Span {
1340 service: bs("svc"),
1341 name: bs("op"),
1342 resource: bs("res"),
1343 span_id: 1,
1344 start: 1,
1345 duration: 1,
1346 attributes: attrs,
1347 ..Default::default()
1348 };
1349 let payload = TracerPayloadBytes {
1350 chunks: vec![make_chunk(vec![span], [0u8; 16])],
1351 ..Default::default()
1352 };
1353 let encoded = to_vec_from_v1(&payload);
1354 let want = [0xc4u8, 0x02, 0xde, 0xad];
1356 assert!(
1357 encoded.windows(4).any(|w| w == want),
1358 "Bytes attribute must be encoded as msgpack bin"
1359 );
1360 }
1361
1362 #[test]
1363 fn list_and_keyvalue_attributes_round_trip_through_recursion() {
1364 let mut nested = VecMap::new();
1365 nested.insert(bs("nk"), AttributeValue::Int(7));
1366 let mut attrs = VecMap::new();
1367 attrs.insert(
1368 bs("list"),
1369 AttributeValue::List(vec![
1370 AttributeValue::String(bs("a")),
1371 AttributeValue::Bool(true),
1372 ]),
1373 );
1374 attrs.insert(bs("kv"), AttributeValue::KeyValue(nested));
1375 let span = V1Span {
1376 service: bs("svc"),
1377 name: bs("op"),
1378 resource: bs("res"),
1379 span_id: 1,
1380 start: 1,
1381 duration: 1,
1382 attributes: attrs,
1383 ..Default::default()
1384 };
1385 let payload = TracerPayloadBytes {
1386 chunks: vec![make_chunk(vec![span], [0u8; 16])],
1387 ..Default::default()
1388 };
1389 let encoded = to_vec_from_v1(&payload);
1390 for s in &[b"list" as &[u8], b"kv", b"a", b"nk"] {
1392 assert!(
1393 encoded.windows(s.len()).any(|w| w == *s),
1394 "{} should appear in payload",
1395 std::str::from_utf8(s).unwrap()
1396 );
1397 }
1398 }
1399
1400 #[test]
1401 fn promoted_fields_at_payload_level() {
1402 let payload = TracerPayloadBytes {
1403 language_name: bs("python"),
1404 language_version: bs("3.11"),
1405 tracer_version: bs("2.0.0"),
1406 runtime_id: bs("rt-1"),
1407 env: bs("prod"),
1408 hostname: bs("h"),
1409 app_version: bs("1.2.3"),
1410 chunks: vec![make_chunk(vec![make_span("svc", "op", 1)], [0u8; 16])],
1411 ..Default::default()
1412 };
1413 let encoded = to_vec_from_v1(&payload);
1414 for s in &[
1415 b"python" as &[u8],
1416 b"3.11",
1417 b"2.0.0",
1418 b"rt-1",
1419 b"prod",
1420 b"1.2.3",
1421 ] {
1422 assert!(
1423 encoded.windows(s.len()).any(|w| w == *s),
1424 "{} should appear",
1425 std::str::from_utf8(s).unwrap()
1426 );
1427 }
1428 }
1429
1430 #[test]
1431 fn chunk_level_attrs_emitted_when_set() {
1432 let chunk = TraceChunkBytes {
1433 trace_id: [0u8; 16],
1434 priority: Some(1),
1435 origin: bs("lambda"),
1436 sampling_mechanism: Some(4),
1437 spans: vec![make_span("svc", "op", 1)],
1438 ..Default::default()
1439 };
1440 let payload = TracerPayloadBytes {
1441 chunks: vec![chunk],
1442 ..Default::default()
1443 };
1444 let encoded = to_vec_from_v1(&payload);
1445 assert!(
1446 encoded.windows(b"lambda".len()).any(|w| w == b"lambda"),
1447 "chunk origin should appear"
1448 );
1449 let want = [chunk_key::SAMPLING_MECHANISM, 0x04];
1451 assert!(encoded.windows(2).any(|w| w == want));
1452 }
1453
1454 #[test]
1455 fn chunk_dropped_trace_emitted_when_true() {
1456 let chunk = TraceChunkBytes {
1457 trace_id: [0u8; 16],
1458 dropped_trace: true,
1459 spans: vec![make_span("svc", "op", 1)],
1460 ..Default::default()
1461 };
1462 let payload = TracerPayloadBytes {
1463 chunks: vec![chunk],
1464 ..Default::default()
1465 };
1466 let encoded = to_vec_from_v1(&payload);
1467 let want = [chunk_key::DROPPED_TRACE, 0xc3];
1469 assert!(
1470 encoded.windows(2).any(|w| w == want),
1471 "DROPPED_TRACE marker + true should appear in payload"
1472 );
1473 }
1474
1475 #[test]
1476 fn chunk_dropped_trace_skipped_when_false() {
1477 let chunk = TraceChunkBytes {
1478 trace_id: [0u8; 16],
1479 dropped_trace: false,
1480 spans: vec![make_span("svc", "op", 1)],
1481 ..Default::default()
1482 };
1483 let payload = TracerPayloadBytes {
1484 chunks: vec![chunk],
1485 ..Default::default()
1486 };
1487 let encoded = to_vec_from_v1(&payload);
1488 assert!(
1489 !encoded.contains(&chunk_key::DROPPED_TRACE),
1490 "DROPPED_TRACE key should not be emitted when false"
1491 );
1492 }
1493
1494 #[test]
1495 fn chunk_attributes_emitted_when_set() {
1496 let mut attrs = VecMap::new();
1497 attrs.insert(bs("region"), AttributeValue::String(bs("us-east-1")));
1498 let chunk = TraceChunkBytes {
1499 trace_id: [0u8; 16],
1500 attributes: attrs,
1501 spans: vec![make_span("svc", "op", 1)],
1502 ..Default::default()
1503 };
1504 let payload = TracerPayloadBytes {
1505 chunks: vec![chunk],
1506 ..Default::default()
1507 };
1508 let encoded = to_vec_from_v1(&payload);
1509 let want = [chunk_key::ATTRIBUTES, 0x93];
1511 assert!(
1512 encoded.windows(2).any(|w| w == want),
1513 "ATTRIBUTES key + flat-triplet array header should appear"
1514 );
1515 assert!(
1516 encoded
1517 .windows(b"us-east-1".len())
1518 .any(|w| w == b"us-east-1"),
1519 "chunk attribute value should be in the payload"
1520 );
1521 }
1522
1523 #[test]
1524 fn span_kind_otel_values() {
1525 for (kind, expected_byte) in [
1526 (SpanKind::Internal, 0x01u8),
1527 (SpanKind::Server, 0x02),
1528 (SpanKind::Client, 0x03),
1529 (SpanKind::Producer, 0x04),
1530 (SpanKind::Consumer, 0x05),
1531 ] {
1532 let span = V1Span {
1533 service: bs("svc"),
1534 name: bs("op"),
1535 resource: bs("res"),
1536 span_id: 1,
1537 start: 1,
1538 duration: 1,
1539 span_kind: kind,
1540 ..Default::default()
1541 };
1542 let payload = TracerPayloadBytes {
1543 chunks: vec![make_chunk(vec![span], [0u8; 16])],
1544 ..Default::default()
1545 };
1546 let encoded = to_vec_from_v1(&payload);
1547 let want = [0x10u8, expected_byte];
1548 assert!(
1549 encoded.windows(2).any(|w| w == want),
1550 "SpanKind {kind:?} should produce byte {expected_byte:#x}"
1551 );
1552 }
1553 }
1554
1555 #[test]
1556 fn string_interning_works_across_chunks() {
1557 let chunk_with_two = TracerPayloadBytes {
1562 chunks: vec![
1563 make_chunk(vec![make_span("shared", "op1", 1)], [0u8; 16]),
1564 make_chunk(vec![make_span("shared", "op2", 2)], [0u8; 16]),
1565 ],
1566 ..Default::default()
1567 };
1568 let single = TracerPayloadBytes {
1569 chunks: vec![make_chunk(vec![make_span("shared", "op1", 1)], [0u8; 16])],
1570 ..Default::default()
1571 };
1572 let two = to_vec_from_v1(&chunk_with_two);
1573 let one = to_vec_from_v1(&single);
1574 let shared_occurrences = two
1575 .windows(b"shared".len())
1576 .filter(|w| *w == b"shared")
1577 .count();
1578 assert_eq!(
1579 shared_occurrences, 1,
1580 "the literal bytes \"shared\" must appear exactly once; subsequent uses must be \
1581 encoded as interning IDs"
1582 );
1583 assert!(
1584 two.len() < 2 * one.len(),
1585 "interning should reduce repeated payload size"
1586 );
1587 }
1588}