libdd_trace_utils/msgpack_encoder/v04/mod.rs
1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::span::v04::Span;
5use crate::span::v1::TracerPayload;
6use crate::span::TraceData;
7use libdd_common::ResultInfallibleExt;
8use rmp::encode::{write_array_len, ByteBuf, RmpWrite, ValueWriteError};
9
10const fn msgpack_string_encoding_len(s: &str) -> usize {
11 const U16_MAX: usize = u16::MAX as usize;
12 let length_marker_len = match s.len() {
13 0..32 => 1,
14 32..256 => 2,
15 256..=U16_MAX => 3,
16 _ => 5,
17 };
18 length_marker_len + s.len()
19}
20
21// Compute the encoding of a string to msgpack in a const manner
22const fn msgpack_const_string_encoding<const ENCODING_LEN: usize>(s: &str) -> [u8; ENCODING_LEN] {
23 // copy_to_slice is not const yet, so we make a helper
24 const fn copy_to_slice(dest: &mut [u8], src: &[u8], n: usize) {
25 let mut i = 0;
26 while i < n {
27 dest[i] = src[i];
28 i += 1;
29 }
30 }
31
32 let mut storage = [0; ENCODING_LEN];
33 let len = s.len() as u64;
34 let len_bytes = if len < 32 {
35 storage[0] = 0xa0 | (len as u8 & 0x1f);
36 0
37 } else if len < 256 {
38 storage[0] = 0xd9;
39 1
40 } else if len <= (u16::MAX as u64) {
41 storage[0] = 0xda;
42 2
43 } else {
44 storage[0] = 0xdb;
45 4
46 };
47 let len_be_bytes = len.to_be_bytes();
48 // `len_be_bytes` holds `len` as 8 big-endian bytes; the marker only needs the low-order
49 // `len_bytes` of those (e.g. for a str8 length of 200, that's byte `[200]`, not `[0]`), so
50 // skip the leading zero bytes rather than copying from the front.
51 copy_to_slice(
52 storage.split_at_mut(1).1,
53 len_be_bytes.split_at(8 - len_bytes).1,
54 len_bytes,
55 );
56 copy_to_slice(storage.split_at_mut(1 + len_bytes).1, s.as_bytes(), s.len());
57 storage
58}
59
60macro_rules! write_const_msgpack_str {
61 ($writer:expr, $str:expr) => {{
62 use rmp::encode::ValueWriteError;
63 const STRING_ENCODING_LEN: usize = super::msgpack_string_encoding_len($str);
64 const STRING_ENCODING: [u8; STRING_ENCODING_LEN] =
65 super::msgpack_const_string_encoding($str);
66
67 $writer
68 .write_bytes(&STRING_ENCODING)
69 .map_err(ValueWriteError::InvalidDataWrite)
70 }};
71}
72
73mod span_v04;
74mod span_v1;
75
76#[inline(always)]
77fn to_writer<W: RmpWrite, T: TraceData, S: AsRef<[Span<T>]>>(
78 writer: &mut W,
79 traces: &[S],
80) -> Result<(), ValueWriteError<W::Error>> {
81 write_array_len(writer, traces.len() as u32)?;
82 for trace in traces {
83 write_array_len(writer, trace.as_ref().len() as u32)?;
84 for span in trace.as_ref() {
85 span_v04::encode_span(writer, span)?;
86 }
87 }
88
89 Ok(())
90}
91
92/// Encodes a collection of traces into a slice of bytes.
93///
94/// # Arguments
95///
96/// * `slice` - A mutable reference to a byte slice.
97/// * `traces` - A reference to a slice of spans.
98///
99/// # Returns
100///
101/// * `Ok(())` - If encoding succeeds.
102/// * `Err(ValueWriteError)` - If encoding fails.
103///
104/// # Errors
105///
106/// This function will return an error if:
107/// - The array length for trace count or span count cannot be written.
108/// - Any span cannot be encoded.
109///
110/// # Examples
111///
112/// ```
113/// use libdd_trace_utils::msgpack_encoder::v04::write_to_slice_from_v04;
114/// use libdd_trace_utils::span::v04::SpanSlice;
115/// use std::borrow::Cow;
116///
117/// let mut buffer = vec![0u8; 1024];
118/// let span = SpanSlice {
119/// name: Cow::Borrowed("test-span"),
120/// ..Default::default()
121/// };
122/// let traces = vec![vec![span]];
123///
124/// write_to_slice_from_v04(&mut &mut buffer[..], &traces).expect("Encoding failed");
125/// ```
126pub fn write_to_slice_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
127 slice: &mut &mut [u8],
128 traces: &[S],
129) -> Result<(), ValueWriteError> {
130 to_writer(slice, traces)
131}
132
133/// Serializes traces into a vector of bytes with a default capacity of 0.
134///
135/// # Arguments
136///
137/// * `traces` - A reference to a slice of spans.
138///
139/// # Returns
140///
141/// * `Vec<u8>` - A vector containing encoded traces.
142///
143/// # Examples
144///
145/// ```
146/// use libdd_trace_utils::msgpack_encoder::v04::to_vec_from_v04;
147/// use libdd_trace_utils::span::v04::SpanSlice;
148/// use std::borrow::Cow;
149///
150/// let span = SpanSlice {
151/// name: Cow::Borrowed("test-span"),
152/// ..Default::default()
153/// };
154/// let traces = vec![vec![span]];
155/// let encoded = to_vec_from_v04(&traces);
156///
157/// assert!(!encoded.is_empty());
158/// ```
159pub fn to_vec_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(traces: &[S]) -> Vec<u8> {
160 to_vec_with_capacity_from_v04(traces, 0)
161}
162
163/// Serializes traces into a vector of bytes with specified capacity.
164///
165/// # Arguments
166///
167/// * `traces` - A reference to a slice of spans.
168/// * `capacity` - Desired initial capacity of the resulting vector.
169///
170/// # Returns
171///
172/// * `Vec<u8>` - A vector containing encoded traces.
173///
174/// # Examples
175///
176/// ```
177/// use libdd_trace_utils::msgpack_encoder::v04::to_vec_with_capacity_from_v04;
178/// use libdd_trace_utils::span::v04::SpanSlice;
179/// use std::borrow::Cow;
180///
181/// let span = SpanSlice {
182/// name: Cow::Borrowed("test-span"),
183/// ..Default::default()
184/// };
185/// let traces = vec![vec![span]];
186/// let encoded = to_vec_with_capacity_from_v04(&traces, 1024);
187///
188/// assert!(encoded.capacity() >= 1024);
189/// ```
190pub fn to_vec_with_capacity_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(
191 traces: &[S],
192 capacity: u32,
193) -> Vec<u8> {
194 let mut buf = ByteBuf::with_capacity(capacity as usize);
195 to_writer(&mut buf, traces)
196 .map_err(super::flatten_value_write_infallible)
197 .unwrap_infallible();
198 buf.into_vec()
199}
200
201/// Computes the number of bytes required to encode the given traces.
202///
203/// This does not allocate any actual buffer, but simulates writing in order to measure
204/// the encoded size of the traces.
205///
206/// # Arguments
207///
208/// * `traces` - A reference to a slice of spans.
209///
210/// # Returns
211///
212/// * `u32` - The number of bytes that would be written by the encoder.
213///
214/// # Examples
215///
216/// ```
217/// use libdd_trace_utils::msgpack_encoder::v04::to_encoded_byte_len_from_v04;
218/// use libdd_trace_utils::span::v04::SpanSlice;
219/// use std::borrow::Cow;
220///
221/// let span = SpanSlice {
222/// name: Cow::Borrowed("test-span"),
223/// ..Default::default()
224/// };
225/// let traces = vec![vec![span]];
226/// let encoded_len = to_encoded_byte_len_from_v04(&traces);
227///
228/// assert!(encoded_len > 0);
229/// ```
230pub fn to_encoded_byte_len_from_v04<T: TraceData, S: AsRef<[Span<T>]>>(traces: &[S]) -> u32 {
231 let mut counter = super::CountLength(0);
232 // `CountLength` impls `std::io::Write` (whose error type is `std::io::Error`, not
233 // `Infallible`), so we can't statically prove infallibility via `unwrap_infallible`
234 // the way we do for `ByteBuf`. In practice `CountLength::write*` only ever return
235 // `Ok`, so the error path here is unreachable today; should `CountLength` ever grow
236 // a fallible code path, fuzz tests on the msgpack encoded length would catch it.
237 let _ = to_writer(&mut counter, traces);
238 counter.0
239}
240
241/// Encodes a [`TracerPayload`] in the v0.4 wire format (downgrade path used when the agent
242/// does not advertise `/v1.0/traces`). The output is a msgpack array of traces, where each
243/// trace is itself a msgpack array of v0.4-shaped spans — matching the existing v0.4 wire
244/// format produced by [`to_vec_from_v04`]. Payload-level `env`/`app_version`/`attributes` are
245/// propagated into every span (see [`span_v1`]'s mapping table); `payload.hostname` has no v0.4
246/// body equivalent (the agent gets hostname from the `Datadog-Meta-Hostname` header instead) and is
247/// intentionally dropped here.
248fn encode_payload_from_v1<W: RmpWrite, T: TraceData>(
249 writer: &mut W,
250 payload: &TracerPayload<T>,
251) -> Result<(), ValueWriteError<W::Error>> {
252 use span_v1::{encode_span, ChunkContext};
253
254 write_array_len(writer, payload.chunks.len() as u32)?;
255 for chunk in &payload.chunks {
256 // v0.4 has no wire-level equivalent of `dropped_trace`; the closest historical signal
257 // is `USER_REJECT` (priority -1), which tells the agent the sampler rejected this trace
258 // without dropping the spans themselves. Only force it when the chunk doesn't already
259 // carry a negative (reject-like) priority.
260 let priority = if chunk.dropped_trace {
261 Some(chunk.priority.filter(|&p| p < 0).unwrap_or(-1))
262 } else {
263 chunk.priority
264 };
265 let ctx = ChunkContext::new(
266 &chunk.trace_id,
267 priority,
268 &chunk.origin,
269 chunk.sampling_mechanism,
270 &chunk.attributes,
271 &payload.env,
272 &payload.app_version,
273 &payload.attributes,
274 );
275 write_array_len(writer, chunk.spans.len() as u32)?;
276 for span in &chunk.spans {
277 encode_span(writer, span, &ctx)?;
278 }
279 }
280 Ok(())
281}
282
283/// Serializes a [`TracerPayload`] (V1 data model) as a v0.4 msgpack payload.
284///
285/// Used by the trace exporter when the agent has not advertised `/v1.0/traces` via `/info`.
286/// The output is byte-compatible with [`to_vec_from_v04`] for equivalent data — chunk-level fields
287/// are propagated to every span and typed attributes are bucketed into the v0.4 `meta` /
288/// `metrics` / `meta_struct` maps per [`span_v1`]'s mapping table.
289pub fn to_vec_from_v1<T: TraceData>(payload: &TracerPayload<T>) -> Vec<u8> {
290 to_vec_with_capacity_from_v1(payload, 0)
291}
292
293/// Serializes a [`TracerPayload`] as a v0.4 msgpack payload with a caller-supplied initial
294/// capacity. Use this when you can size the buffer up front (e.g. from
295/// [`to_encoded_byte_len_from_v1`]) to avoid reallocations.
296pub fn to_vec_with_capacity_from_v1<T: TraceData>(
297 payload: &TracerPayload<T>,
298 capacity: u32,
299) -> Vec<u8> {
300 let mut buf = ByteBuf::with_capacity(capacity as usize);
301 encode_payload_from_v1(&mut buf, payload)
302 .map_err(super::flatten_value_write_infallible)
303 .unwrap_infallible();
304 buf.into_vec()
305}
306
307/// Encodes a [`TracerPayload`] as v0.4 msgpack into the provided slice. Useful for callers
308/// that own a pre-sized buffer (e.g. for FFI / zero-copy paths).
309///
310/// # Errors
311///
312/// Returns any [`ValueWriteError`] from the underlying writer (typically buffer-too-small).
313pub fn write_to_slice_from_v1<T: TraceData>(
314 slice: &mut &mut [u8],
315 payload: &TracerPayload<T>,
316) -> Result<(), ValueWriteError> {
317 encode_payload_from_v1(slice, payload)
318}
319
320/// Returns the exact number of bytes [`to_vec_from_v1`] would write for `payload`. Walks
321/// the payload through a counting writer without allocating an output buffer.
322pub fn to_encoded_byte_len_from_v1<T: TraceData>(payload: &TracerPayload<T>) -> u32 {
323 let mut counter = super::CountLength(0);
324 let _ = encode_payload_from_v1(&mut counter, payload);
325 counter.0
326}
327
328#[cfg(test)]
329mod tests {
330 //! Regression tests for [`msgpack_const_string_encoding`] across every msgpack string
331 //! length-marker boundary (fixstr / str8 / str16), since the length only fits in the
332 //! low-order bytes of `len.to_be_bytes()` and it's easy to accidentally copy from the
333 //! high-order (zero) end instead.
334 use super::msgpack_const_string_encoding;
335
336 fn encode<const N: usize>(s: &str) -> [u8; N] {
337 msgpack_const_string_encoding::<N>(s)
338 }
339
340 #[test]
341 fn fixstr_boundary_31_bytes() {
342 let s = "a".repeat(31);
343 let bytes: [u8; 32] = encode(&s);
344 let value = rmpv::decode::read_value(&mut &bytes[..]).expect("decode failed");
345 assert_eq!(value.as_str(), Some(s.as_str()));
346 }
347
348 #[test]
349 fn str8_boundary_200_bytes() {
350 let s = "b".repeat(200);
351 let bytes: [u8; 202] = encode(&s);
352 let value = rmpv::decode::read_value(&mut &bytes[..]).expect("decode failed");
353 assert_eq!(value.as_str(), Some(s.as_str()));
354 }
355
356 #[test]
357 fn str16_boundary_300_bytes() {
358 let s = "c".repeat(300);
359 let bytes: [u8; 303] = encode(&s);
360 let value = rmpv::decode::read_value(&mut &bytes[..]).expect("decode failed");
361 assert_eq!(value.as_str(), Some(s.as_str()));
362 }
363}