diff --git a/libdd-trace-utils/src/msgpack_encoder/v1/mod.rs b/libdd-trace-utils/src/msgpack_encoder/v1/mod.rs index f72567f917..2c24f400bc 100644 --- a/libdd-trace-utils/src/msgpack_encoder/v1/mod.rs +++ b/libdd-trace-utils/src/msgpack_encoder/v1/mod.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 mod span_v04; +mod span_v1; use crate::span::v04::Span; +use crate::span::v1::TracerPayload; use crate::span::TraceData; use crate::tracer_metadata::TracerMetadata; use libdd_common::ResultInfallibleExt; @@ -421,6 +423,160 @@ pub fn to_encoded_byte_len]>>( counter.0 } +/// Encodes a [`TracerPayload`] (V1 data model) as a V1 msgpack payload. +fn encode_payload_v1( + writer: &mut W, + payload: &TracerPayload, +) -> Result<(), ValueWriteError> { + let mut table = StringTable::new(); + + let has_attributes = !payload.attributes.is_empty(); + + let map_len = 1u32 // chunks always present + + (!payload.language_name.borrow().is_empty()) as u32 + + (!payload.language_version.borrow().is_empty()) as u32 + + (!payload.tracer_version.borrow().is_empty()) as u32 + + (!payload.runtime_id.borrow().is_empty()) as u32 + + (!payload.env.borrow().is_empty()) as u32 + + (!payload.hostname.borrow().is_empty()) as u32 + + (!payload.app_version.borrow().is_empty()) as u32 + + has_attributes as u32; + + write_map_len(writer, map_len)?; + + if !payload.language_name.borrow().is_empty() { + write_uint8(writer, trace_key::LANGUAGE_NAME)?; + table.write_interned(writer, payload.language_name.borrow())?; + } + + if !payload.language_version.borrow().is_empty() { + write_uint8(writer, trace_key::LANGUAGE_VERSION)?; + table.write_interned(writer, payload.language_version.borrow())?; + } + + if !payload.tracer_version.borrow().is_empty() { + write_uint8(writer, trace_key::TRACER_VERSION)?; + table.write_interned(writer, payload.tracer_version.borrow())?; + } + + if !payload.runtime_id.borrow().is_empty() { + write_uint8(writer, trace_key::RUNTIME_ID)?; + table.write_interned(writer, payload.runtime_id.borrow())?; + } + + if !payload.env.borrow().is_empty() { + write_uint8(writer, trace_key::ENV_REF)?; + table.write_interned(writer, payload.env.borrow())?; + } + + if !payload.hostname.borrow().is_empty() { + write_uint8(writer, trace_key::HOSTNAME_REF)?; + table.write_interned(writer, payload.hostname.borrow())?; + } + + if !payload.app_version.borrow().is_empty() { + write_uint8(writer, trace_key::APP_VERSION_REF)?; + table.write_interned(writer, payload.app_version.borrow())?; + } + + if has_attributes { + write_uint8(writer, trace_key::ATTRIBUTES)?; + span_v1::encode_attributes_map(writer, &payload.attributes, &mut table)?; + } + + write_uint8(writer, trace_key::CHUNKS)?; + write_array_len(writer, payload.chunks.len() as u32)?; + for chunk in &payload.chunks { + encode_chunk_v1(writer, chunk, &mut table)?; + } + + Ok(()) +} + +/// Encodes one V1 chunk (a group of spans sharing a trace ID). +fn encode_chunk_v1( + writer: &mut W, + chunk: &crate::span::v1::TraceChunk, + table: &mut StringTable, +) -> Result<(), ValueWriteError> { + let has_origin = chunk + .origin + .as_ref() + .is_some_and(|o| !>::borrow(o).is_empty()); + + let fields = 2u32 // trace_id + spans + + has_origin as u32 + + chunk.priority.is_some() as u32 + + chunk.sampling_mechanism.is_some() as u32; + + write_map_len(writer, fields)?; + + write_uint8(writer, chunk_key::TRACE_ID)?; + write_bin(writer, &chunk.trace_id)?; + + if let Some(origin) = chunk + .origin + .as_ref() + .filter(|o| !>::borrow(o).is_empty()) + { + write_uint8(writer, chunk_key::ORIGIN)?; + table.write_interned(writer, >::borrow(origin))?; + } + + if let Some(priority) = chunk.priority { + write_uint8(writer, chunk_key::PRIORITY)?; + write_sint(writer, priority as i64)?; + } + + if let Some(mechanism) = chunk.sampling_mechanism { + write_uint8(writer, chunk_key::SAMPLING_MECHANISM)?; + write_uint(writer, mechanism as u64)?; + } + + write_uint8(writer, chunk_key::SPANS)?; + write_array_len(writer, chunk.spans.len() as u32)?; + for span in &chunk.spans { + span_v1::encode_span(writer, span, table)?; + } + + Ok(()) +} + +/// Serializes a [`TracerPayload`] into a `Vec` using the V1 msgpack format. +pub fn to_vec_from_payload(payload: &TracerPayload) -> Vec { + to_vec_from_payload_with_capacity(payload, 0) +} + +/// Serializes a [`TracerPayload`] into a `Vec` with a pre-allocated capacity. +pub fn to_vec_from_payload_with_capacity( + payload: &TracerPayload, + capacity: u32, +) -> Vec { + let mut buf = ByteBuf::with_capacity(capacity as usize); + encode_payload_v1(&mut buf, payload) + .map_err(super::flatten_value_write_infallible) + .unwrap_infallible(); + buf.into_vec() +} + +/// Serializes a [`TracerPayload`] into a caller-provided slice. +/// +/// # Errors +/// Returns a `ValueWriteError` if the underlying writer fails. +pub fn write_payload_to_slice( + slice: &mut &mut [u8], + payload: &TracerPayload, +) -> Result<(), ValueWriteError> { + encode_payload_v1(slice, payload) +} + +/// Returns the number of bytes `payload` would occupy when encoded. +pub fn to_encoded_byte_len_from_payload(payload: &TracerPayload) -> u32 { + let mut counter = super::CountLength(0); + let _ = encode_payload_v1(&mut counter, payload); + counter.0 +} + #[cfg(test)] mod tests { use super::*; @@ -864,3 +1020,874 @@ mod tests { ); } } + +#[cfg(test)] +mod v1_payload_tests { + //! Unit tests for the M3 encoder (`encode_payload_v1`). + //! + //! Verifies the encoder produces a valid V1 payload from the canonical + //! [`crate::span::v1::TracerPayload`] data model and that core invariants (interning, byte + //! length, optional fields) hold. + + use super::*; + use crate::span::v1::{ + AttributeValue, Span as V1Span, SpanBytes as V1SpanBytes, SpanKind, TraceChunkBytes, + TracerPayloadBytes, + }; + use libdd_tinybytes::BytesString; + + fn bs(s: &str) -> BytesString { + BytesString::from_slice(s.as_bytes()).unwrap_or_default() + } + + fn make_span(service: &str, name: &str, span_id: u64) -> V1SpanBytes { + V1Span { + service: bs(service), + name: bs(name), + resource: bs("res"), + span_id, + start: 1_000_000, + duration: 500, + ..Default::default() + } + } + + fn make_chunk(spans: Vec, trace_id: [u8; 16]) -> TraceChunkBytes { + TraceChunkBytes { + trace_id, + spans, + ..Default::default() + } + } + + #[test] + fn empty_payload_is_valid_msgpack_map() { + let payload = TracerPayloadBytes::default(); + let encoded = to_vec_from_payload(&payload); + // Map with a single entry (chunks), then an empty array. `0x81` = fixmap of length 1, + // followed by chunk key (0x0b), then `0x90` (fixarray length 0). + assert_eq!(encoded, vec![0x81, 0x0b, 0x90]); + } + + #[test] + fn payload_byte_len_matches_to_vec() { + let chunk = make_chunk(vec![make_span("svc", "op", 1)], [0u8; 16]); + let payload = TracerPayloadBytes { + chunks: vec![chunk], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + let len = to_encoded_byte_len_from_payload(&payload); + assert_eq!(encoded.len() as u32, len); + } + + #[test] + fn span_kind_is_always_emitted_as_uint() { + // Default SpanKind (Internal=1) must be emitted. The encoded payload contains + // `kind_key (0x10) | uint 1 (0x01)`. + let chunk = make_chunk(vec![make_span("svc", "op", 1)], [0u8; 16]); + let payload = TracerPayloadBytes { + chunks: vec![chunk], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + let pat = [0x10u8, 0x01u8]; + assert!( + encoded.windows(2).any(|w| w == pat), + "Kind (key=16) Internal (=1) must be emitted" + ); + } + + #[test] + fn typed_attributes_carry_correct_type_discriminants() { + let mut attrs = HashMap::new(); + attrs.insert(bs("k_str"), AttributeValue::String(bs("v"))); + let span = V1Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1, + duration: 1, + attributes: attrs, + ..Default::default() + }; + let chunk = make_chunk(vec![span], [0u8; 16]); + let payload = TracerPayloadBytes { + chunks: vec![chunk], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + // String attribute → type discriminant = 1 (`AnyValueKey::String`). + assert!( + encoded.windows(b"k_str".len()).any(|w| w == b"k_str"), + "attribute key must appear" + ); + } + + #[test] + fn bytes_attribute_uses_bin_marker() { + // A Bytes attribute must use the msgpack `bin` family, not `str`. + let mut attrs = HashMap::new(); + attrs.insert( + bs("payload"), + AttributeValue::Bytes(libdd_tinybytes::Bytes::copy_from_slice(b"\xde\xad")), + ); + let span = V1Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1, + duration: 1, + attributes: attrs, + ..Default::default() + }; + let payload = TracerPayloadBytes { + chunks: vec![make_chunk(vec![span], [0u8; 16])], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + // bin8 marker `0xc4` followed by length `0x02` and the bytes themselves. + let want = [0xc4u8, 0x02, 0xde, 0xad]; + assert!( + encoded.windows(4).any(|w| w == want), + "Bytes attribute must be encoded as msgpack bin" + ); + } + + #[test] + fn list_and_keyvalue_attributes_round_trip_through_recursion() { + let mut nested = HashMap::new(); + nested.insert(bs("nk"), AttributeValue::Int(7)); + let mut attrs = HashMap::new(); + attrs.insert( + bs("list"), + AttributeValue::List(vec![ + AttributeValue::String(bs("a")), + AttributeValue::Bool(true), + ]), + ); + attrs.insert(bs("kv"), AttributeValue::KeyValue(nested)); + let span = V1Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1, + duration: 1, + attributes: attrs, + ..Default::default() + }; + let payload = TracerPayloadBytes { + chunks: vec![make_chunk(vec![span], [0u8; 16])], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + // The keys and the nested key must all appear at least once. + for s in &[b"list" as &[u8], b"kv", b"a", b"nk"] { + assert!( + encoded.windows(s.len()).any(|w| w == *s), + "{} should appear in payload", + std::str::from_utf8(s).unwrap() + ); + } + } + + #[test] + fn promoted_fields_at_payload_level() { + let payload = TracerPayloadBytes { + language_name: bs("python"), + language_version: bs("3.11"), + tracer_version: bs("2.0.0"), + runtime_id: bs("rt-1"), + env: bs("prod"), + hostname: bs("h"), + app_version: bs("1.2.3"), + chunks: vec![make_chunk(vec![make_span("svc", "op", 1)], [0u8; 16])], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + for s in &[ + b"python" as &[u8], + b"3.11", + b"2.0.0", + b"rt-1", + b"prod", + b"1.2.3", + ] { + assert!( + encoded.windows(s.len()).any(|w| w == *s), + "{} should appear", + std::str::from_utf8(s).unwrap() + ); + } + } + + #[test] + fn chunk_level_attrs_emitted_when_set() { + let chunk = TraceChunkBytes { + trace_id: [0u8; 16], + priority: Some(1), + origin: Some(bs("lambda")), + sampling_mechanism: Some(4), + spans: vec![make_span("svc", "op", 1)], + ..Default::default() + }; + let payload = TracerPayloadBytes { + chunks: vec![chunk], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + assert!( + encoded.windows(b"lambda".len()).any(|w| w == b"lambda"), + "chunk origin should appear" + ); + // sampling_mechanism=4 → SAMPLING_MECHANISM (0x07) + positive fixint 0x04 + let want = [chunk_key::SAMPLING_MECHANISM, 0x04]; + assert!(encoded.windows(2).any(|w| w == want)); + } + + #[test] + fn span_kind_otel_values() { + for (kind, expected_byte) in [ + (SpanKind::Internal, 0x01u8), + (SpanKind::Server, 0x02), + (SpanKind::Client, 0x03), + (SpanKind::Producer, 0x04), + (SpanKind::Consumer, 0x05), + ] { + let span = V1Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1, + duration: 1, + span_kind: kind, + ..Default::default() + }; + let payload = TracerPayloadBytes { + chunks: vec![make_chunk(vec![span], [0u8; 16])], + ..Default::default() + }; + let encoded = to_vec_from_payload(&payload); + let want = [0x10u8, expected_byte]; + assert!( + encoded.windows(2).any(|w| w == want), + "SpanKind {kind:?} should produce byte {expected_byte:#x}" + ); + } + } + + #[test] + fn string_interning_works_across_chunks() { + // The string "shared" appears in two chunks. The second occurrence must be a uint ID, + // not a fresh str. Compare against a baseline with a single occurrence to verify. + let chunk_with_two = TracerPayloadBytes { + chunks: vec![ + make_chunk(vec![make_span("shared", "op1", 1)], [0u8; 16]), + make_chunk(vec![make_span("shared", "op2", 2)], [0u8; 16]), + ], + ..Default::default() + }; + let single = TracerPayloadBytes { + chunks: vec![make_chunk(vec![make_span("shared", "op1", 1)], [0u8; 16])], + ..Default::default() + }; + let two = to_vec_from_payload(&chunk_with_two); + let one = to_vec_from_payload(&single); + assert!( + two.len() < 2 * one.len(), + "interning should reduce repeated payload size" + ); + } +} + +#[cfg(test)] +mod cross_validation_tests { + //! Cross-validates that the M1 encoder (v0.4 spans → V1 payload) and the M3 encoder + //! (v1::Span → V1 payload) produce **byte-identical** output for equivalent inputs. + //! + //! All tests are limited to deterministic content (at most one attribute key per map) so the + //! `HashMap` iteration order cannot diverge between the two inputs. + + use super::*; + use crate::span::v04::SpanBytes as V04Span; + use crate::span::v1::{ + AttributeValue, SpanBytes as V1SpanBytes, SpanKind, TraceChunkBytes, TracerPayloadBytes, + }; + use libdd_tinybytes::BytesString; + + fn bs(s: &str) -> BytesString { + BytesString::from_slice(s.as_bytes()).unwrap_or_default() + } + + /// Builds a 128-bit big-endian trace_id from `(high, low)` 64-bit halves. + fn tid_bytes(high: u64, low: u64) -> [u8; 16] { + let mut out = [0u8; 16]; + out[..8].copy_from_slice(&high.to_be_bytes()); + out[8..].copy_from_slice(&low.to_be_bytes()); + out + } + + /// Asserts that encoding `v04` (with `metadata`) via M1 produces the same bytes as + /// encoding `v1` via M3. Includes a hex-diff message on mismatch. + #[track_caller] + fn assert_byte_equal( + v04_traces: &[Vec], + metadata: &TracerMetadata, + v1_payload: &TracerPayloadBytes, + ) { + let m1 = to_vec(v04_traces, metadata); + let m3 = to_vec_from_payload(v1_payload); + if m1 != m3 { + panic!( + "M1 and M3 encoders diverged:\n M1 ({:3} bytes): {}\n M3 ({:3} bytes): {}", + m1.len(), + hex_dump(&m1), + m3.len(), + hex_dump(&m3) + ); + } + } + + fn hex_dump(b: &[u8]) -> String { + b.iter().map(|c| format!("{c:02x}")).collect::() + } + + #[test] + fn empty_payload_byte_identical() { + let v04: Vec> = vec![]; + let v1 = TracerPayloadBytes::default(); + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn minimal_single_span_byte_identical() { + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 0x42, + span_id: 1, + start: 1_000_000, + duration: 500, + ..Default::default() + }]]; + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 0x42), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1_000_000, + duration: 500, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn span_with_parent_and_error_byte_identical() { + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 2, + parent_id: 1, + start: 1000, + duration: 100, + error: 1, + ..Default::default() + }]]; + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 2, + parent_id: 1, + start: 1000, + duration: 100, + error: true, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn promoted_fields_byte_identical() { + // M1 reads env/version/component/span.kind from v04 meta and promotes them; M3 takes + // them directly from the v1::Span fields. Both must produce the same bytes. + let mut meta = HashMap::new(); + meta.insert(bs("env"), bs("prod")); + meta.insert(bs("version"), bs("1.2.3")); + meta.insert(bs("component"), bs("flask")); + meta.insert(bs("span.kind"), bs("server")); + + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + meta, + ..Default::default() + }]]; + + // metadata.env populated → M1 picks env from metadata first (it's set on the builder). + let metadata = TracerMetadata { + env: "prod".to_string(), + app_version: "1.2.3".to_string(), + ..Default::default() + }; + + let v1 = TracerPayloadBytes { + env: bs("prod"), + app_version: bs("1.2.3"), + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + env: bs("prod"), + version: bs("1.2.3"), + component: bs("flask"), + span_kind: SpanKind::Server, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &metadata, &v1); + } + + #[test] + fn single_string_meta_attribute_byte_identical() { + // One non-promoted meta tag → one attribute triplet. With a single entry the HashMap + // iteration order cannot vary. + let mut meta = HashMap::new(); + meta.insert(bs("custom.tag"), bs("hello")); + + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + meta, + ..Default::default() + }]]; + + let mut attrs = HashMap::new(); + attrs.insert(bs("custom.tag"), AttributeValue::String(bs("hello"))); + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + attributes: attrs, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn single_float_metric_byte_identical() { + let mut metrics = HashMap::new(); + metrics.insert(bs("score"), 1.5f64); + + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + metrics, + ..Default::default() + }]]; + + let mut attrs = HashMap::new(); + attrs.insert(bs("score"), AttributeValue::Float(1.5)); + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + attributes: attrs, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn single_bytes_meta_struct_byte_identical() { + let mut meta_struct = HashMap::new(); + meta_struct.insert( + bs("payload"), + libdd_tinybytes::Bytes::copy_from_slice(b"\xde\xad\xbe\xef"), + ); + + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + meta_struct, + ..Default::default() + }]]; + + let mut attrs = HashMap::new(); + attrs.insert( + bs("payload"), + AttributeValue::Bytes(libdd_tinybytes::Bytes::copy_from_slice(b"\xde\xad\xbe\xef")), + ); + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + attributes: attrs, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn chunk_origin_only_byte_identical() { + // The M1 encoder's `is_promoted` filter only strips env/version/component/span.kind/ + // _dd.p.tid — it intentionally keeps `_dd.origin` in span attributes even though it's + // also lifted to the chunk. M3 must reproduce that duplication for byte equality. + let mut meta = HashMap::new(); + meta.insert(bs("_dd.origin"), bs("lambda")); + + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + meta, + ..Default::default() + }]]; + + let mut attrs = HashMap::new(); + attrs.insert(bs("_dd.origin"), AttributeValue::String(bs("lambda"))); + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + origin: Some(bs("lambda")), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + attributes: attrs, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn trace_id_128_bit_from_dd_p_tid_byte_identical() { + let mut meta = HashMap::new(); + meta.insert(bs("_dd.p.tid"), bs("640cfd5400000000")); + + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 0x0123456789abcdef, + span_id: 1, + start: 1000, + duration: 100, + meta, + ..Default::default() + }]]; + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0x640cfd5400000000, 0x0123456789abcdef), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn tracer_metadata_fields_byte_identical() { + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + ..Default::default() + }]]; + let metadata = TracerMetadata { + language: "python".to_string(), + language_version: "3.11".to_string(), + tracer_version: "2.0.0".to_string(), + runtime_id: "abc-uuid".to_string(), + hostname: "h1".to_string(), + ..Default::default() + }; + + let v1 = TracerPayloadBytes { + language_name: bs("python"), + language_version: bs("3.11"), + tracer_version: bs("2.0.0"), + runtime_id: bs("abc-uuid"), + hostname: bs("h1"), + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &metadata, &v1); + } + + #[test] + fn payload_attribute_git_commit_sha_byte_identical() { + let v04 = vec![vec![V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + ..Default::default() + }]]; + let metadata = TracerMetadata { + git_commit_sha: "abc123".to_string(), + ..Default::default() + }; + + let mut payload_attrs = HashMap::new(); + payload_attrs.insert( + bs("_dd.git.commit.sha"), + AttributeValue::String(bs("abc123")), + ); + + let v1 = TracerPayloadBytes { + attributes: payload_attrs, + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &metadata, &v1); + } + + #[test] + fn span_with_single_link_byte_identical() { + let v04_span = V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + span_links: vec![crate::span::v04::SpanLink { + trace_id: 0x0123456789abcdef, + trace_id_high: 0, + span_id: 99, + tracestate: bs("running"), + flags: 0, + attributes: HashMap::new(), + }], + ..Default::default() + }; + let v04 = vec![vec![v04_span]]; + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + span_links: vec![crate::span::v1::SpanLinkBytes { + trace_id: tid_bytes(0, 0x0123456789abcdef), + span_id: 99, + tracestate: bs("running"), + flags: 0, + attributes: HashMap::new(), + }], + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } + + #[test] + fn span_with_single_event_byte_identical() { + use crate::span::v04::{AttributeAnyValue, AttributeArrayValue}; + + let v04_span = V04Span { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + trace_id: 1, + span_id: 1, + start: 1000, + duration: 100, + span_events: vec![crate::span::v04::SpanEvent { + time_unix_nano: 42, + name: bs("exception"), + attributes: HashMap::from([( + bs("exception.message"), + AttributeAnyValue::SingleValue(AttributeArrayValue::String(bs("boom"))), + )]), + }], + ..Default::default() + }; + let v04 = vec![vec![v04_span]]; + + let v1 = TracerPayloadBytes { + chunks: vec![TraceChunkBytes { + trace_id: tid_bytes(0, 1), + spans: vec![V1SpanBytes { + service: bs("svc"), + name: bs("op"), + resource: bs("res"), + span_id: 1, + start: 1000, + duration: 100, + span_events: vec![crate::span::v1::SpanEventBytes { + time_unix_nano: 42, + name: bs("exception"), + attributes: HashMap::from([( + bs("exception.message"), + AttributeValue::String(bs("boom")), + )]), + }], + ..Default::default() + }], + ..Default::default() + }], + ..Default::default() + }; + + assert_byte_equal(&v04, &TracerMetadata::default(), &v1); + } +} diff --git a/libdd-trace-utils/src/msgpack_encoder/v1/span_v04.rs b/libdd-trace-utils/src/msgpack_encoder/v1/span_v04.rs index 2c5962f8f1..022f80a7af 100644 --- a/libdd-trace-utils/src/msgpack_encoder/v1/span_v04.rs +++ b/libdd-trace-utils/src/msgpack_encoder/v1/span_v04.rs @@ -61,8 +61,6 @@ pub(super) enum AnyValueKey { Int64 = 4, Bytes = 5, Array = 6, - /// Not used in V04→V1 conversion (V04 has no key-value list type), defined for completeness. - #[allow(dead_code)] KeyValueList = 7, } diff --git a/libdd-trace-utils/src/msgpack_encoder/v1/span_v1.rs b/libdd-trace-utils/src/msgpack_encoder/v1/span_v1.rs new file mode 100644 index 0000000000..982342fc21 --- /dev/null +++ b/libdd-trace-utils/src/msgpack_encoder/v1/span_v1.rs @@ -0,0 +1,256 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! V1 msgpack encoder that consumes the [`crate::span::v1`] data model. +//! +//! The byte layout matches [`super::span_v04`] so equivalent inputs produce byte-identical output. + +use crate::span::v1::{AttributeValue, Span, SpanEvent, SpanLink}; +use crate::span::TraceData; +use rmp::encode::{ + write_array_len, write_bin, write_bool, write_f64, write_map_len, write_sint, write_u64, + write_uint, write_uint8, RmpWrite, ValueWriteError, +}; +use std::borrow::Borrow; + +use super::span_v04::{AnyValueKey, SpanEventKey, SpanKey, SpanLinkKey}; +use super::StringTable; + +/// Encodes a typed [`AttributeValue`] as `[type_uint8, value]`. +pub(super) fn encode_attribute_value( + writer: &mut W, + value: &AttributeValue, + table: &mut StringTable, +) -> Result<(), ValueWriteError> { + match value { + AttributeValue::String(s) => { + write_uint8(writer, AnyValueKey::String as u8)?; + table.write_interned(writer, s.borrow())?; + } + AttributeValue::Bool(b) => { + write_uint8(writer, AnyValueKey::Bool as u8)?; + write_bool(writer, *b).map_err(ValueWriteError::InvalidDataWrite)?; + } + AttributeValue::Float(f) => { + write_uint8(writer, AnyValueKey::Double as u8)?; + write_f64(writer, *f)?; + } + AttributeValue::Int(i) => { + write_uint8(writer, AnyValueKey::Int64 as u8)?; + write_sint(writer, *i)?; + } + AttributeValue::Bytes(b) => { + write_uint8(writer, AnyValueKey::Bytes as u8)?; + write_bin(writer, b.borrow())?; + } + AttributeValue::List(arr) => { + write_uint8(writer, AnyValueKey::Array as u8)?; + write_array_len(writer, arr.len() as u32)?; + for v in arr { + encode_attribute_value(writer, v, table)?; + } + } + AttributeValue::KeyValue(map) => { + write_uint8(writer, AnyValueKey::KeyValueList as u8)?; + write_map_len(writer, map.len() as u32)?; + for (k, v) in map { + table.write_interned(writer, k.borrow())?; + encode_attribute_value(writer, v, table)?; + } + } + } + Ok(()) +} + +/// Encodes a flat triplet attributes array: `[key, type_uint8, value, ...]`. +pub(super) fn encode_attributes_map( + writer: &mut W, + map: &std::collections::HashMap>, + table: &mut StringTable, +) -> Result<(), ValueWriteError> { + write_array_len(writer, (map.len() as u32) * 3)?; + for (k, v) in map { + table.write_interned(writer, k.borrow())?; + encode_attribute_value(writer, v, table)?; + } + Ok(()) +} + +/// Encodes span links from the V1 data model. +pub(super) fn encode_span_links( + writer: &mut W, + span_links: &[SpanLink], + table: &mut StringTable, +) -> Result<(), ValueWriteError> { + write_uint8(writer, SpanKey::SpanLinks as u8)?; + write_array_len(writer, span_links.len() as u32)?; + + for link in span_links { + let link_len = 1 // trace_id (always) + + (link.span_id != 0) as u32 + + (!link.attributes.is_empty()) as u32 + + (!link.tracestate.borrow().is_empty()) as u32 + + (link.flags != 0) as u32; + + write_map_len(writer, link_len)?; + + write_uint8(writer, SpanLinkKey::TraceId as u8)?; + write_bin(writer, &link.trace_id)?; + + if link.span_id != 0 { + write_uint8(writer, SpanLinkKey::SpanId as u8)?; + write_u64(writer, link.span_id)?; + } + + if !link.attributes.is_empty() { + write_uint8(writer, SpanLinkKey::Attributes as u8)?; + encode_attributes_map(writer, &link.attributes, table)?; + } + + if !link.tracestate.borrow().is_empty() { + write_uint8(writer, SpanLinkKey::TraceState as u8)?; + table.write_interned(writer, link.tracestate.borrow())?; + } + + if link.flags != 0 { + write_uint8(writer, SpanLinkKey::Flags as u8)?; + write_uint(writer, link.flags as u64)?; + } + } + + Ok(()) +} + +/// Encodes span events from the V1 data model. +pub(super) fn encode_span_events( + writer: &mut W, + span_events: &[SpanEvent], + table: &mut StringTable, +) -> Result<(), ValueWriteError> { + write_uint8(writer, SpanKey::SpanEvents as u8)?; + write_array_len(writer, span_events.len() as u32)?; + + for event in span_events { + let event_len = 2 // time + name + + (!event.attributes.is_empty()) as u32; + + write_map_len(writer, event_len)?; + + write_uint8(writer, SpanEventKey::Time as u8)?; + write_u64(writer, event.time_unix_nano)?; + + write_uint8(writer, SpanEventKey::Name as u8)?; + table.write_interned(writer, event.name.borrow())?; + + if !event.attributes.is_empty() { + write_uint8(writer, SpanEventKey::Attributes as u8)?; + encode_attributes_map(writer, &event.attributes, table)?; + } + } + + Ok(()) +} + +/// Encodes a [`Span`] (V1 data model) into V1 msgpack. +pub(super) fn encode_span( + writer: &mut W, + span: &Span, + table: &mut StringTable, +) -> Result<(), ValueWriteError> { + let is_parent = span.parent_id != 0; + let has_duration = span.duration != 0; + let has_error = span.error; + let has_attributes = !span.attributes.is_empty(); + let has_env = !span.env.borrow().is_empty(); + let has_version = !span.version.borrow().is_empty(); + let has_component = !span.component.borrow().is_empty(); + + let span_len = 3 // span_id, start, kind — always present + + (!span.service.borrow().is_empty()) as u32 + + (!span.name.borrow().is_empty()) as u32 + + (!span.resource.borrow().is_empty()) as u32 + + (!span.r#type.borrow().is_empty()) as u32 + + is_parent as u32 + + has_duration as u32 + + has_error as u32 + + has_attributes as u32 + + (!span.span_links.is_empty()) as u32 + + (!span.span_events.is_empty()) as u32 + + has_env as u32 + + has_version as u32 + + has_component as u32; + + write_map_len(writer, span_len)?; + + if !span.service.borrow().is_empty() { + write_uint8(writer, SpanKey::Service as u8)?; + table.write_interned(writer, span.service.borrow())?; + } + + if !span.name.borrow().is_empty() { + write_uint8(writer, SpanKey::Name as u8)?; + table.write_interned(writer, span.name.borrow())?; + } + + if !span.resource.borrow().is_empty() { + write_uint8(writer, SpanKey::Resource as u8)?; + table.write_interned(writer, span.resource.borrow())?; + } + + write_uint8(writer, SpanKey::SpanId as u8)?; + write_u64(writer, span.span_id)?; + + write_uint8(writer, SpanKey::Start as u8)?; + write_u64(writer, span.start as u64)?; + + if is_parent { + write_uint8(writer, SpanKey::ParentId as u8)?; + write_u64(writer, span.parent_id)?; + } + + if has_duration { + write_uint8(writer, SpanKey::Duration as u8)?; + write_u64(writer, span.duration.max(0) as u64)?; + } + + if has_error { + write_uint8(writer, SpanKey::Error as u8)?; + write_bool(writer, true).map_err(ValueWriteError::InvalidDataWrite)?; + } + + if !span.r#type.borrow().is_empty() { + write_uint8(writer, SpanKey::Type as u8)?; + table.write_interned(writer, span.r#type.borrow())?; + } + + if has_attributes { + write_uint8(writer, SpanKey::Attributes as u8)?; + encode_attributes_map(writer, &span.attributes, table)?; + } + + if !span.span_links.is_empty() { + encode_span_links(writer, &span.span_links, table)?; + } + + if !span.span_events.is_empty() { + encode_span_events(writer, &span.span_events, table)?; + } + + if has_env { + write_uint8(writer, SpanKey::Env as u8)?; + table.write_interned(writer, span.env.borrow())?; + } + if has_version { + write_uint8(writer, SpanKey::Version as u8)?; + table.write_interned(writer, span.version.borrow())?; + } + if has_component { + write_uint8(writer, SpanKey::Component as u8)?; + table.write_interned(writer, span.component.borrow())?; + } + // SpanKind is always emitted (default = Internal). + write_uint8(writer, SpanKey::Kind as u8)?; + write_uint(writer, span.span_kind as u64)?; + + Ok(()) +} diff --git a/libdd-trace-utils/src/span/mod.rs b/libdd-trace-utils/src/span/mod.rs index e6358dfc7a..ceb68e5b53 100644 --- a/libdd-trace-utils/src/span/mod.rs +++ b/libdd-trace-utils/src/span/mod.rs @@ -4,6 +4,7 @@ pub mod trace_utils; pub mod v04; pub mod v05; +pub mod v1; use crate::msgpack_decoder::decode::buffer::read_string_ref_nomut; use crate::msgpack_decoder::decode::error::DecodeError; diff --git a/libdd-trace-utils/src/span/v1/mod.rs b/libdd-trace-utils/src/span/v1/mod.rs new file mode 100644 index 0000000000..bef124ee0a --- /dev/null +++ b/libdd-trace-utils/src/span/v1/mod.rs @@ -0,0 +1,302 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::span::{BytesData, SliceData, TraceData}; +use std::collections::HashMap; + +/// OpenTelemetry SpanKind values, encoded on the wire as a `uint32`. +/// Unset or unrecognized kinds default to [`SpanKind::Internal`]. +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SpanKind { + #[default] + Internal = 1, + Server = 2, + Client = 3, + Producer = 4, + Consumer = 5, +} + +impl SpanKind { + /// Parses a v0.4 `span.kind` meta value into a [`SpanKind`]. + /// Unrecognized values map to [`SpanKind::Internal`]. + pub fn from_meta(s: &str) -> Self { + match s { + "server" => SpanKind::Server, + "client" => SpanKind::Client, + "producer" => SpanKind::Producer, + "consumer" => SpanKind::Consumer, + _ => SpanKind::Internal, + } + } +} + +/// Typed V1 attribute value. +/// Replaces v0.4's split `meta` / `metrics` / `meta_struct` maps. +#[derive(Debug, PartialEq)] +pub enum AttributeValue { + String(T::Text), + Float(f64), + Int(i64), + Bool(bool), + Bytes(T::Bytes), + KeyValue(HashMap>), + List(Vec>), +} + +impl Clone for AttributeValue +where + T::Text: Clone, + T::Bytes: Clone, +{ + fn clone(&self) -> Self { + match self { + AttributeValue::String(v) => AttributeValue::String(v.clone()), + AttributeValue::Float(v) => AttributeValue::Float(*v), + AttributeValue::Int(v) => AttributeValue::Int(*v), + AttributeValue::Bool(v) => AttributeValue::Bool(*v), + AttributeValue::Bytes(v) => AttributeValue::Bytes(v.clone()), + AttributeValue::KeyValue(m) => AttributeValue::KeyValue(m.clone()), + AttributeValue::List(v) => AttributeValue::List(v.clone()), + } + } +} + +/// The generic representation of a V1 span. +/// +/// `T` is the type used to represent strings in the span, it can be either owned (e.g. +/// BytesString) or borrowed (e.g. &str). To define a generic function taking any `Span` you can +/// use the [`TraceData`] trait: +/// ``` +/// use libdd_trace_utils::span::{v1::Span, TraceData}; +/// fn foo(span: Span) { +/// let _ = span.attributes.get("foo"); +/// } +/// ``` +#[derive(Debug, PartialEq, Default)] +pub struct Span { + pub service: T::Text, + pub name: T::Text, + pub resource: T::Text, + pub r#type: T::Text, + /// 128-bit trace ID stored as big-endian bytes. + pub trace_id: [u8; 16], + pub span_id: u64, + pub parent_id: u64, + pub start: i64, + pub duration: i64, + pub error: bool, + pub span_kind: SpanKind, + pub env: T::Text, + pub version: T::Text, + pub component: T::Text, + pub attributes: HashMap>, + pub span_links: Vec>, + pub span_events: Vec>, +} + +impl Clone for Span +where + T::Text: Clone, + T::Bytes: Clone, +{ + fn clone(&self) -> Self { + Span { + service: self.service.clone(), + name: self.name.clone(), + resource: self.resource.clone(), + r#type: self.r#type.clone(), + trace_id: self.trace_id, + span_id: self.span_id, + parent_id: self.parent_id, + start: self.start, + duration: self.duration, + error: self.error, + span_kind: self.span_kind, + env: self.env.clone(), + version: self.version.clone(), + component: self.component.clone(), + attributes: self.attributes.clone(), + span_links: self.span_links.clone(), + span_events: self.span_events.clone(), + } + } +} + +/// The generic representation of a V1 span link. +/// `T` is the type used to represent strings in the span link. +#[derive(Debug, PartialEq, Default)] +pub struct SpanLink { + pub trace_id: [u8; 16], + pub span_id: u64, + pub attributes: HashMap>, + pub tracestate: T::Text, + pub flags: u32, +} + +impl Clone for SpanLink +where + T::Text: Clone, + T::Bytes: Clone, +{ + fn clone(&self) -> Self { + SpanLink { + trace_id: self.trace_id, + span_id: self.span_id, + attributes: self.attributes.clone(), + tracestate: self.tracestate.clone(), + flags: self.flags, + } + } +} + +/// The generic representation of a V1 span event. +/// `T` is the type used to represent strings in the span event. +#[derive(Debug, PartialEq, Default)] +pub struct SpanEvent { + pub time_unix_nano: u64, + pub name: T::Text, + pub attributes: HashMap>, +} + +impl Clone for SpanEvent +where + T::Text: Clone, + T::Bytes: Clone, +{ + fn clone(&self) -> Self { + SpanEvent { + time_unix_nano: self.time_unix_nano, + name: self.name.clone(), + attributes: self.attributes.clone(), + } + } +} + +/// A V1 trace chunk: a group of spans sharing the same `trace_id`, plus chunk-level metadata. +#[derive(Debug, PartialEq, Default)] +pub struct TraceChunk { + pub trace_id: [u8; 16], + pub priority: Option, + pub origin: Option, + pub sampling_mechanism: Option, + pub dropped_trace: bool, + pub attributes: HashMap>, + pub spans: Vec>, +} + +impl Clone for TraceChunk +where + T::Text: Clone, + T::Bytes: Clone, +{ + fn clone(&self) -> Self { + TraceChunk { + trace_id: self.trace_id, + priority: self.priority, + origin: self.origin.clone(), + sampling_mechanism: self.sampling_mechanism, + dropped_trace: self.dropped_trace, + attributes: self.attributes.clone(), + spans: self.spans.clone(), + } + } +} + +/// A V1 tracer payload: tracer-level metadata and the trace chunks it carries. +#[derive(Debug, PartialEq, Default)] +pub struct TracerPayload { + pub language_name: T::Text, + pub language_version: T::Text, + pub tracer_version: T::Text, + pub runtime_id: T::Text, + pub env: T::Text, + pub hostname: T::Text, + pub app_version: T::Text, + pub attributes: HashMap>, + pub chunks: Vec>, +} + +impl Clone for TracerPayload +where + T::Text: Clone, + T::Bytes: Clone, +{ + fn clone(&self) -> Self { + TracerPayload { + language_name: self.language_name.clone(), + language_version: self.language_version.clone(), + tracer_version: self.tracer_version.clone(), + runtime_id: self.runtime_id.clone(), + env: self.env.clone(), + hostname: self.hostname.clone(), + app_version: self.app_version.clone(), + attributes: self.attributes.clone(), + chunks: self.chunks.clone(), + } + } +} + +pub type SpanBytes = Span; +pub type SpanLinkBytes = SpanLink; +pub type SpanEventBytes = SpanEvent; +pub type AttributeValueBytes = AttributeValue; +pub type TraceChunkBytes = TraceChunk; +pub type TracerPayloadBytes = TracerPayload; + +pub type SpanSlice<'a> = Span>; +pub type SpanLinkSlice<'a> = SpanLink>; +pub type SpanEventSlice<'a> = SpanEvent>; +pub type AttributeValueSlice<'a> = AttributeValue>; +pub type TraceChunkSlice<'a> = TraceChunk>; +pub type TracerPayloadSlice<'a> = TracerPayload>; + +#[cfg(test)] +mod tests { + use super::*; + use libdd_tinybytes::BytesString; + + #[test] + fn span_kind_default_is_internal() { + assert_eq!(SpanKind::default(), SpanKind::Internal); + } + + #[test] + fn span_kind_from_meta() { + assert_eq!(SpanKind::from_meta("server"), SpanKind::Server); + assert_eq!(SpanKind::from_meta("client"), SpanKind::Client); + assert_eq!(SpanKind::from_meta("producer"), SpanKind::Producer); + assert_eq!(SpanKind::from_meta("consumer"), SpanKind::Consumer); + assert_eq!(SpanKind::from_meta("internal"), SpanKind::Internal); + assert_eq!(SpanKind::from_meta(""), SpanKind::Internal); + assert_eq!(SpanKind::from_meta("anything-else"), SpanKind::Internal); + } + + #[test] + fn span_kind_repr_matches_otel_spec() { + assert_eq!(SpanKind::Internal as u32, 1); + assert_eq!(SpanKind::Server as u32, 2); + assert_eq!(SpanKind::Client as u32, 3); + assert_eq!(SpanKind::Producer as u32, 4); + assert_eq!(SpanKind::Consumer as u32, 5); + } + + #[test] + fn span_default_has_zero_trace_id_and_internal_kind() { + let s = SpanBytes::default(); + assert_eq!(s.trace_id, [0u8; 16]); + assert_eq!(s.span_kind, SpanKind::Internal); + assert!(!s.error); + assert!(s.attributes.is_empty()); + } + + #[test] + fn attribute_value_clone_preserves_variants() { + let s = AttributeValueBytes::String(BytesString::from_static("v")); + assert_eq!(s.clone(), s); + let n = AttributeValueBytes::Int(42); + assert_eq!(n.clone(), n); + let list = AttributeValueBytes::List(vec![AttributeValueBytes::Bool(true)]); + assert_eq!(list.clone(), list); + } +}