Skip to main content

libdd_trace_utils/msgpack_encoder/v1/
mod.rs

1// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4mod 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
19/// Integer keys for the top-level V1 trace payload map.
20mod 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    /// Payload-level attributes map (e.g. `_dd.apm_mode`, `_dd.git.commit.sha`).
30    pub const ATTRIBUTES: u8 = 10;
31    pub const CHUNKS: u8 = 11;
32}
33
34/// Integer keys for V1 chunk-level fields.
35mod 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    /// Sampling mechanism (previously the `_dd.p.dm` span tag).
43    pub const SAMPLING_MECHANISM: u8 = 7;
44}
45
46/// Integer keys for V1 span fields.
47#[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/// Integer keys for V1 span link fields.
68#[repr(u8)]
69pub(super) enum SpanLinkKey {
70    TraceId = 1,
71    SpanId = 2,
72    Attributes = 3,
73    TraceState = 4,
74    Flags = 5,
75}
76
77/// Integer keys for V1 span event fields.
78#[repr(u8)]
79pub(super) enum SpanEventKey {
80    Time = 1,
81    Name = 2,
82    Attributes = 3,
83}
84
85/// Type discriminants for attribute values.
86/// An attribute value is encoded as [type_uint8][actual_value].
87#[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
98/// Number of msgpack items written per `[type, value]` pair when typed values are flattened
99/// into a parent array (e.g. `AttributeValue::List`).
100pub(super) const TYPED_VALUE_STRIDE: u32 = 2;
101
102/// Number of msgpack items written per `[key, type, value]` triplet when typed attribute
103/// entries are flattened into a parent array (top-level attribute maps and
104/// `AttributeValue::KeyValue`).
105pub(super) const FLAT_ATTR_STRIDE: u32 = 3;
106
107/// Streaming string intern table.
108///
109/// The first time a string is written, it is emitted as a msgpack `str` and assigned an
110/// incrementing integer ID. On subsequent occurrences only the ID is emitted as a msgpack `uint`.
111/// ID 0 is reserved for the empty string (pre-inserted in the constructor).
112///
113/// The string table is scoped per payload: each `to_vec_from_v04` / `to_vec_from_v1` (and
114/// their `write_to_slice_from_v04` / `write_to_slice_from_v1` counterparts) call starts with a
115/// fresh table so deduplication is payload-local.
116pub(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    /// Writes `s` to `writer` using string interning.
128    ///
129    /// - First occurrence of `s` → msgpack `str`, ID recorded for future references
130    /// - Subsequent occurrence → msgpack `uint` carrying the previously assigned ID
131    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
148/// Returns the span start time in UNIX nanos, falling back to the current wall-clock time when
149/// the input is negative. Matches the agent's `validateAndFixStartTime`, which substitutes
150/// `time.Now().UnixNano()` for invalid start values; without this, a negative `i64` would wrap
151/// to a near-`u64::MAX` timestamp on cast.
152pub(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
163/// Promoted fields extracted from the payload's spans, written at the top-level map.
164struct PayloadAttrs<'a> {
165    env: Option<&'a str>,
166    hostname: Option<&'a str>,
167    app_version: Option<&'a str>,
168    /// `_dd.apm_mode` span tag, promoted to payload-level attributes.
169    apm_mode: Option<&'a str>,
170    /// `_dd.git.commit.sha` span tag, promoted to payload-level attributes.
171    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    // Prefer TracerMetadata (set once on the builder) over span scanning. Fall back to
182    // span meta only when the builder-level value is missing — e.g. v04 payloads where
183    // the SDK propagated these as span tags.
184    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
229/// Promoted fields extracted from spans and written at the chunk level.
230struct ChunkAttrs<'a> {
231    /// Full 128-bit trace ID (encodes as 16-byte big-endian binary).
232    trace_id: u128,
233    /// Sampling priority from `_sampling_priority_v1` metric on the root span.
234    sampling_priority: Option<i32>,
235    /// Origin tag from `_dd.origin` meta on the root span.
236    origin: Option<&'a str>,
237    /// Sampling mechanism from `_dd.p.dm` meta on the root span.
238    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    // trace_id is invariant per chunk. The v04 wire format carries only the low 64 bits;
246    // the high 64 bits are propagated as the hex string meta tag "_dd.p.tid".
247    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        // Root span: either no parent in this chunk, or tagged _dd.top_level=1 (remote parent).
265        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            // Root span is authoritative: its values supersede any non-root fallback,
270            // including absence (a field missing on the root should not be filled from non-roots).
271            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            // _dd.p.dm is a signed integer stored as a string; unsigned_abs preserves the
274            // magnitude.
275            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        // No root found yet — accumulate fallback values from non-root spans (partial flush).
284        // Root span values will override these if a root is eventually encountered.
285        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
308/// Encodes all traces as a V1 msgpack payload.
309///
310/// Top-level format:
311/// ```text
312/// Map {
313///   trace_key::CONTAINER_ID (2)  → str|uint       // optional, interned
314///   trace_key::ENV_REF      (7)  → str|uint       // optional, interned
315///   trace_key::HOSTNAME_REF (8)  → str|uint       // optional, interned
316///   trace_key::APP_VERSION  (9)  → str|uint       // optional, interned
317///   trace_key::ATTRIBUTES   (10) → Array[...]     // optional, flat triplets: key, type, value
318///   trace_key::CHUNKS       (11) → Array[Chunk, ...]
319/// }
320/// ```
321fn 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 // chunks always present
334        + (!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        // Encoded as a flat array of triplets: [key, type_uint, value, ...]
394        // String values use type discriminant 1.
395        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
412/// Encodes one chunk (a group of spans sharing a trace ID).
413///
414/// ```text
415/// Map {
416///   chunk_key::TRACE_ID           (6) → bin[16]       // 128-bit big-endian
417///   chunk_key::ORIGIN             (2) → str|uint       // optional, interned
418///   chunk_key::PRIORITY           (1) → int            // optional
419///   chunk_key::SAMPLING_MECHANISM (7) → uint           // optional
420///   chunk_key::SPANS              (4) → Array[Span, ...]
421/// }
422/// ```
423fn 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 // trace_id + spans are always present
431        + 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
464/// Serializes traces into a slice using the V1 msgpack format.
465///
466/// # Errors
467/// Returns a `ValueWriteError` if the underlying writer fails.
468pub fn write_to_slice_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
469    // &mut &mut [u8] lets the caller see the slice shrink as bytes are written.
470    slice: &mut &mut [u8],
471    traces: &[S],
472    metadata: &TracerMetadata,
473) -> Result<(), ValueWriteError> {
474    encode_payload_from_v04(slice, traces, metadata)
475}
476
477/// Serializes traces into a `Vec<u8>` using the V1 msgpack format.
478pub 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
485/// Serializes traces into a `Vec<u8>` with a pre-allocated capacity.
486pub 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
498/// Returns the number of bytes the V1 payload for `traces` would occupy.
499pub 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    // `CountLength` impls `std::io::Write` (whose error type is `std::io::Error`, not
505    // `Infallible`), so we can't statically prove infallibility via `unwrap_infallible`
506    // the way we do for `ByteBuf`. In practice `CountLength::write*` only ever return
507    // `Ok`, so the error path here is unreachable today; should `CountLength` ever grow
508    // a fallible code path, fuzz tests on the msgpack encoded length would catch it.
509    let _ = encode_payload_from_v04(&mut counter, traces, metadata);
510    counter.0
511}
512
513/// Encodes a [`TracerPayload`] (V1 data model) as a V1 msgpack payload.
514fn 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 // chunks always present
523        + (!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
589/// Encodes one V1 chunk (a group of spans sharing a trace ID).
590fn 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 // trace_id + spans
600        + !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
645/// Serializes a `TracerPayload` into a vector of bytes with a default capacity of 0.
646///
647/// # Arguments
648///
649/// * `payload` - A reference to a `TracerPayload`.
650///
651/// # Returns
652///
653/// * `Vec<u8>` - A vector containing the encoded payload.
654///
655/// # Examples
656///
657/// ```
658/// use libdd_trace_utils::msgpack_encoder::v1::to_vec_from_v1;
659/// use libdd_trace_utils::span::v1::TracerPayloadSlice;
660///
661/// let payload = TracerPayloadSlice {
662///     language_name: "rust".into(),
663///     ..Default::default()
664/// };
665/// let encoded = to_vec_from_v1(&payload);
666///
667/// assert!(!encoded.is_empty());
668/// ```
669pub fn to_vec_from_v1<T: TraceData>(payload: &TracerPayload<T>) -> Vec<u8> {
670    to_vec_with_capacity_from_v1(payload, 0)
671}
672
673/// Serializes a `TracerPayload` into a vector of bytes with specified capacity.
674///
675/// # Arguments
676///
677/// * `payload` - A reference to a `TracerPayload`.
678/// * `capacity` - Desired initial capacity of the resulting vector.
679///
680/// # Returns
681///
682/// * `Vec<u8>` - A vector containing the encoded payload.
683///
684/// # Examples
685///
686/// ```
687/// use libdd_trace_utils::msgpack_encoder::v1::to_vec_with_capacity_from_v1;
688/// use libdd_trace_utils::span::v1::TracerPayloadSlice;
689///
690/// let payload = TracerPayloadSlice {
691///     language_name: "rust".into(),
692///     ..Default::default()
693/// };
694/// let encoded = to_vec_with_capacity_from_v1(&payload, 1024);
695///
696/// assert!(encoded.capacity() >= 1024);
697/// ```
698pub 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
709/// Encodes a `TracerPayload` into a slice of bytes.
710///
711/// # Arguments
712///
713/// * `slice` - A mutable reference to a byte slice.
714/// * `payload` - A reference to a `TracerPayload`.
715///
716/// # Returns
717///
718/// * `Ok(())` - If encoding succeeds.
719/// * `Err(ValueWriteError)` - If encoding fails.
720///
721/// # Errors
722///
723/// This function will return an error if the underlying writer fails (e.g. buffer too small).
724///
725/// # Examples
726///
727/// ```
728/// use libdd_trace_utils::msgpack_encoder::v1::write_to_slice_from_v1;
729/// use libdd_trace_utils::span::v1::TracerPayloadSlice;
730///
731/// let mut buffer = vec![0u8; 1024];
732/// let payload = TracerPayloadSlice {
733///     language_name: "rust".into(),
734///     ..Default::default()
735/// };
736///
737/// write_to_slice_from_v1(&mut &mut buffer[..], &payload).expect("Encoding failed");
738/// ```
739pub 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
746/// Computes the number of bytes required to encode the given `TracerPayload`.
747///
748/// This does not allocate any actual buffer, but simulates writing in order to measure
749/// the encoded size of the payload.
750///
751/// # Arguments
752///
753/// * `payload` - A reference to a `TracerPayload`.
754///
755/// # Returns
756///
757/// * `u32` - The number of bytes that would be written by the encoder.
758///
759/// # Examples
760///
761/// ```
762/// use libdd_trace_utils::msgpack_encoder::v1::to_encoded_byte_len_from_v1;
763/// use libdd_trace_utils::span::v1::TracerPayloadSlice;
764///
765/// let payload = TracerPayloadSlice {
766///     language_name: "rust".into(),
767///     ..Default::default()
768/// };
769/// let encoded_len = to_encoded_byte_len_from_v1(&payload);
770///
771/// assert!(encoded_len > 0);
772/// ```
773pub 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        // Must still produce a valid msgpack map with an empty chunks array.
818        assert!(!encoded.is_empty());
819    }
820
821    #[test]
822    fn test_string_interning_reduces_size() {
823        // Two spans with the same service name — second occurrence should use the integer ID.
824        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        // Single span for baseline.
829        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        // The two-trace payload should be less than 2× the single-trace payload
836        // if interning is working (the second "my-service" is encoded as an integer).
837        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        // The payload must contain "lambda" somewhere (the origin string).
871        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        // A span with a non-zero parent_id but _dd.top_level=1.0 is a root in its chunk.
896        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, // remote parent — not in this chunk
909            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        // Both attribute strings must appear in the payload bytes.
993        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        // The attribute key names must also be present (first occurrence is a raw str).
1004        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        // A span with no _dd.apm_mode or _dd.git.commit.sha must not produce key 10.
1019        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        // Payload with metadata must be larger (it carries extra fields).
1061        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        // Expected 16-byte BE: high = 0x640cfd5400000000, low = 0x0123456789abcdef
1086        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        // _dd.p.tid must not also leak into span attributes.
1095        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        // Absent _dd.p.tid → high 64 bits zero.
1105        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        // `_dd.p.dm` is a signed integer stored as a string (e.g. "-4" → manual rule).
1120        // The encoder must parse it, take unsigned_abs, and emit it at chunk level.
1121        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        // The chunk-level sampling_mechanism (key 7) must be encoded as uint 4.
1141        // The byte sequence is `chunk_key::SAMPLING_MECHANISM (0x07)` followed by the
1142        // msgpack representation of 4 (positive fixint 0x04).
1143        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        // Partial flush: no root span (every span has a non-zero parent and no
1153        // `_dd.top_level`). Values must be accumulated from non-root spans.
1154        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, // non-zero parent → not a root
1173            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        // Each attribute must be present at chunk level — collected from a different
1205        // non-root span.
1206        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        // priority 2 → msgpack positive fixint 0x02 preceded by PRIORITY key
1212        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        // sampling_mechanism = unsigned_abs("-3") = 3 → 0x03 preceded by SAMPLING_MECHANISM key
1218        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    //! Unit tests for the v1::Span encoder (`encode_payload_from_v1`).
1229    //!
1230    //! Verifies the encoder produces a valid V1 payload from the canonical
1231    //! [`crate::span::v1::TracerPayload`] data model and that core invariants (interning, byte
1232    //! length, optional fields) hold.
1233
1234    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        // Map with a single entry (chunks), then an empty array. `0x81` = fixmap of length 1,
1271        // followed by chunk key (0x0b), then `0x90` (fixarray length 0).
1272        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        // Default SpanKind (Internal=1) must be emitted. The encoded payload contains
1290        // `kind_key (0x10) | uint 1 (0x01)`.
1291        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        // String attribute → type discriminant = 1 (`AnyValueKey::String`).
1325        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        // A Bytes attribute must use the msgpack `bin` family, not `str`.
1334        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        // bin8 marker `0xc4` followed by length `0x02` and the bytes themselves.
1355        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        // The keys and the nested key must all appear at least once.
1391        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        // sampling_mechanism=4 → SAMPLING_MECHANISM (0x07) + positive fixint 0x04
1450        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        // DROPPED_TRACE (0x05) + msgpack true marker (0xc3)
1468        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        // ATTRIBUTES (0x03) + msgpack fixarray header for 3 elements (0x93)
1510        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        // The string "shared" appears in two chunks. The second occurrence must be a uint ID,
1558        // not a fresh str. Verify by (a) scanning the encoded bytes for the literal "shared"
1559        // — it must appear exactly once — and (b) confirming the two-chunk payload is smaller
1560        // than two independent single-chunk payloads.
1561        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}