From 395f700554d463977ab41d1dfec538e60f8ac514 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 1 Apr 2026 14:41:05 +0200 Subject: [PATCH 1/8] feat: root_span_id handling in otel thread ctx --- libdd-library-config/src/tracer_metadata.rs | 31 ++-- libdd-profiling/src/otel_thread_ctx.rs | 162 +++++++++++++------- 2 files changed, 127 insertions(+), 66 deletions(-) diff --git a/libdd-library-config/src/tracer_metadata.rs b/libdd-library-config/src/tracer_metadata.rs index 130a347d98..25cc2bbb27 100644 --- a/libdd-library-config/src/tracer_metadata.rs +++ b/libdd-library-config/src/tracer_metadata.rs @@ -34,15 +34,20 @@ pub struct TracerMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub container_id: Option, /// Ordered list of attribute key names for thread-level context records. Key indices from - /// thread context records index into this table. Keep empty if thread-level context is not - /// used. + /// thread context records index into this table. Set to `None` to disable thread-level related + /// attributes to the process-level context. + /// + /// If set to `Some`, the first key will be automatically to be `datadog.local_root_span_id` in + /// the OTel process context, because the thread context handling elsewhere in libdatadog + /// relies on this key's index to be zero. Only set additional keys in + /// `threadlocal_attribute_keys`; the root span id is considered to be always implicitly here. /// /// This field is specific to OTel process context. It is ignored for (de)serialization, and is /// only used when converting to an OTel process context in /// [TracerMetadata::to_otel_process_ctx]. #[cfg(feature = "otel-thread-ctx")] #[serde(skip)] - pub threadlocal_attribute_keys: Vec, + pub threadlocal_attribute_keys: Option>, } impl Default for TracerMetadata { @@ -59,7 +64,7 @@ impl Default for TracerMetadata { process_tags: None, container_id: None, #[cfg(feature = "otel-thread-ctx")] - threadlocal_attribute_keys: vec![], + threadlocal_attribute_keys: None, } } } @@ -124,21 +129,25 @@ impl TracerMetadata { ]; #[cfg(feature = "otel-thread-ctx")] - if !threadlocal_attribute_keys.is_empty() { + if let Some(threadlocal_attribute_keys) = threadlocal_attribute_keys.as_ref() { attributes.push(key_value( "threadlocal.schema_version", "tlsdesc_v1_dev".to_owned(), )); + attributes.push(KeyValue { key: "threadlocal.attribute_key_map".to_owned(), value: Some(AnyValue { value: Some(any_value::Value::ArrayValue(ArrayValue { - values: threadlocal_attribute_keys - .iter() - .map(|k| AnyValue { - value: Some(any_value::Value::StringValue(k.clone())), - }) - .collect(), + values: std::iter::once(AnyValue { + value: Some(any_value::Value::StringValue( + "datadog.root_span_id".to_owned(), + )), + }) + .chain(threadlocal_attribute_keys.iter().map(|k| AnyValue { + value: Some(any_value::Value::StringValue(k.clone())), + })) + .collect(), })), }), key_ref: 0, diff --git a/libdd-profiling/src/otel_thread_ctx.rs b/libdd-profiling/src/otel_thread_ctx.rs index 3ef14e42e2..901529176c 100644 --- a/libdd-profiling/src/otel_thread_ctx.rs +++ b/libdd-profiling/src/otel_thread_ctx.rs @@ -107,6 +107,10 @@ pub mod linux { } } + // We maintain the convention in libdatadog that the `root_span_id` attribute key is always the + // very first in the string table, so its key index is guaranteed to be zero. + const ROOT_SPAN_KEY_INDEX: u8 = 0; + /// Maximum size in bytes of the `attrs_data` field. /// /// Chosen so that the total record size (`28 + MAX_ATTRS_DATA_SIZE`) stays within the 640-byte @@ -165,8 +169,15 @@ pub mod linux { } impl ThreadContextRecord { - /// Build a record with the given trace id, span id and attributes. - pub fn new(trace_id: [u8; 16], span_id: [u8; 8], attrs: &[(u8, &str)]) -> Self { + /// Build a record with the given trace id, span id and attributes. The `root_span_id` is a + /// distinguished attribute with special handling for convenience, but it ends up as other + /// attributes in `attrs_data`. + fn new( + trace_id: [u8; 16], + span_id: [u8; 8], + root_span_id: &str, + attrs: &[(u8, &str)], + ) -> Self { const { assert!(size_of::() == 640) } let mut record = Self { @@ -174,7 +185,7 @@ pub mod linux { span_id, ..Default::default() }; - record.set_attrs(attrs); + record.set_attrs(root_span_id, attrs); record } @@ -199,11 +210,13 @@ pub mod linux { /// recovery would require us to be able to rollback to the previous attributes which would /// hurt the happy path, or leave the record in a inconsistent state. Another possibility /// would be to error out and reset the record in that situation. - pub fn set_attrs(&mut self, attributes: &[(u8, &str)]) -> bool { + fn set_attrs(&mut self, root_span_id: &str, attributes: &[(u8, &str)]) -> bool { let mut offset = 0; let mut fully_encoded = true; + let root_span_attr = (ROOT_SPAN_KEY_INDEX, root_span_id); + let attrs = std::iter::once(&root_span_attr).chain(attributes); - for &(key_index, val) in attributes { + for &(key_index, val) in attrs { let val_bytes = val.as_bytes(); let val_len = val_bytes.len(); let val_len = if val_len > 255 { @@ -261,8 +274,18 @@ pub mod linux { impl ThreadContext { /// Create a new thread context with the given trace/span IDs and encoded attributes. - pub fn new(trace_id: [u8; 16], span_id: [u8; 8], attrs: &[(u8, &str)]) -> Self { - Self::from(ThreadContextRecord::new(trace_id, span_id, attrs)) + pub fn new( + trace_id: [u8; 16], + span_id: [u8; 8], + root_span_id: &str, + attrs: &[(u8, &str)], + ) -> Self { + Self::from(ThreadContextRecord::new( + trace_id, + span_id, + root_span_id, + attrs, + )) } /// Turn this thread context into a raw pointer to the underlying [ThreadContextRecord]. @@ -319,7 +342,7 @@ pub mod linux { /// also observes `valid = 1`. pub fn attach(self) -> Option { // [^tls-slot-ordering]: since we get back the previous context, we should in principle - // use an `Acquire` compiler fence to make sure we don't get back a not-yet-initiliazed + // use an `Acquire` compiler fence to make sure we don't get back a not-yet-initialized // record. // // However, this thread (excluding the reader signal handler) is the only one to ever @@ -335,7 +358,12 @@ pub mod linux { /// /// If there's currently no attached context, `update` will create one, and is in this case /// equivalent to `ThreadContext::new(trace_id, span_id, attrs).attach()`. - pub fn update(trace_id: [u8; 16], span_id: [u8; 8], attrs: &[(u8, &str)]) { + pub fn update( + trace_id: [u8; 16], + span_id: [u8; 8], + root_span_id: &str, + attrs: &[(u8, &str)], + ) { let slot = get_tls_slot(); if let Some(current) = unsafe { slot.load(Ordering::Relaxed).as_mut() } { @@ -344,7 +372,7 @@ pub mod linux { current.trace_id = trace_id; current.span_id = span_id; - current.set_attrs(attrs); + current.set_attrs(root_span_id, attrs); compiler_fence(Ordering::SeqCst); current.valid.store(1, Ordering::Relaxed); @@ -354,7 +382,7 @@ pub mod linux { // `ThreadContext::new` already initialises `valid = 1`. let _ = Self::swap( slot, - ThreadContext::new(trace_id, span_id, attrs).into_raw(), + ThreadContext::new(trace_id, span_id, root_span_id, attrs).into_raw(), ); } } @@ -370,7 +398,7 @@ pub mod linux { impl Drop for ThreadContext { fn drop(&mut self) { // Safety: `self.0` was obtained from a `Box::new`, and `ThreadContext` represents - // ownership of the underyling memory. + // ownership of the underlying memory. unsafe { let _ = Box::from_raw(self.0.as_ptr()); } @@ -399,7 +427,7 @@ pub mod linux { read_tls_context_ptr().is_null(), "TLS must be null initially" ); - ThreadContext::new(trace_id, span_id, &[]).attach(); + ThreadContext::new(trace_id, span_id, "", &[]).attach(); assert!( !read_tls_context_ptr().is_null(), "TLS must not be null after attach" @@ -430,7 +458,7 @@ pub mod linux { let trace_id = [1u8; 16]; let span_id = [2u8; 8]; - ThreadContext::new(trace_id, span_id, &[]).attach(); + ThreadContext::new(trace_id, span_id, "", &[]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null(), "TLS must be non-null after attach"); @@ -440,7 +468,7 @@ pub mod linux { assert_eq!(record.trace_id, trace_id); assert_eq!(record.span_id, span_id); assert_eq!(record.valid.load(Ordering::Relaxed), 1); - assert_eq!(record.attrs_data_size, 0); + assert_eq!(record.attrs_data_size, 2); let _ = ThreadContext::detach(); } @@ -448,20 +476,24 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn attribute_encoding_basic() { - let attrs: &[(u8, &str)] = &[(0, "GET"), (1, "/api/v1")]; - ThreadContext::new([0u8; 16], [0u8; 8], attrs).attach(); + let root_span_id = "aaaa-bbbb"; + let attrs: &[(u8, &str)] = &[(1, "GET"), (2, "/api/v1")]; + ThreadContext::new([0u8; 16], [0u8; 8], root_span_id, attrs).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - let expected_size: u16 = (2 + 3 + 2 + 7) as u16; + let expected_size: u16 = (2 + 9 + 2 + 3 + 2 + 7) as u16; assert_eq!(record.attrs_data_size, expected_size); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 3); - assert_eq!(&record.attrs_data[2..5], b"GET"); - assert_eq!(record.attrs_data[5], 1); - assert_eq!(record.attrs_data[6], 7); - assert_eq!(&record.attrs_data[7..14], b"/api/v1"); + assert_eq!(record.attrs_data[1], 9); + assert_eq!(&record.attrs_data[2..11], b"aaaa-bbbb"); + assert_eq!(record.attrs_data[11], 1); + assert_eq!(record.attrs_data[12], 3); + assert_eq!(&record.attrs_data[13..16], b"GET"); + assert_eq!(record.attrs_data[16], 2); + assert_eq!(record.attrs_data[17], 7); + assert_eq!(&record.attrs_data[18..25], b"/api/v1"); let _ = ThreadContext::detach(); } @@ -471,29 +503,30 @@ pub mod linux { fn attribute_truncation_on_overflow() { // Build attributes whose combined encoded size exceeds MAX_ATTRS_DATA_SIZE. // Each max entry: 1 (key) + 1 (len) + 255 (val) = 257 bytes. - // Two such entries: 514 bytes. A third entry of 100 chars would need 102 bytes, - // bringing the total to 616 > 612, so the third entry must be dropped. + // Two such entries: 514 bytes, plus an empty root_span_id: 516. + // A third entry of 100 chars would need 102 bytes, bringing the total to 618 > 612, so + // the third entry must be dropped. let val_a = "a".repeat(255); // 257 bytes encoded let val_b = "b".repeat(255); // 257 bytes encoded → 514 total let val_c = "c".repeat(100); // 102 bytes encoded → 616 total: must be dropped let attrs: &[(u8, &str)] = &[ - (0, val_a.as_str()), - (1, val_b.as_str()), - (2, val_c.as_str()), + (1, val_a.as_str()), + (2, val_b.as_str()), + (3, val_c.as_str()), ]; - ThreadContext::new([0u8; 16], [0u8; 8], attrs).attach(); + ThreadContext::new([0u8; 16], [0u8; 8], "", attrs).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - // Only the first two entries fit (514 bytes). - assert_eq!(record.attrs_data_size, 514); - assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 255); - assert_eq!(record.attrs_data[257], 1); - assert_eq!(record.attrs_data[258], 255); + // Only the first two entries fit (514 bytes + 2 bytes for empty root span id). + assert_eq!(record.attrs_data_size, 516); + assert_eq!(record.attrs_data[2], 1); + assert_eq!(record.attrs_data[3], 255); + assert_eq!(record.attrs_data[259], 2); + assert_eq!(record.attrs_data[260], 255); let _ = ThreadContext::detach(); } @@ -501,25 +534,30 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn update_record_in_place() { - let trace_id_1 = [1u8; 16]; - let span_id_1 = [1u8; 8]; - let trace_id_2 = [2u8; 16]; - let span_id_2 = [2u8; 8]; + let trace_id1 = [1u8; 16]; + let span_id1 = [1u8; 8]; + let root_span_id1 = "xxxx"; + let trace_id2 = [2u8; 16]; + let span_id2 = [2u8; 8]; + let root_span_id2 = "yyyy"; // Updating before any context is attached should be equivalent to `attach()` - ThreadContext::update(trace_id_1, span_id_1, &[(0, "v1")]); + ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, "v1")]); let ptr_before = read_tls_context_ptr(); assert!(!ptr_before.is_null()); let record = unsafe { &*ptr_before }; - assert_eq!(record.trace_id, trace_id_1); - assert_eq!(record.span_id, span_id_1); + assert_eq!(record.trace_id, trace_id1); + assert_eq!(record.span_id, span_id1); assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 2); - assert_eq!(&record.attrs_data[2..4], b"v1"); + assert_eq!(record.attrs_data[1], 4); + assert_eq!(&record.attrs_data[2..6], root_span_id1.as_bytes()); + assert_eq!(record.attrs_data[6], 0); + assert_eq!(record.attrs_data[7], 2); + assert_eq!(&record.attrs_data[8..10], b"v1"); - ThreadContext::update(trace_id_2, span_id_2, &[(0, "v2")]); + ThreadContext::update(trace_id2, span_id2, root_span_id2, &[(0, "v2")]); let ptr_after = read_tls_context_ptr(); assert_eq!( @@ -528,12 +566,15 @@ pub mod linux { ); let record = unsafe { &*ptr_after }; - assert_eq!(record.trace_id, trace_id_2); - assert_eq!(record.span_id, span_id_2); + assert_eq!(record.trace_id, trace_id2); + assert_eq!(record.span_id, span_id2); assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 2); - assert_eq!(&record.attrs_data[2..4], b"v2"); + assert_eq!(record.attrs_data[1], 4); + assert_eq!(&record.attrs_data[2..6], root_span_id2.as_bytes()); + assert_eq!(record.attrs_data[6], 0); + assert_eq!(record.attrs_data[7], 2); + assert_eq!(&record.attrs_data[8..10], b"v2"); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -542,7 +583,7 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn explicit_detach_nulls_tls() { - ThreadContext::new([3u8; 16], [3u8; 8], &[]).attach(); + ThreadContext::new([3u8; 16], [3u8; 8], "aaaa", &[]).attach(); assert!(!read_tls_context_ptr().is_null()); let _ = ThreadContext::detach(); @@ -557,14 +598,14 @@ pub mod linux { #[cfg_attr(miri, ignore)] fn long_value_capped_at_255_bytes() { let long_val = "a".repeat(300); - ThreadContext::new([0u8; 16], [0u8; 8], &[(0, long_val.as_str())]).attach(); + ThreadContext::new([0u8; 16], [0u8; 8], "bbbb", &[(0, long_val.as_str())]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - let val_len = record.attrs_data[1] as usize; + let val_len = record.attrs_data[2 + 4 + 1]; assert_eq!(val_len, 255, "value must be capped at 255 bytes"); - assert_eq!(record.attrs_data_size as usize, 2 + 255); + assert_eq!(record.attrs_data_size, 2 + 4 + 2 + 255); let _ = ThreadContext::detach(); } @@ -580,11 +621,14 @@ pub mod linux { let spawned_trace_id = [0xABu8; 16]; let spawned_span_id = [0xCDu8; 8]; + let spawned_root_span_id = "xxxx"; let main_trace_id = [0x11u8; 16]; let main_span_id = [0x22u8; 8]; + let main_root_span_id = "yyyy"; let handle = std::thread::spawn(move || { - ThreadContext::new(spawned_trace_id, spawned_span_id, &[]).attach(); + ThreadContext::new(spawned_trace_id, spawned_span_id, spawned_root_span_id, &[]) + .attach(); // Let the main thread attach its own record and verify its slot. b.wait(); @@ -597,6 +641,10 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, spawned_trace_id); assert_eq!(record.span_id, spawned_span_id); + assert_eq!( + &record.attrs_data[2..(record.attrs_data_size as usize)], + spawned_root_span_id.as_bytes() + ); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -610,13 +658,17 @@ pub mod linux { "main thread should see a null pointer and not another thread's context" ); - ThreadContext::new(main_trace_id, main_span_id, &[]).attach(); + ThreadContext::new(main_trace_id, main_span_id, main_root_span_id, &[]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null(), "main thread TLS must be set"); let record = unsafe { &*ptr }; assert_eq!(record.trace_id, main_trace_id); assert_eq!(record.span_id, main_span_id); + assert_eq!( + &record.attrs_data[2..(record.attrs_data_size as usize)], + main_root_span_id.as_bytes() + ); barrier.wait(); From f80500f5658cd07bdb811be87d4a828d8d0e0cd1 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 1 Apr 2026 14:56:11 +0200 Subject: [PATCH 2/8] chore: small fixes and typos --- libdd-library-config/src/tracer_metadata.rs | 6 +++--- libdd-profiling/src/otel_thread_ctx.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libdd-library-config/src/tracer_metadata.rs b/libdd-library-config/src/tracer_metadata.rs index 25cc2bbb27..2b488fc076 100644 --- a/libdd-library-config/src/tracer_metadata.rs +++ b/libdd-library-config/src/tracer_metadata.rs @@ -37,10 +37,10 @@ pub struct TracerMetadata { /// thread context records index into this table. Set to `None` to disable thread-level related /// attributes to the process-level context. /// - /// If set to `Some`, the first key will be automatically to be `datadog.local_root_span_id` in - /// the OTel process context, because the thread context handling elsewhere in libdatadog + /// If set to `Some`, the first key will be automatically set to `datadog.local_root_span_id` + /// in the OTel process context, because the thread context handling elsewhere in libdatadog /// relies on this key's index to be zero. Only set additional keys in - /// `threadlocal_attribute_keys`; the root span id is considered to be always implicitly here. + /// `threadlocal_attribute_keys`; the root span id is considered to always be here implicitly. /// /// This field is specific to OTel process context. It is ignored for (de)serialization, and is /// only used when converting to an OTel process context in diff --git a/libdd-profiling/src/otel_thread_ctx.rs b/libdd-profiling/src/otel_thread_ctx.rs index 901529176c..9c4c24ab06 100644 --- a/libdd-profiling/src/otel_thread_ctx.rs +++ b/libdd-profiling/src/otel_thread_ctx.rs @@ -214,9 +214,9 @@ pub mod linux { let mut offset = 0; let mut fully_encoded = true; let root_span_attr = (ROOT_SPAN_KEY_INDEX, root_span_id); - let attrs = std::iter::once(&root_span_attr).chain(attributes); + let full_attrs = std::iter::once(&root_span_attr).chain(attributes); - for &(key_index, val) in attrs { + for &(key_index, val) in full_attrs { let val_bytes = val.as_bytes(); let val_len = val_bytes.len(); let val_len = if val_len > 255 { From 8cd66b3951278166a0538694b7153b170088a3de Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 2 Apr 2026 11:43:07 +0200 Subject: [PATCH 3/8] chore: root_span_id -> local_root_span_id: [u8; 8] --- libdd-profiling/src/otel_thread_ctx.rs | 135 +++++++++++++------------ 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/libdd-profiling/src/otel_thread_ctx.rs b/libdd-profiling/src/otel_thread_ctx.rs index 9c4c24ab06..5613be2a02 100644 --- a/libdd-profiling/src/otel_thread_ctx.rs +++ b/libdd-profiling/src/otel_thread_ctx.rs @@ -107,8 +107,8 @@ pub mod linux { } } - // We maintain the convention in libdatadog that the `root_span_id` attribute key is always the - // very first in the string table, so its key index is guaranteed to be zero. + // We maintain the convention in libdatadog that the `local_root_span_id` attribute key is + // always the very first in the string table, so its key index is guaranteed to be zero. const ROOT_SPAN_KEY_INDEX: u8 = 0; /// Maximum size in bytes of the `attrs_data` field. @@ -169,13 +169,13 @@ pub mod linux { } impl ThreadContextRecord { - /// Build a record with the given trace id, span id and attributes. The `root_span_id` is a - /// distinguished attribute with special handling for convenience, but it ends up as other - /// attributes in `attrs_data`. + /// Build a record with the given trace id, span id and attributes. The + /// `local_root_span_id` is a distinguished attribute with special handling for + /// convenience, but it ends up as other attributes in `attrs_data`. fn new( trace_id: [u8; 16], span_id: [u8; 8], - root_span_id: &str, + local_root_span_id: [u8; 8], attrs: &[(u8, &str)], ) -> Self { const { assert!(size_of::() == 640) } @@ -185,7 +185,7 @@ pub mod linux { span_id, ..Default::default() }; - record.set_attrs(root_span_id, attrs); + record.set_attrs(local_root_span_id, attrs); record } @@ -210,13 +210,15 @@ pub mod linux { /// recovery would require us to be able to rollback to the previous attributes which would /// hurt the happy path, or leave the record in a inconsistent state. Another possibility /// would be to error out and reset the record in that situation. - fn set_attrs(&mut self, root_span_id: &str, attributes: &[(u8, &str)]) -> bool { - let mut offset = 0; + fn set_attrs(&mut self, local_root_span_id: [u8; 8], attributes: &[(u8, &str)]) -> bool { let mut fully_encoded = true; - let root_span_attr = (ROOT_SPAN_KEY_INDEX, root_span_id); - let full_attrs = std::iter::once(&root_span_attr).chain(attributes); - for &(key_index, val) in full_attrs { + self.attrs_data[0] = ROOT_SPAN_KEY_INDEX; + self.attrs_data[1] = 8; + self.attrs_data[2..10].copy_from_slice(local_root_span_id.as_slice()); + let mut offset = 10; + + for &(key_index, val) in attributes { let val_bytes = val.as_bytes(); let val_len = val_bytes.len(); let val_len = if val_len > 255 { @@ -277,13 +279,13 @@ pub mod linux { pub fn new( trace_id: [u8; 16], span_id: [u8; 8], - root_span_id: &str, + local_root_span_id: [u8; 8], attrs: &[(u8, &str)], ) -> Self { Self::from(ThreadContextRecord::new( trace_id, span_id, - root_span_id, + local_root_span_id, attrs, )) } @@ -361,7 +363,7 @@ pub mod linux { pub fn update( trace_id: [u8; 16], span_id: [u8; 8], - root_span_id: &str, + local_root_span_id: [u8; 8], attrs: &[(u8, &str)], ) { let slot = get_tls_slot(); @@ -372,7 +374,7 @@ pub mod linux { current.trace_id = trace_id; current.span_id = span_id; - current.set_attrs(root_span_id, attrs); + current.set_attrs(local_root_span_id, attrs); compiler_fence(Ordering::SeqCst); current.valid.store(1, Ordering::Relaxed); @@ -382,7 +384,7 @@ pub mod linux { // `ThreadContext::new` already initialises `valid = 1`. let _ = Self::swap( slot, - ThreadContext::new(trace_id, span_id, root_span_id, attrs).into_raw(), + ThreadContext::new(trace_id, span_id, local_root_span_id, attrs).into_raw(), ); } } @@ -422,12 +424,13 @@ pub mod linux { fn tls_lifecycle_basic() { let trace_id = [1u8; 16]; let span_id = [2u8; 8]; + let root_span_id = [3u8; 8]; assert!( read_tls_context_ptr().is_null(), "TLS must be null initially" ); - ThreadContext::new(trace_id, span_id, "", &[]).attach(); + ThreadContext::new(trace_id, span_id, root_span_id, &[]).attach(); assert!( !read_tls_context_ptr().is_null(), "TLS must not be null after attach" @@ -457,8 +460,9 @@ pub mod linux { fn raw_tls_pointer_read() { let trace_id = [1u8; 16]; let span_id = [2u8; 8]; + let root_span_id = [3u8; 8]; - ThreadContext::new(trace_id, span_id, "", &[]).attach(); + ThreadContext::new(trace_id, span_id, root_span_id, &[]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null(), "TLS must be non-null after attach"); @@ -468,7 +472,8 @@ pub mod linux { assert_eq!(record.trace_id, trace_id); assert_eq!(record.span_id, span_id); assert_eq!(record.valid.load(Ordering::Relaxed), 1); - assert_eq!(record.attrs_data_size, 2); + // 1 (key) + 1 (len) + 8 (root_span_id bytes) = 10 + assert_eq!(record.attrs_data_size, 10); let _ = ThreadContext::detach(); } @@ -476,24 +481,25 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn attribute_encoding_basic() { - let root_span_id = "aaaa-bbbb"; + let root_span_id = [0u8; 8]; let attrs: &[(u8, &str)] = &[(1, "GET"), (2, "/api/v1")]; ThreadContext::new([0u8; 16], [0u8; 8], root_span_id, attrs).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - let expected_size: u16 = (2 + 9 + 2 + 3 + 2 + 7) as u16; + // 1+1+8 (root_span_id) + 1+1+3 (GET) + 1+1+7 (/api/v1) + let expected_size: u16 = (2 + 8 + 2 + 3 + 2 + 7) as u16; assert_eq!(record.attrs_data_size, expected_size); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 9); - assert_eq!(&record.attrs_data[2..11], b"aaaa-bbbb"); - assert_eq!(record.attrs_data[11], 1); - assert_eq!(record.attrs_data[12], 3); - assert_eq!(&record.attrs_data[13..16], b"GET"); - assert_eq!(record.attrs_data[16], 2); - assert_eq!(record.attrs_data[17], 7); - assert_eq!(&record.attrs_data[18..25], b"/api/v1"); + assert_eq!(record.attrs_data[1], 8); + assert_eq!(&record.attrs_data[2..10], &root_span_id); + assert_eq!(record.attrs_data[10], 1); + assert_eq!(record.attrs_data[11], 3); + assert_eq!(&record.attrs_data[12..15], b"GET"); + assert_eq!(record.attrs_data[15], 2); + assert_eq!(record.attrs_data[16], 7); + assert_eq!(&record.attrs_data[17..24], b"/api/v1"); let _ = ThreadContext::detach(); } @@ -503,12 +509,13 @@ pub mod linux { fn attribute_truncation_on_overflow() { // Build attributes whose combined encoded size exceeds MAX_ATTRS_DATA_SIZE. // Each max entry: 1 (key) + 1 (len) + 255 (val) = 257 bytes. - // Two such entries: 514 bytes, plus an empty root_span_id: 516. - // A third entry of 100 chars would need 102 bytes, bringing the total to 618 > 612, so + // root_span_id: 1 (key) + 1 (len) + 8 (val) = 10 bytes. + // Two such entries: 514 bytes, plus root_span_id: 524. + // A third entry of 100 chars would need 102 bytes, bringing the total to 626 > 612, so // the third entry must be dropped. let val_a = "a".repeat(255); // 257 bytes encoded let val_b = "b".repeat(255); // 257 bytes encoded → 514 total - let val_c = "c".repeat(100); // 102 bytes encoded → 616 total: must be dropped + let val_c = "c".repeat(100); // 102 bytes encoded → 626 total: must be dropped let attrs: &[(u8, &str)] = &[ (1, val_a.as_str()), @@ -516,17 +523,17 @@ pub mod linux { (3, val_c.as_str()), ]; - ThreadContext::new([0u8; 16], [0u8; 8], "", attrs).attach(); + ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], attrs).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - // Only the first two entries fit (514 bytes + 2 bytes for empty root span id). - assert_eq!(record.attrs_data_size, 516); - assert_eq!(record.attrs_data[2], 1); - assert_eq!(record.attrs_data[3], 255); - assert_eq!(record.attrs_data[259], 2); - assert_eq!(record.attrs_data[260], 255); + // Only the first two entries fit (514 bytes + 10 bytes for root_span_id). + assert_eq!(record.attrs_data_size, 524); + assert_eq!(record.attrs_data[10], 1); + assert_eq!(record.attrs_data[11], 255); + assert_eq!(record.attrs_data[267], 2); + assert_eq!(record.attrs_data[268], 255); let _ = ThreadContext::detach(); } @@ -536,10 +543,10 @@ pub mod linux { fn update_record_in_place() { let trace_id1 = [1u8; 16]; let span_id1 = [1u8; 8]; - let root_span_id1 = "xxxx"; + let root_span_id1 = [0x78u8; 8]; let trace_id2 = [2u8; 16]; let span_id2 = [2u8; 8]; - let root_span_id2 = "yyyy"; + let root_span_id2 = [0x79u8; 8]; // Updating before any context is attached should be equivalent to `attach()` ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, "v1")]); @@ -551,11 +558,11 @@ pub mod linux { assert_eq!(record.span_id, span_id1); assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 4); - assert_eq!(&record.attrs_data[2..6], root_span_id1.as_bytes()); - assert_eq!(record.attrs_data[6], 0); - assert_eq!(record.attrs_data[7], 2); - assert_eq!(&record.attrs_data[8..10], b"v1"); + assert_eq!(record.attrs_data[1], 8); + assert_eq!(&record.attrs_data[2..10], &root_span_id1); + assert_eq!(record.attrs_data[10], 0); + assert_eq!(record.attrs_data[11], 2); + assert_eq!(&record.attrs_data[12..14], b"v1"); ThreadContext::update(trace_id2, span_id2, root_span_id2, &[(0, "v2")]); @@ -570,11 +577,11 @@ pub mod linux { assert_eq!(record.span_id, span_id2); assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 4); - assert_eq!(&record.attrs_data[2..6], root_span_id2.as_bytes()); - assert_eq!(record.attrs_data[6], 0); - assert_eq!(record.attrs_data[7], 2); - assert_eq!(&record.attrs_data[8..10], b"v2"); + assert_eq!(record.attrs_data[1], 8); + assert_eq!(&record.attrs_data[2..10], &root_span_id2); + assert_eq!(record.attrs_data[10], 0); + assert_eq!(record.attrs_data[11], 2); + assert_eq!(&record.attrs_data[12..14], b"v2"); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -583,7 +590,7 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn explicit_detach_nulls_tls() { - ThreadContext::new([3u8; 16], [3u8; 8], "aaaa", &[]).attach(); + ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], &[]).attach(); assert!(!read_tls_context_ptr().is_null()); let _ = ThreadContext::detach(); @@ -598,14 +605,16 @@ pub mod linux { #[cfg_attr(miri, ignore)] fn long_value_capped_at_255_bytes() { let long_val = "a".repeat(300); - ThreadContext::new([0u8; 16], [0u8; 8], "bbbb", &[(0, long_val.as_str())]).attach(); + ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], &[(0, long_val.as_str())]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - let val_len = record.attrs_data[2 + 4 + 1]; + // root_span_id occupies offset 0..10, then the attr entry starts at 10: key at [10], + // len at [11] + let val_len = record.attrs_data[2 + 8 + 1]; assert_eq!(val_len, 255, "value must be capped at 255 bytes"); - assert_eq!(record.attrs_data_size, 2 + 4 + 2 + 255); + assert_eq!(record.attrs_data_size, 2 + 8 + 2 + 255); let _ = ThreadContext::detach(); } @@ -621,10 +630,10 @@ pub mod linux { let spawned_trace_id = [0xABu8; 16]; let spawned_span_id = [0xCDu8; 8]; - let spawned_root_span_id = "xxxx"; + let spawned_root_span_id = [0xEFu8; 8]; let main_trace_id = [0x11u8; 16]; let main_span_id = [0x22u8; 8]; - let main_root_span_id = "yyyy"; + let main_root_span_id = [0x33u8; 8]; let handle = std::thread::spawn(move || { ThreadContext::new(spawned_trace_id, spawned_span_id, spawned_root_span_id, &[]) @@ -641,10 +650,7 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, spawned_trace_id); assert_eq!(record.span_id, spawned_span_id); - assert_eq!( - &record.attrs_data[2..(record.attrs_data_size as usize)], - spawned_root_span_id.as_bytes() - ); + assert_eq!(&record.attrs_data[2..10], &spawned_root_span_id); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -665,10 +671,7 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, main_trace_id); assert_eq!(record.span_id, main_span_id); - assert_eq!( - &record.attrs_data[2..(record.attrs_data_size as usize)], - main_root_span_id.as_bytes() - ); + assert_eq!(&record.attrs_data[2..10], &main_root_span_id); barrier.wait(); From 00848874c155936b2dfcc39b0f54f5ae22e08d2e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 2 Apr 2026 11:49:10 +0200 Subject: [PATCH 4/8] fix: fix wrong local_root_span_id attr name in otel process ctx --- libdd-library-config/src/tracer_metadata.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-library-config/src/tracer_metadata.rs b/libdd-library-config/src/tracer_metadata.rs index 2b488fc076..8035b5a1ef 100644 --- a/libdd-library-config/src/tracer_metadata.rs +++ b/libdd-library-config/src/tracer_metadata.rs @@ -141,7 +141,7 @@ impl TracerMetadata { value: Some(any_value::Value::ArrayValue(ArrayValue { values: std::iter::once(AnyValue { value: Some(any_value::Value::StringValue( - "datadog.root_span_id".to_owned(), + "datadog.local_root_span_id".to_owned(), )), }) .chain(threadlocal_attribute_keys.iter().map(|k| AnyValue { From bfc6c2392e38b74b0906e8f2b2a3def77c0ad66e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 2 Apr 2026 12:27:25 +0200 Subject: [PATCH 5/8] chore: relax str to [u8] for thread ctx attrs --- libdd-profiling/src/otel_thread_ctx.rs | 41 +++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/libdd-profiling/src/otel_thread_ctx.rs b/libdd-profiling/src/otel_thread_ctx.rs index 5613be2a02..461a6a8a6f 100644 --- a/libdd-profiling/src/otel_thread_ctx.rs +++ b/libdd-profiling/src/otel_thread_ctx.rs @@ -40,7 +40,7 @@ //! //! let trace_id = [0u8; 16]; //! let span_id = [1u8; 8]; -//! let attrs: &[(u8, &str)] = &[(0, "GET"), (1, "/api/v1")]; +//! let attrs: &[(u8, &[u8])] = &[(0, "GET"), (1, "/api/v1")]; //! //! // Publish a new context and save the previously attached one (if any). //! let ctx = ThreadContext::new(trace_id, span_id, attrs); @@ -176,7 +176,7 @@ pub mod linux { trace_id: [u8; 16], span_id: [u8; 8], local_root_span_id: [u8; 8], - attrs: &[(u8, &str)], + attrs: &[(u8, &[u8])], ) -> Self { const { assert!(size_of::() == 640) } @@ -210,7 +210,7 @@ pub mod linux { /// recovery would require us to be able to rollback to the previous attributes which would /// hurt the happy path, or leave the record in a inconsistent state. Another possibility /// would be to error out and reset the record in that situation. - fn set_attrs(&mut self, local_root_span_id: [u8; 8], attributes: &[(u8, &str)]) -> bool { + fn set_attrs(&mut self, local_root_span_id: [u8; 8], attributes: &[(u8, &[u8])]) -> bool { let mut fully_encoded = true; self.attrs_data[0] = ROOT_SPAN_KEY_INDEX; @@ -218,8 +218,7 @@ pub mod linux { self.attrs_data[2..10].copy_from_slice(local_root_span_id.as_slice()); let mut offset = 10; - for &(key_index, val) in attributes { - let val_bytes = val.as_bytes(); + for &(key_index, val_bytes) in attributes { let val_len = val_bytes.len(); let val_len = if val_len > 255 { fully_encoded = false; @@ -235,7 +234,7 @@ pub mod linux { } self.attrs_data[offset] = key_index; - // `val_len <= 255` thanks to the `min()` + // `val_len <= 255` from the check above self.attrs_data[offset + 1] = val_len as u8; self.attrs_data[offset + 2..offset + 2 + val_len] .copy_from_slice(&val_bytes[..val_len]); @@ -280,7 +279,7 @@ pub mod linux { trace_id: [u8; 16], span_id: [u8; 8], local_root_span_id: [u8; 8], - attrs: &[(u8, &str)], + attrs: &[(u8, &[u8])], ) -> Self { Self::from(ThreadContextRecord::new( trace_id, @@ -364,7 +363,7 @@ pub mod linux { trace_id: [u8; 16], span_id: [u8; 8], local_root_span_id: [u8; 8], - attrs: &[(u8, &str)], + attrs: &[(u8, &[u8])], ) { let slot = get_tls_slot(); @@ -482,7 +481,7 @@ pub mod linux { #[cfg_attr(miri, ignore)] fn attribute_encoding_basic() { let root_span_id = [0u8; 8]; - let attrs: &[(u8, &str)] = &[(1, "GET"), (2, "/api/v1")]; + let attrs: &[(u8, &[u8])] = &[(1, b"GET"), (2, b"/api/v1")]; ThreadContext::new([0u8; 16], [0u8; 8], root_span_id, attrs).attach(); let ptr = read_tls_context_ptr(); @@ -513,14 +512,14 @@ pub mod linux { // Two such entries: 514 bytes, plus root_span_id: 524. // A third entry of 100 chars would need 102 bytes, bringing the total to 626 > 612, so // the third entry must be dropped. - let val_a = "a".repeat(255); // 257 bytes encoded - let val_b = "b".repeat(255); // 257 bytes encoded → 514 total - let val_c = "c".repeat(100); // 102 bytes encoded → 626 total: must be dropped - - let attrs: &[(u8, &str)] = &[ - (1, val_a.as_str()), - (2, val_b.as_str()), - (3, val_c.as_str()), + let val_a = b"a".repeat(255); // 257 bytes encoded + let val_b = b"b".repeat(255); // 257 bytes encoded → 514 total + let val_c = b"c".repeat(100); // 102 bytes encoded → 626 total: must be dropped + + let attrs: &[(u8, &[u8])] = &[ + (1, val_a.as_slice()), + (2, val_b.as_slice()), + (3, val_c.as_slice()), ]; ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], attrs).attach(); @@ -549,7 +548,7 @@ pub mod linux { let root_span_id2 = [0x79u8; 8]; // Updating before any context is attached should be equivalent to `attach()` - ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, "v1")]); + ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, b"v1")]); let ptr_before = read_tls_context_ptr(); assert!(!ptr_before.is_null()); @@ -564,7 +563,7 @@ pub mod linux { assert_eq!(record.attrs_data[11], 2); assert_eq!(&record.attrs_data[12..14], b"v1"); - ThreadContext::update(trace_id2, span_id2, root_span_id2, &[(0, "v2")]); + ThreadContext::update(trace_id2, span_id2, root_span_id2, &[(0, b"v2")]); let ptr_after = read_tls_context_ptr(); assert_eq!( @@ -604,8 +603,8 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn long_value_capped_at_255_bytes() { - let long_val = "a".repeat(300); - ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], &[(0, long_val.as_str())]).attach(); + let long_val = b"a".repeat(300); + ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], &[(0, long_val.as_slice())]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); From 5807f64c015d883c94025104685bfbd89421cc8a Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 2 Apr 2026 14:25:38 +0200 Subject: [PATCH 6/8] Revert "chore: relax str to [u8] for thread ctx attrs" This reverts commit 7dd922ec0a2214e86e1f26138b7e32ed381581b4. After discussion, the spec actually requires UTF8 bytes, so let's enforce that at the type system level. --- libdd-profiling/src/otel_thread_ctx.rs | 136 ++++++++++++++----------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/libdd-profiling/src/otel_thread_ctx.rs b/libdd-profiling/src/otel_thread_ctx.rs index 461a6a8a6f..53de284791 100644 --- a/libdd-profiling/src/otel_thread_ctx.rs +++ b/libdd-profiling/src/otel_thread_ctx.rs @@ -40,7 +40,7 @@ //! //! let trace_id = [0u8; 16]; //! let span_id = [1u8; 8]; -//! let attrs: &[(u8, &[u8])] = &[(0, "GET"), (1, "/api/v1")]; +//! let attrs: &[(u8, &str)] = &[(0, "GET"), (1, "/api/v1")]; //! //! // Publish a new context and save the previously attached one (if any). //! let ctx = ThreadContext::new(trace_id, span_id, attrs); @@ -176,7 +176,7 @@ pub mod linux { trace_id: [u8; 16], span_id: [u8; 8], local_root_span_id: [u8; 8], - attrs: &[(u8, &[u8])], + attrs: &[(u8, &str)], ) -> Self { const { assert!(size_of::() == 640) } @@ -210,15 +210,28 @@ pub mod linux { /// recovery would require us to be able to rollback to the previous attributes which would /// hurt the happy path, or leave the record in a inconsistent state. Another possibility /// would be to error out and reset the record in that situation. - fn set_attrs(&mut self, local_root_span_id: [u8; 8], attributes: &[(u8, &[u8])]) -> bool { + fn set_attrs(&mut self, local_root_span_id: [u8; 8], attributes: &[(u8, &str)]) -> bool { let mut fully_encoded = true; + const { assert!(MAX_ATTRS_DATA_SIZE >= 18) } + // The local root span id is provided as raw bytes (can be seen as a big-endian u64), + // but readers will expect a string hex representation. We convert it to a fixed + // 16-characters string in the usual lowercase hex format. + // + // There's currently no easy way to use Rust format capabilities to write directly in a + // fixed-size array. Since the conversion is simple, we do it manually. + const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef"; self.attrs_data[0] = ROOT_SPAN_KEY_INDEX; - self.attrs_data[1] = 8; - self.attrs_data[2..10].copy_from_slice(local_root_span_id.as_slice()); - let mut offset = 10; + self.attrs_data[1] = 16; + for (i, &byte) in local_root_span_id.iter().enumerate() { + self.attrs_data[2 + i * 2] = HEX_DIGITS[(byte >> 4) as usize]; + self.attrs_data[2 + i * 2 + 1] = HEX_DIGITS[(byte & 0xF) as usize]; + } - for &(key_index, val_bytes) in attributes { + let mut offset = 18; + + for &(key_index, val) in attributes { + let val_bytes = val.as_bytes(); let val_len = val_bytes.len(); let val_len = if val_len > 255 { fully_encoded = false; @@ -234,7 +247,7 @@ pub mod linux { } self.attrs_data[offset] = key_index; - // `val_len <= 255` from the check above + // `val_len <= 255` thanks to the `min()` self.attrs_data[offset + 1] = val_len as u8; self.attrs_data[offset + 2..offset + 2 + val_len] .copy_from_slice(&val_bytes[..val_len]); @@ -279,7 +292,7 @@ pub mod linux { trace_id: [u8; 16], span_id: [u8; 8], local_root_span_id: [u8; 8], - attrs: &[(u8, &[u8])], + attrs: &[(u8, &str)], ) -> Self { Self::from(ThreadContextRecord::new( trace_id, @@ -363,7 +376,7 @@ pub mod linux { trace_id: [u8; 16], span_id: [u8; 8], local_root_span_id: [u8; 8], - attrs: &[(u8, &[u8])], + attrs: &[(u8, &str)], ) { let slot = get_tls_slot(); @@ -471,8 +484,8 @@ pub mod linux { assert_eq!(record.trace_id, trace_id); assert_eq!(record.span_id, span_id); assert_eq!(record.valid.load(Ordering::Relaxed), 1); - // 1 (key) + 1 (len) + 8 (root_span_id bytes) = 10 - assert_eq!(record.attrs_data_size, 10); + // 1 (key) + 1 (len) + 16 (root_span_id hex chars) = 18 + assert_eq!(record.attrs_data_size, 18); let _ = ThreadContext::detach(); } @@ -480,25 +493,24 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn attribute_encoding_basic() { - let root_span_id = [0u8; 8]; - let attrs: &[(u8, &[u8])] = &[(1, b"GET"), (2, b"/api/v1")]; - ThreadContext::new([0u8; 16], [0u8; 8], root_span_id, attrs).attach(); + let attrs: &[(u8, &str)] = &[(1, "GET"), (2, "/api/v1")]; + ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], attrs).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - // 1+1+8 (root_span_id) + 1+1+3 (GET) + 1+1+7 (/api/v1) - let expected_size: u16 = (2 + 8 + 2 + 3 + 2 + 7) as u16; + // 1+1+16 (root_span_id hex) + 1+1+3 (GET) + 1+1+7 (/api/v1) + let expected_size: u16 = (2 + 16 + 2 + 3 + 2 + 7) as u16; assert_eq!(record.attrs_data_size, expected_size); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 8); - assert_eq!(&record.attrs_data[2..10], &root_span_id); - assert_eq!(record.attrs_data[10], 1); - assert_eq!(record.attrs_data[11], 3); - assert_eq!(&record.attrs_data[12..15], b"GET"); - assert_eq!(record.attrs_data[15], 2); - assert_eq!(record.attrs_data[16], 7); - assert_eq!(&record.attrs_data[17..24], b"/api/v1"); + assert_eq!(record.attrs_data[1], 16); + assert_eq!(&record.attrs_data[2..18], b"0000000000000000"); + assert_eq!(record.attrs_data[18], 1); + assert_eq!(record.attrs_data[19], 3); + assert_eq!(&record.attrs_data[20..23], b"GET"); + assert_eq!(record.attrs_data[23], 2); + assert_eq!(record.attrs_data[24], 7); + assert_eq!(&record.attrs_data[25..32], b"/api/v1"); let _ = ThreadContext::detach(); } @@ -508,18 +520,18 @@ pub mod linux { fn attribute_truncation_on_overflow() { // Build attributes whose combined encoded size exceeds MAX_ATTRS_DATA_SIZE. // Each max entry: 1 (key) + 1 (len) + 255 (val) = 257 bytes. - // root_span_id: 1 (key) + 1 (len) + 8 (val) = 10 bytes. - // Two such entries: 514 bytes, plus root_span_id: 524. - // A third entry of 100 chars would need 102 bytes, bringing the total to 626 > 612, so + // root_span_id: 1 (key) + 1 (len) + 16 (hex val) = 18 bytes. + // Two such entries: 514 bytes, plus root_span_id: 532. + // A third entry of 100 chars would need 102 bytes, bringing the total to 634 > 612, so // the third entry must be dropped. - let val_a = b"a".repeat(255); // 257 bytes encoded - let val_b = b"b".repeat(255); // 257 bytes encoded → 514 total - let val_c = b"c".repeat(100); // 102 bytes encoded → 626 total: must be dropped - - let attrs: &[(u8, &[u8])] = &[ - (1, val_a.as_slice()), - (2, val_b.as_slice()), - (3, val_c.as_slice()), + let val_a = "a".repeat(255); // 257 bytes encoded + let val_b = "b".repeat(255); // 257 bytes encoded → 514 total + let val_c = "c".repeat(100); // 102 bytes encoded → 626 total: must be dropped + + let attrs: &[(u8, &str)] = &[ + (1, val_a.as_str()), + (2, val_b.as_str()), + (3, val_c.as_str()), ]; ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], attrs).attach(); @@ -527,12 +539,12 @@ pub mod linux { let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - // Only the first two entries fit (514 bytes + 10 bytes for root_span_id). - assert_eq!(record.attrs_data_size, 524); - assert_eq!(record.attrs_data[10], 1); - assert_eq!(record.attrs_data[11], 255); - assert_eq!(record.attrs_data[267], 2); - assert_eq!(record.attrs_data[268], 255); + // Only the first two entries fit (514 bytes + 18 bytes for root_span_id). + assert_eq!(record.attrs_data_size, 532); + assert_eq!(record.attrs_data[18], 1); + assert_eq!(record.attrs_data[19], 255); + assert_eq!(record.attrs_data[275], 2); + assert_eq!(record.attrs_data[276], 255); let _ = ThreadContext::detach(); } @@ -548,7 +560,7 @@ pub mod linux { let root_span_id2 = [0x79u8; 8]; // Updating before any context is attached should be equivalent to `attach()` - ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, b"v1")]); + ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, "v1")]); let ptr_before = read_tls_context_ptr(); assert!(!ptr_before.is_null()); @@ -557,13 +569,13 @@ pub mod linux { assert_eq!(record.span_id, span_id1); assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 8); - assert_eq!(&record.attrs_data[2..10], &root_span_id1); - assert_eq!(record.attrs_data[10], 0); - assert_eq!(record.attrs_data[11], 2); - assert_eq!(&record.attrs_data[12..14], b"v1"); + assert_eq!(record.attrs_data[1], 16); + assert_eq!(&record.attrs_data[2..18], b"7878787878787878"); + assert_eq!(record.attrs_data[18], 0); + assert_eq!(record.attrs_data[19], 2); + assert_eq!(&record.attrs_data[20..22], b"v1"); - ThreadContext::update(trace_id2, span_id2, root_span_id2, &[(0, b"v2")]); + ThreadContext::update(trace_id2, span_id2, root_span_id2, &[(0, "v2")]); let ptr_after = read_tls_context_ptr(); assert_eq!( @@ -576,11 +588,11 @@ pub mod linux { assert_eq!(record.span_id, span_id2); assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); - assert_eq!(record.attrs_data[1], 8); - assert_eq!(&record.attrs_data[2..10], &root_span_id2); - assert_eq!(record.attrs_data[10], 0); - assert_eq!(record.attrs_data[11], 2); - assert_eq!(&record.attrs_data[12..14], b"v2"); + assert_eq!(record.attrs_data[1], 16); + assert_eq!(&record.attrs_data[2..18], b"7979797979797979"); + assert_eq!(record.attrs_data[18], 0); + assert_eq!(record.attrs_data[19], 2); + assert_eq!(&record.attrs_data[20..22], b"v2"); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -603,17 +615,17 @@ pub mod linux { #[test] #[cfg_attr(miri, ignore)] fn long_value_capped_at_255_bytes() { - let long_val = b"a".repeat(300); - ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], &[(0, long_val.as_slice())]).attach(); + let long_val = "a".repeat(300); + ThreadContext::new([0u8; 16], [0u8; 8], [0u8; 8], &[(0, long_val.as_str())]).attach(); let ptr = read_tls_context_ptr(); assert!(!ptr.is_null()); let record = unsafe { &*ptr }; - // root_span_id occupies offset 0..10, then the attr entry starts at 10: key at [10], - // len at [11] - let val_len = record.attrs_data[2 + 8 + 1]; + // root_span_id occupies offset 0..18, then the attr entry starts at 18: key at [18], + // len at [19] + let val_len = record.attrs_data[2 + 16 + 1]; assert_eq!(val_len, 255, "value must be capped at 255 bytes"); - assert_eq!(record.attrs_data_size, 2 + 8 + 2 + 255); + assert_eq!(record.attrs_data_size, 2 + 16 + 2 + 255); let _ = ThreadContext::detach(); } @@ -649,7 +661,7 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, spawned_trace_id); assert_eq!(record.span_id, spawned_span_id); - assert_eq!(&record.attrs_data[2..10], &spawned_root_span_id); + assert_eq!(&record.attrs_data[2..18], b"efefefefefefefef"); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -670,7 +682,7 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, main_trace_id); assert_eq!(record.span_id, main_span_id); - assert_eq!(&record.attrs_data[2..10], &main_root_span_id); + assert_eq!(&record.attrs_data[2..18], b"3333333333333333"); barrier.wait(); From a557e43785b1fa6e512e44cf56755cc5a657d013 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 2 Apr 2026 16:58:02 +0200 Subject: [PATCH 7/8] test: update tracer metadata test for local root span id --- libdd-library-config/src/tracer_metadata.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libdd-library-config/src/tracer_metadata.rs b/libdd-library-config/src/tracer_metadata.rs index 8035b5a1ef..321b4a50a9 100644 --- a/libdd-library-config/src/tracer_metadata.rs +++ b/libdd-library-config/src/tracer_metadata.rs @@ -259,11 +259,11 @@ mod tests { #[test] fn threadlocal_attrs_present_with_correct_values() { let ctx = TracerMetadata { - threadlocal_attribute_keys: vec![ + threadlocal_attribute_keys: Some(vec![ "span.id".to_owned(), "trace.id".to_owned(), "custom.key".to_owned(), - ], + ]), ..Default::default() } .to_otel_process_ctx(); @@ -291,6 +291,14 @@ mod tests { other => panic!("expected StringValue, got {:?}", other), }) .collect(); - assert_eq!(keys, ["span.id", "trace.id", "custom.key"]); + assert_eq!( + keys, + [ + "datadog.local_root_span_id", + "span.id", + "trace.id", + "custom.key" + ] + ); } } From a0d136da57604378b8edf741f3bf8165cf9294f9 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Fri, 3 Apr 2026 12:40:55 +0200 Subject: [PATCH 8/8] test(otel-thread-ctx): use more realistic, non-repeating span ids --- libdd-profiling/src/otel_thread_ctx.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/libdd-profiling/src/otel_thread_ctx.rs b/libdd-profiling/src/otel_thread_ctx.rs index 53de284791..16e09478b1 100644 --- a/libdd-profiling/src/otel_thread_ctx.rs +++ b/libdd-profiling/src/otel_thread_ctx.rs @@ -553,11 +553,11 @@ pub mod linux { #[cfg_attr(miri, ignore)] fn update_record_in_place() { let trace_id1 = [1u8; 16]; - let span_id1 = [1u8; 8]; - let root_span_id1 = [0x78u8; 8]; + let span_id1 = [0x01, 0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78]; + let root_span_id1 = [0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F]; let trace_id2 = [2u8; 16]; - let span_id2 = [2u8; 8]; - let root_span_id2 = [0x79u8; 8]; + let span_id2 = [0x0A, 0x1B, 0x2C, 0x3D, 0x4E, 0x5F, 0x6A, 0x7B]; + let root_span_id2 = [0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80]; // Updating before any context is attached should be equivalent to `attach()` ThreadContext::update(trace_id1, span_id1, root_span_id1, &[(0, "v1")]); @@ -570,7 +570,7 @@ pub mod linux { assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); assert_eq!(record.attrs_data[1], 16); - assert_eq!(&record.attrs_data[2..18], b"7878787878787878"); + assert_eq!(&record.attrs_data[2..18], b"78797a7b7c7d7e7f"); assert_eq!(record.attrs_data[18], 0); assert_eq!(record.attrs_data[19], 2); assert_eq!(&record.attrs_data[20..22], b"v1"); @@ -589,7 +589,7 @@ pub mod linux { assert_eq!(record.valid.load(Ordering::Relaxed), 1); assert_eq!(record.attrs_data[0], 0); assert_eq!(record.attrs_data[1], 16); - assert_eq!(&record.attrs_data[2..18], b"7979797979797979"); + assert_eq!(&record.attrs_data[2..18], b"797a7b7c7d7e7f80"); assert_eq!(record.attrs_data[18], 0); assert_eq!(record.attrs_data[19], 2); assert_eq!(&record.attrs_data[20..22], b"v2"); @@ -640,11 +640,11 @@ pub mod linux { let b = barrier.clone(); let spawned_trace_id = [0xABu8; 16]; - let spawned_span_id = [0xCDu8; 8]; - let spawned_root_span_id = [0xEFu8; 8]; + let spawned_span_id = [0xCD, 0xBC, 0xAB, 0x9A, 0x89, 0x78, 0x67, 0x56]; + let spawned_root_span_id = [0xEF, 0xDE, 0xCD, 0xBC, 0xAB, 0x9A, 0x89, 0x78]; let main_trace_id = [0x11u8; 16]; - let main_span_id = [0x22u8; 8]; - let main_root_span_id = [0x33u8; 8]; + let main_span_id = [0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99]; + let main_root_span_id = [0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA]; let handle = std::thread::spawn(move || { ThreadContext::new(spawned_trace_id, spawned_span_id, spawned_root_span_id, &[]) @@ -661,7 +661,7 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, spawned_trace_id); assert_eq!(record.span_id, spawned_span_id); - assert_eq!(&record.attrs_data[2..18], b"efefefefefefefef"); + assert_eq!(&record.attrs_data[2..18], b"efdecdbcab9a8978"); let _ = ThreadContext::detach(); assert!(read_tls_context_ptr().is_null()); @@ -682,7 +682,7 @@ pub mod linux { let record = unsafe { &*ptr }; assert_eq!(record.trace_id, main_trace_id); assert_eq!(record.span_id, main_span_id); - assert_eq!(&record.attrs_data[2..18], b"3333333333333333"); + assert_eq!(&record.attrs_data[2..18], b"33445566778899aa"); barrier.wait();