From 87d411bd4b7b5b08455096d84162747f6d7eb5bd Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 9 Sep 2026 14:42:42 +0530 Subject: [PATCH 01/19] Add the random flag --- opentelemetry/src/trace/span_context.rs | 27 ++++++++- opentelemetry/src/trace_context.rs | 78 ++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/opentelemetry/src/trace/span_context.rs b/opentelemetry/src/trace/span_context.rs index 91c07e2e42..0719429ded 100644 --- a/opentelemetry/src/trace/span_context.rs +++ b/opentelemetry/src/trace/span_context.rs @@ -356,8 +356,9 @@ impl SpanContext { /// Returns details about the trace. /// - /// Unlike `TraceState` values, these are present in all traces. The current - /// version of the specification only supports a single flag [`TraceFlags::SAMPLED`]. + /// Unlike `TraceState` values, these are present in all traces. See + /// [`TraceFlags`] for the flags defined by the + /// [W3C TraceContext specification](https://www.w3.org/TR/trace-context-2/#trace-flags). pub fn trace_flags(&self) -> TraceFlags { self.trace_flags } @@ -380,6 +381,13 @@ impl SpanContext { self.trace_flags.is_sampled() } + /// Returns `true` if the `random-trace-id` trace flag is set. + /// + /// See [`TraceFlags::RANDOM`]. + pub fn is_random(&self) -> bool { + self.trace_flags.is_random() + } + /// A reference to the span context's [`TraceState`]. pub fn trace_state(&self) -> &TraceState { &self.trace_state @@ -583,4 +591,19 @@ mod tests { assert_eq!(updated.get("key20"), Some("updated")); assert_eq!(updated.get("key0"), Some("value0")); } + + #[test] + fn span_context_is_random() { + assert!(!SpanContext::NONE.is_random()); + + let random_only = SpanContext::new( + TraceId::from(1), + SpanId::from(1), + TraceFlags::RANDOM, + false, + TraceState::default(), + ); + assert!(random_only.is_random()); + assert!(!random_only.is_sampled()); + } } diff --git a/opentelemetry/src/trace_context.rs b/opentelemetry/src/trace_context.rs index 78f59a9adc..8e9645dc39 100644 --- a/opentelemetry/src/trace_context.rs +++ b/opentelemetry/src/trace_context.rs @@ -5,13 +5,13 @@ use std::ops::{BitAnd, BitOr, Not}; /// Flags that can be set on a `SpanContext`. /// -/// The current version of the specification only supports a single flag -/// [`TraceFlags::SAMPLED`]. +/// The current version of the specification supports +/// [`TraceFlags::SAMPLED`] and [`TraceFlags::RANDOM`]. /// /// See the W3C TraceContext specification's [trace-flags] section for more /// details. /// -/// [trace-flags]: https://www.w3.org/TR/trace-context/#trace-flags +/// [trace-flags]: https://www.w3.org/TR/trace-context-2/#trace-flags #[derive(Clone, Debug, Default, PartialEq, Eq, Copy, Hash)] pub struct TraceFlags(u8); @@ -32,6 +32,15 @@ impl TraceFlags { /// [W3C TraceContext specification]: https://www.w3.org/TR/trace-context/#sampled-flag pub const SAMPLED: TraceFlags = TraceFlags(0x01); + /// Trace flags with the `random-trace-id` flag set to `1`. + /// + /// Asserts that at least the right-most 7 bytes of the trace ID were generated + /// randomly or pseudo-randomly with uniform distribution. See the + /// `random-trace-id` section of the [W3C TraceContext specification] for details. + /// + /// [W3C TraceContext specification]: https://www.w3.org/TR/trace-context-2/#random-trace-id-flag + pub const RANDOM: TraceFlags = TraceFlags(0x02); + /// Construct new trace flags pub const fn new(flags: u8) -> Self { TraceFlags(flags) @@ -51,6 +60,20 @@ impl TraceFlags { } } + /// Returns `true` if the `random-trace-id` flag is set + pub fn is_random(&self) -> bool { + (*self & TraceFlags::RANDOM) == TraceFlags::RANDOM + } + + /// Returns copy of the current flags with the `random-trace-id` flag set. + pub fn with_random(&self, random: bool) -> Self { + if random { + *self | TraceFlags::RANDOM + } else { + *self & !TraceFlags::RANDOM + } + } + /// Returns the flags as a `u8` pub fn to_u8(self) -> u8 { self.0 @@ -254,4 +277,53 @@ mod tests { assert_eq!(test_case.0, SpanId::from_bytes(test_case.2)); } } + + #[test] + fn trace_flags_constants() { + assert_eq!(TraceFlags::NOT_SAMPLED.to_u8(), 0x00); + assert_eq!(TraceFlags::SAMPLED.to_u8(), 0x01); + assert_eq!(TraceFlags::RANDOM.to_u8(), 0x02); + } + + #[test] + fn trace_flags_is_random() { + for (bits, expected) in [ + (0x00, false), + (0x01, false), + (0x02, true), + (0x03, true), + (0xfd, false), + (0xff, true), + ] { + assert_eq!( + TraceFlags::new(bits).is_random(), + expected, + "flags {bits:#04x}" + ); + } + } + + #[test] + fn trace_flags_random_and_sampled_are_independent() { + let both = TraceFlags::SAMPLED.with_random(true); + assert!(both.is_sampled() && both.is_random()); + assert_eq!(both, TraceFlags::RANDOM.with_sampled(true)); + assert_eq!(both.with_random(false), TraceFlags::SAMPLED); + assert_eq!(both.with_sampled(false), TraceFlags::RANDOM); + } + + #[test] + fn trace_flags_with_random_preserves_unknown_bits() { + assert_eq!(TraceFlags::new(0xf0).with_random(true).to_u8(), 0xf2); + assert_eq!(TraceFlags::new(0xf2).with_random(false).to_u8(), 0xf0); + } + + #[test] + fn trace_flags_hex_formatting() { + assert_eq!(format!("{:02x}", TraceFlags::RANDOM), "02"); + assert_eq!( + format!("{:02x}", TraceFlags::SAMPLED | TraceFlags::RANDOM), + "03" + ); + } } From 92e101480e891e8999a97e913842761f09e463bd Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 9 Sep 2026 14:42:42 +0530 Subject: [PATCH 02/19] Pass the flag along --- .../src/propagation/trace_context.rs | 43 ++++++++++-- opentelemetry-zipkin/src/propagator/mod.rs | 66 ++++++++++++++++++- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/opentelemetry-sdk/src/propagation/trace_context.rs b/opentelemetry-sdk/src/propagation/trace_context.rs index 1c36ced99d..cea2aa888e 100644 --- a/opentelemetry-sdk/src/propagation/trace_context.rs +++ b/opentelemetry-sdk/src/propagation/trace_context.rs @@ -44,6 +44,9 @@ fn trace_context_header_fields() -> &'static [String; 2] { /// /// `tracestate: vendorname1=opaqueValue1,vendorname2=opaqueValue2` /// +/// The `trace-flags` field carries the `sampled` and `random-trace-id` flags; +/// all other bits are zeroed on extract and inject as the specification requires. +/// /// See the [w3c trace-context docs] for more details. /// /// [w3c trace-context docs]: https://w3c.github.io/trace-context/ @@ -99,9 +102,10 @@ impl TraceContextPropagator { let opts = u8::from_str_radix(parts[3], 16).map_err(|_| ())?; // Build trace flags clearing all flags other than the trace-context - // supported sampling bit. Unknown flags are accepted but zeroed out, - // as required by https://www.w3.org/TR/trace-context/#other-flags - let trace_flags = TraceFlags::new(opts) & TraceFlags::SAMPLED; + // defined sampled and random-trace-id bits. Unknown flags are accepted + // but zeroed out, as required by + // https://www.w3.org/TR/trace-context-2/#other-flags + let trace_flags = TraceFlags::new(opts) & (TraceFlags::SAMPLED | TraceFlags::RANDOM); let trace_state = match extractor.get(TRACESTATE_HEADER) { Some(trace_state_str) => { @@ -134,7 +138,7 @@ impl TextMapPropagator for TraceContextPropagator { SUPPORTED_VERSION, span_context.trace_id(), span_context.span_id(), - span_context.trace_flags() & TraceFlags::SAMPLED + span_context.trace_flags() & (TraceFlags::SAMPLED | TraceFlags::RANDOM) ); injector.set(TRACEPARENT_HEADER, header_value); injector.set(TRACESTATE_HEADER, span_context.trace_state().header()); @@ -167,7 +171,10 @@ mod tests { vec![ ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())), ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), - ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::RANDOM, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED | TraceFlags::RANDOM, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-07", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED | TraceFlags::RANDOM, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-fe", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::RANDOM, true, TraceState::from_str("foo=bar").unwrap())), ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), @@ -204,7 +211,11 @@ mod tests { vec![ ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())), - ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::new(0xff), true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::RANDOM, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED | TraceFlags::RANDOM, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::new(0xff), true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::new(0x06), true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::new(0x82), true, TraceState::from_str("foo=bar").unwrap())), ("", "", SpanContext::empty_context()), ] } @@ -297,4 +308,24 @@ mod tests { assert_eq!(Extractor::get(&injector, TRACESTATE_HEADER), Some(state)) } + + #[test] + fn round_trip_random_flag() { + let propagator = TraceContextPropagator::new(); + + for flags in ["00", "01", "02", "03"] { + let header = format!("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-{flags}"); + let mut extractor = HashMap::new(); + extractor.insert(TRACEPARENT_HEADER.to_string(), header.clone()); + + let cx = propagator.extract(&extractor); + + let mut injector = HashMap::new(); + propagator.inject_context(&cx, &mut injector); + assert_eq!( + Extractor::get(&injector, TRACEPARENT_HEADER), + Some(header.as_str()) + ); + } + } } diff --git a/opentelemetry-zipkin/src/propagator/mod.rs b/opentelemetry-zipkin/src/propagator/mod.rs index 8ece2b4d59..6f2d333d6f 100644 --- a/opentelemetry-zipkin/src/propagator/mod.rs +++ b/opentelemetry-zipkin/src/propagator/mod.rs @@ -31,7 +31,7 @@ const B3_SPAN_ID_HEADER: &str = "x-b3-spanid"; const B3_SAMPLED_HEADER: &str = "x-b3-sampled"; const B3_PARENT_SPAN_ID_HEADER: &str = "x-b3-parentspanid"; -const TRACE_FLAG_DEFERRED: TraceFlags = TraceFlags::new(0x02); +const TRACE_FLAG_DEFERRED: TraceFlags = TraceFlags::new(0x80); const TRACE_FLAG_DEBUG: TraceFlags = TraceFlags::new(0x04); static B3_SINGLE_FIELDS: Lazy<[String; 1]> = Lazy::new(|| [B3_SINGLE_HEADER.to_owned()]); @@ -307,6 +307,7 @@ impl TextMapPropagator for Propagator { mod tests { use super::*; use opentelemetry::testing::trace::TestSpan; + use opentelemetry_sdk::propagation::TraceContextPropagator; use std::collections::HashMap; const TRACE_ID_STR: &str = "4bf92f3577b34da6a3ce929d0e0e4736"; @@ -401,6 +402,8 @@ mod tests { ("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-d", SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TRACE_FLAG_DEBUG, true, TraceState::default())), ("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TRACE_FLAG_DEFERRED, true, TraceState::default())), ("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0", SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::default(), true, TraceState::default())), + ("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1", SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::SAMPLED | TraceFlags::RANDOM, true, TraceState::default())), + ("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0", SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::RANDOM, true, TraceState::default())), ("1", SpanContext::new(TraceId::INVALID, SpanId::INVALID, TraceFlags::SAMPLED, true, TraceState::default())), ("0", SpanContext::new(TraceId::INVALID, SpanId::INVALID, TraceFlags::default(), true, TraceState::default())), ] @@ -415,6 +418,8 @@ mod tests { (Some(TRACE_ID_STR), Some(SPAN_ID_STR), None, Some("1"), SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TRACE_FLAG_DEBUG, true, TraceState::default())), (Some(TRACE_ID_STR), Some(SPAN_ID_STR), None, None, SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TRACE_FLAG_DEFERRED, true, TraceState::default())), (Some(TRACE_ID_STR), Some(SPAN_ID_STR), Some("0"), None, SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::default(), true, TraceState::default())), + (Some(TRACE_ID_STR), Some(SPAN_ID_STR), Some("1"), None, SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::SAMPLED | TraceFlags::RANDOM, true, TraceState::default())), + (Some(TRACE_ID_STR), Some(SPAN_ID_STR), Some("0"), None, SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::RANDOM, true, TraceState::default())), (None, None, Some("0"), None, SpanContext::empty_context()), (None, None, Some("1"), None, SpanContext::new(TraceId::INVALID, SpanId::INVALID, TraceFlags::SAMPLED, true, TraceState::default())) ] @@ -680,4 +685,63 @@ mod tests { ] ); } + + #[test] + fn w3c_random_flag_injects_sampled_state_via_b3() { + let mut headers = HashMap::new(); + headers.insert( + "traceparent".to_string(), + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03".to_string(), + ); + let cx = TraceContextPropagator::new().extract(&headers); + + let mut single = HashMap::new(); + Propagator::with_encoding(B3Encoding::SingleHeader).inject_context(&cx, &mut single); + assert_eq!( + single.get(B3_SINGLE_HEADER), + Some(&"4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1".to_owned()) + ); + + let mut multi = HashMap::new(); + Propagator::with_encoding(B3Encoding::MultipleHeader).inject_context(&cx, &mut multi); + assert_eq!(multi.get(B3_SAMPLED_HEADER), Some(&"1".to_owned())); + assert_eq!(multi.get(B3_DEBUG_FLAG_HEADER), None); + } + + #[test] + fn b3_private_flags_stay_off_traceparent() { + let w3c = TraceContextPropagator::new(); + + // Deferred sampling has no sampling state on the wire. + let mut deferred = HashMap::new(); + deferred.insert( + B3_SINGLE_HEADER.to_string(), + "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7".to_string(), + ); + let cx = Propagator::with_encoding(B3Encoding::SingleHeader).extract(&deferred); + assert_eq!(cx.span().span_context().trace_flags(), TRACE_FLAG_DEFERRED); + let mut out = HashMap::new(); + w3c.inject_context(&cx, &mut out); + assert_eq!( + out.get("traceparent"), + Some(&"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00".to_owned()) + ); + + // Debug sampling is indicated by x-b3-flags set to 1. + let mut debug = HashMap::new(); + debug.insert(B3_TRACE_ID_HEADER.to_string(), TRACE_ID_STR.to_string()); + debug.insert(B3_SPAN_ID_HEADER.to_string(), SPAN_ID_STR.to_string()); + debug.insert(B3_DEBUG_FLAG_HEADER.to_string(), "1".to_string()); + let cx = Propagator::with_encoding(B3Encoding::MultipleHeader).extract(&debug); + assert_eq!( + cx.span().span_context().trace_flags(), + TRACE_FLAG_DEBUG | TraceFlags::SAMPLED + ); + let mut out = HashMap::new(); + w3c.inject_context(&cx, &mut out); + assert_eq!( + out.get("traceparent"), + Some(&"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_owned()) + ); + } } From e9cc1dcc3b5e21d6c90abebead271e289dd0715a Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 9 Sep 2026 14:42:42 +0530 Subject: [PATCH 03/19] Wire up random IDs --- opentelemetry-appender-tracing/src/layer.rs | 4 +- .../src/trace/id_generator/mod.rs | 47 +++++ opentelemetry-sdk/src/trace/mod.rs | 15 +- opentelemetry-sdk/src/trace/tracer.rs | 173 +++++++++++++++++- 4 files changed, 229 insertions(+), 10 deletions(-) diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index fb5535473e..fba0ac56c8 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -893,7 +893,7 @@ mod tests { ); assert_eq!( log.record.trace_context().unwrap().trace_flags.unwrap(), - TraceFlags::SAMPLED + TraceFlags::SAMPLED | TraceFlags::RANDOM ); // validate attributes. @@ -1113,7 +1113,7 @@ mod tests { ); assert_eq!( log.record.trace_context().unwrap().trace_flags.unwrap(), - TraceFlags::SAMPLED + TraceFlags::SAMPLED | TraceFlags::RANDOM ); for attribute in log.record.attributes_iter() { diff --git a/opentelemetry-sdk/src/trace/id_generator/mod.rs b/opentelemetry-sdk/src/trace/id_generator/mod.rs index c753de152c..09cefb3234 100644 --- a/opentelemetry-sdk/src/trace/id_generator/mod.rs +++ b/opentelemetry-sdk/src/trace/id_generator/mod.rs @@ -10,6 +10,21 @@ pub trait IdGenerator: Send + Sync + fmt::Debug { /// Generate a new `SpanId` fn new_span_id(&self) -> SpanId; + + /// Returns `true` if every trace ID returned by [`new_trace_id`] has at least its + /// right-most 7 bytes generated randomly or pseudo-randomly with uniform + /// distribution, as required by + /// [W3C Trace Context Level 2](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag). + /// + /// When this returns `true`, the SDK sets [`TraceFlags::RANDOM`] on the span + /// context of every root span. The flag is a positive assertion about the + /// trace ID, so this defaults to `false` and implementations must opt in explicitly. + /// + /// [`new_trace_id`]: IdGenerator::new_trace_id + /// [`TraceFlags::RANDOM`]: opentelemetry::trace::TraceFlags::RANDOM + fn is_random(&self) -> bool { + false + } } /// Default [`IdGenerator`] implementation. @@ -28,6 +43,38 @@ impl IdGenerator for RandomIdGenerator { fn new_span_id(&self) -> SpanId { CURRENT_RNG.with(|rng| SpanId::from(rng.borrow_mut().random::())) } + + fn is_random(&self) -> bool { + true + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + use super::*; + + #[derive(Debug)] + struct CounterIdGenerator; + + impl IdGenerator for CounterIdGenerator { + fn new_trace_id(&self) -> TraceId { + TraceId::from(1) + } + + fn new_span_id(&self) -> SpanId { + SpanId::from(1) + } + } + + #[test] + fn id_generator_is_random_defaults_to_false() { + assert!(!CounterIdGenerator.is_random()); + } + + #[test] + fn random_id_generator_is_random() { + assert!(RandomIdGenerator::default().is_random()); + } } thread_local! { diff --git a/opentelemetry-sdk/src/trace/mod.rs b/opentelemetry-sdk/src/trace/mod.rs index bbbc52a18c..537e84abe6 100644 --- a/opentelemetry-sdk/src/trace/mod.rs +++ b/opentelemetry-sdk/src/trace/mod.rs @@ -289,7 +289,10 @@ mod tests { assert_eq!(span.links.len(), 1); assert_eq!(span.links[0].span_context.trace_id(), TraceId::from(47)); assert_eq!(span.links[0].span_context.span_id(), SpanId::from(11)); - assert_eq!(span.span_context.trace_flags(), TraceFlags::SAMPLED); + assert_eq!( + span.span_context.trace_flags(), + TraceFlags::SAMPLED | TraceFlags::RANDOM + ); assert!(!span.span_context.is_remote()); assert_eq!(span.status, Status::Unset); } @@ -324,7 +327,10 @@ mod tests { assert_eq!(span.attributes.len(), 1); assert_eq!(span.events.len(), 1); assert_eq!(span.events[0].name, "test-event"); - assert_eq!(span.span_context.trace_flags(), TraceFlags::SAMPLED); + assert_eq!( + span.span_context.trace_flags(), + TraceFlags::SAMPLED | TraceFlags::RANDOM + ); assert!(!span.span_context.is_remote()); let status_expected = Status::error("cancelled"); assert_eq!(span.status, status_expected); @@ -361,7 +367,10 @@ mod tests { assert_eq!(span.attributes.len(), 1); assert_eq!(span.events.len(), 1); assert_eq!(span.events[0].name, "test-event"); - assert_eq!(span.span_context.trace_flags(), TraceFlags::SAMPLED); + assert_eq!( + span.span_context.trace_flags(), + TraceFlags::SAMPLED | TraceFlags::RANDOM + ); assert!(!span.span_context.is_remote()); assert_eq!(span.status, Status::Ok); } diff --git a/opentelemetry-sdk/src/trace/tracer.rs b/opentelemetry-sdk/src/trace/tracer.rs index 8f57b40f3a..a00eb46e0c 100644 --- a/opentelemetry-sdk/src/trace/tracer.rs +++ b/opentelemetry-sdk/src/trace/tracer.rs @@ -187,6 +187,7 @@ impl opentelemetry::trace::Tracer for SdkTracer { let config = provider.config(); let span_id = config.id_generator.new_span_id(); let trace_id; + let trace_flags; let mut psc = &SpanContext::empty_context(); let parent_span = if parent_cx.has_active_span() { @@ -195,12 +196,14 @@ impl opentelemetry::trace::Tracer for SdkTracer { None }; - // Build context for sampling decision + // Inherit flags with the trace ID, or derive them from its generator. if let Some(sc) = parent_span.as_ref().map(|parent| parent.span_context()) { trace_id = sc.trace_id(); + trace_flags = sc.trace_flags(); psc = sc; } else { trace_id = config.id_generator.new_trace_id(); + trace_flags = TraceFlags::default().with_random(config.id_generator.is_random()); }; let samplings_result = config.sampler.should_sample( @@ -212,7 +215,6 @@ impl opentelemetry::trace::Tracer for SdkTracer { builder.links.as_deref().unwrap_or(&[]), ); - let trace_flags = psc.trace_flags(); let trace_state = samplings_result.trace_state; let span_limits = config.span_limits; // Build optional inner context, `None` if not recording. @@ -250,8 +252,13 @@ impl opentelemetry::trace::Tracer for SdkTracer { ) } SamplingDecision::Drop => { - let span_context = - SpanContext::new(trace_id, span_id, TraceFlags::default(), false, trace_state); + let span_context = SpanContext::new( + trace_id, + span_id, + trace_flags.with_sampled(false), + false, + trace_state, + ); Span::new(span_context, None, self.clone(), span_limits) } }; @@ -270,16 +277,19 @@ impl opentelemetry::trace::Tracer for SdkTracer { #[cfg(all(test, feature = "testing", feature = "trace"))] mod tests { use crate::{ + propagation::TraceContextPropagator, testing::trace::TestSpan, - trace::{Sampler, SamplingDecision, SamplingResult, ShouldSample}, + trace::{IdGenerator, Sampler, SamplingDecision, SamplingResult, ShouldSample}, }; use opentelemetry::{ + propagation::{Extractor, TextMapPropagator}, trace::{ Link, Span, SpanContext, SpanId, SpanKind, TraceContextExt, TraceFlags, TraceId, TraceState, Tracer, TracerProvider, }, Context, KeyValue, }; + use std::collections::HashMap; #[derive(Clone, Debug)] struct TestSampler {} @@ -617,4 +627,157 @@ mod tests { assert_ne!(child.parent_span_id, active_span_id); assert_ne!(child.span_context.trace_id(), active_trace_id); } + + #[derive(Debug)] + struct TestIdGenerator { + random: bool, + } + + impl IdGenerator for TestIdGenerator { + fn new_trace_id(&self) -> TraceId { + TraceId::from(1) + } + + fn new_span_id(&self) -> SpanId { + SpanId::from(1) + } + + fn is_random(&self) -> bool { + self.random + } + } + + #[derive(Clone, Debug)] + struct RecordOnlySampler; + + impl ShouldSample for RecordOnlySampler { + fn should_sample( + &self, + _parent_context: Option<&Context>, + _trace_id: TraceId, + _name: &str, + _span_kind: &SpanKind, + _attributes: &[KeyValue], + _links: &[Link], + ) -> SamplingResult { + SamplingResult { + decision: SamplingDecision::RecordOnly, + attributes: Vec::new(), + trace_state: TraceState::default(), + } + } + } + + fn remote_parent(flags: TraceFlags) -> Context { + Context::new().with_remote_span_context(SpanContext::new( + TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), + SpanId::from(0x00f0_67aa_0ba9_02b7), + flags, + true, + TraceState::default(), + )) + } + + #[test] + fn root_span_sets_random_flag_with_random_id_generator() { + let tracer_provider = crate::trace::SdkTracerProvider::builder().build(); + let tracer = tracer_provider.tracer("test"); + let span = tracer.start("root"); + let sc = span.span_context(); + assert!(sc.is_random()); + assert!(sc.is_sampled()); + } + + #[test] + fn root_span_uses_id_generator_random_flag() { + for random in [false, true] { + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_id_generator(TestIdGenerator { random }) + .build(); + let tracer = tracer_provider.tracer("test"); + let span = tracer.start("root"); + assert_eq!(span.span_context().is_random(), random); + assert!(span.span_context().is_sampled()); + } + } + + #[test] + fn root_record_only_keeps_random_flag() { + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(RecordOnlySampler) + .build(); + let tracer = tracer_provider.tracer("test"); + let span = tracer.start("root"); + let sc = span.span_context(); + assert!(sc.is_random()); + assert!(!sc.is_sampled()); + assert!(span.is_recording()); + } + + #[test] + fn root_drop_keeps_random_flag() { + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOff) + .build(); + let tracer = tracer_provider.tracer("test"); + let span = tracer.start("root"); + let sc = span.span_context(); + assert!(sc.is_random()); + assert!(!sc.is_sampled()); + assert!(!span.is_recording()); + } + + #[test] + fn child_inherits_random_flag_from_local_parent() { + let tracer_provider = crate::trace::SdkTracerProvider::builder().build(); + let tracer = tracer_provider.tracer("test"); + let root = tracer.start("root"); + let root_trace_id = root.span_context().trace_id(); + let parent_cx = Context::current_with_span(root); + let child = tracer.start_with_context("child", &parent_cx); + let sc = child.span_context(); + assert_eq!(sc.trace_id(), root_trace_id); + assert!(sc.is_random()); + assert!(sc.is_sampled()); + } + + #[test] + fn child_inherits_flags_from_remote_parent() { + for (random, parent_flags) in [ + (true, TraceFlags::SAMPLED), + (false, TraceFlags::SAMPLED | TraceFlags::RANDOM), + ] { + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_id_generator(TestIdGenerator { random }) + .build(); + let tracer = tracer_provider.tracer("test"); + let child = tracer.start_with_context("child", &remote_parent(parent_flags)); + assert_eq!(child.span_context().trace_flags(), parent_flags); + } + } + + #[test] + fn dropped_child_of_remote_parent_preserves_random_flag() { + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOff) + .build(); + let tracer = tracer_provider.tracer("test"); + let child = tracer.start_with_context( + "child", + &remote_parent(TraceFlags::SAMPLED | TraceFlags::RANDOM), + ); + let sc = child.span_context(); + assert!(sc.is_random()); + assert!(!sc.is_sampled()); + assert!(!child.is_recording()); + + let expected = format!("00-{}-{}-02", sc.trace_id(), sc.span_id()); + let mut injector = HashMap::new(); + TraceContextPropagator::new() + .inject_context(&Context::current_with_span(child), &mut injector); + assert_eq!( + Extractor::get(&injector, "traceparent"), + Some(expected.as_str()) + ); + } } From 531790a97282ec3b24587e09b256bb38148c5a79 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 9 Sep 2026 14:42:42 +0530 Subject: [PATCH 04/19] Update API changelog --- opentelemetry/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opentelemetry/CHANGELOG.md b/opentelemetry/CHANGELOG.md index bc53d20b9c..8ecd56f47d 100644 --- a/opentelemetry/CHANGELOG.md +++ b/opentelemetry/CHANGELOG.md @@ -6,6 +6,9 @@ Released 2026-Sep-18 +- Added `TraceFlags::RANDOM`, `TraceFlags::is_random`, `TraceFlags::with_random`, and + `SpanContext::is_random` for the W3C Trace Context Level 2 `random-trace-id` flag + ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). - Fix `TraceState` accepting more than the 32 list-members the W3C trace-context specification allows. `from_str`, `from_key_value` and `insert` now keep at most 32, dropping members from the end of the list as the specification prescribes, so From b613429a5618428c6ce2779f4b7715e1dcd04234 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 9 Sep 2026 14:42:43 +0530 Subject: [PATCH 05/19] Update SDK changelog --- opentelemetry-sdk/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 836d6f680a..c4e78fd1fc 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -6,6 +6,12 @@ Released 2026-Sep-18 +- `IdGenerator` gained a defaulted `is_random()` method (default `false`); + `RandomIdGenerator` returns `true`. Root spans now carry `TraceFlags::RANDOM` when the + configured generator reports random trace IDs, child spans inherit it from their + parent, and spans with a `Drop` sampling decision preserve inherited trace flags other + than `SAMPLED`. `TraceContextPropagator` now propagates the `random-trace-id` flag + ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). - Publicly export the `OTEL_*`/`OTEL_*_DEFAULT` environment variable name and default value constants for `BatchSpanProcessor` (`opentelemetry_sdk::trace`), `BatchLogProcessor` (`opentelemetry_sdk::logs`), and `PeriodicReader` From 578341dc5308194152c69f2ad5d2eb56483e0a71 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 9 Sep 2026 14:42:43 +0530 Subject: [PATCH 06/19] Update Zipkin changelog --- opentelemetry-zipkin/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/opentelemetry-zipkin/CHANGELOG.md b/opentelemetry-zipkin/CHANGELOG.md index e02ec80950..641a73912c 100644 --- a/opentelemetry-zipkin/CHANGELOG.md +++ b/opentelemetry-zipkin/CHANGELOG.md @@ -5,6 +5,10 @@ ## 0.33.0 Released 2026-Sep-18 +- The B3 propagator's internal "deferred" marker no longer overlaps the W3C + `random-trace-id` trace flag; span contexts carrying that flag now inject their + sampling state correctly + ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). ## 0.32.0 From a5d4da46a1ec322f746bdcc7e8e20293cc74cf04 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Fri, 11 Sep 2026 02:39:06 +0530 Subject: [PATCH 07/19] Keep B3 drops explicit --- opentelemetry-sdk/src/trace/tracer.rs | 21 ++++++++++- opentelemetry-zipkin/src/propagator/mod.rs | 41 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/trace/tracer.rs b/opentelemetry-sdk/src/trace/tracer.rs index a00eb46e0c..c6370f7954 100644 --- a/opentelemetry-sdk/src/trace/tracer.rs +++ b/opentelemetry-sdk/src/trace/tracer.rs @@ -252,10 +252,11 @@ impl opentelemetry::trace::Tracer for SdkTracer { ) } SamplingDecision::Drop => { + // Keep RANDOM, but clear private flags after a local drop. let span_context = SpanContext::new( trace_id, span_id, - trace_flags.with_sampled(false), + trace_flags & TraceFlags::RANDOM, false, trace_state, ); @@ -780,4 +781,22 @@ mod tests { Some(expected.as_str()) ); } + + #[test] + fn dropped_child_keeps_only_random_flag() { + // Propagators may stash private bits (e.g. B3's deferred marker) in the + // parent's flags; a local `Drop` decision must not carry them forward. + let private_bit = TraceFlags::new(0x80); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOff) + .build(); + let tracer = tracer_provider.tracer("test"); + + let child = + tracer.start_with_context("child", &remote_parent(TraceFlags::RANDOM | private_bit)); + assert_eq!(child.span_context().trace_flags(), TraceFlags::RANDOM); + + let child = tracer.start_with_context("child", &remote_parent(private_bit)); + assert_eq!(child.span_context().trace_flags(), TraceFlags::default()); + } } diff --git a/opentelemetry-zipkin/src/propagator/mod.rs b/opentelemetry-zipkin/src/propagator/mod.rs index 6f2d333d6f..b9207a1359 100644 --- a/opentelemetry-zipkin/src/propagator/mod.rs +++ b/opentelemetry-zipkin/src/propagator/mod.rs @@ -708,6 +708,47 @@ mod tests { assert_eq!(multi.get(B3_DEBUG_FLAG_HEADER), None); } + #[test] + fn locally_dropped_child_of_deferred_parent_injects_sampled_zero() { + use opentelemetry::trace::{Span, Tracer, TracerProvider}; + use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider}; + + let mut incoming = HashMap::new(); + incoming.insert( + B3_SINGLE_HEADER.to_string(), + "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7".to_string(), + ); + let parent_cx = Propagator::with_encoding(B3Encoding::SingleHeader).extract(&incoming); + assert_eq!( + parent_cx.span().span_context().trace_flags(), + TRACE_FLAG_DEFERRED + ); + + let provider = SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOff) + .build(); + let child = provider + .tracer("test") + .start_with_context("child", &parent_cx); + assert!(!child.is_recording()); + let span_id = child.span_context().span_id().to_string(); + let child_cx = Context::current_with_span(child); + + // The SDK made a local decision, so B3 must send an explicit `0` + // rather than deferring again. + let mut single = HashMap::new(); + Propagator::with_encoding(B3Encoding::SingleHeader).inject_context(&child_cx, &mut single); + assert_eq!( + single.get(B3_SINGLE_HEADER), + Some(&format!("{TRACE_ID_STR}-{span_id}-0")) + ); + + let mut multi = HashMap::new(); + Propagator::with_encoding(B3Encoding::MultipleHeader).inject_context(&child_cx, &mut multi); + assert_eq!(multi.get(B3_SAMPLED_HEADER), Some(&"0".to_owned())); + assert_eq!(multi.get(B3_DEBUG_FLAG_HEADER), None); + } + #[test] fn b3_private_flags_stay_off_traceparent() { let w3c = TraceContextPropagator::new(); From b3a79aecdd6a6588164a1d2f4ad367c421441fd8 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Fri, 11 Sep 2026 02:39:07 +0530 Subject: [PATCH 08/19] Update SDK changelog --- opentelemetry-sdk/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index c4e78fd1fc..0cd65aaefd 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -9,8 +9,8 @@ Released 2026-Sep-18 - `IdGenerator` gained a defaulted `is_random()` method (default `false`); `RandomIdGenerator` returns `true`. Root spans now carry `TraceFlags::RANDOM` when the configured generator reports random trace IDs, child spans inherit it from their - parent, and spans with a `Drop` sampling decision preserve inherited trace flags other - than `SAMPLED`. `TraceContextPropagator` now propagates the `random-trace-id` flag + parent, and spans with a `Drop` sampling decision keep `RANDOM` while clearing all + other trace flags. `TraceContextPropagator` now propagates the `random-trace-id` flag ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). - Publicly export the `OTEL_*`/`OTEL_*_DEFAULT` environment variable name and default value constants for `BatchSpanProcessor` (`opentelemetry_sdk::trace`), From 1e06a00fb4b53bf4be7f42e50978ba842daeef80 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Fri, 11 Sep 2026 03:03:03 +0530 Subject: [PATCH 09/19] Keep B3 bits out of OTLP --- opentelemetry-proto/src/transform/common.rs | 41 ++++++++++ opentelemetry-proto/src/transform/logs.rs | 31 ++++++-- opentelemetry-proto/src/transform/trace.rs | 88 +++++++++++++++------ 3 files changed, 128 insertions(+), 32 deletions(-) diff --git a/opentelemetry-proto/src/transform/common.rs b/opentelemetry-proto/src/transform/common.rs index d9f14e3b84..9d4007a10b 100644 --- a/opentelemetry-proto/src/transform/common.rs +++ b/opentelemetry-proto/src/transform/common.rs @@ -14,6 +14,47 @@ pub(crate) fn to_nanos(time: SystemTime) -> u64 { .as_nanos() as u64 } +/// Keeps W3C trace flags for OTLP, excluding propagator-private bits. +#[cfg(all( + feature = "gen-tonic-messages", + any(feature = "trace", feature = "logs") +))] +pub(crate) fn w3c_trace_flags(flags: opentelemetry::TraceFlags) -> u32 { + use opentelemetry::TraceFlags; + (flags & (TraceFlags::SAMPLED | TraceFlags::RANDOM)).to_u8() as u32 +} + +#[cfg(all( + test, + feature = "gen-tonic-messages", + any(feature = "trace", feature = "logs") +))] +mod w3c_trace_flags_tests { + use super::w3c_trace_flags; + use opentelemetry::TraceFlags; + + #[test] + fn keeps_only_w3c_trace_flags() { + // B3 uses 0x80 for deferred sampling and 0x04 for debug. + for (flags, expected) in [ + (0x00, 0x00), + (0x01, 0x01), + (0x02, 0x02), + (0x03, 0x03), + (0x80, 0x00), + (0x84, 0x00), + (0x83, 0x03), + (0xff, 0x03), + ] { + assert_eq!( + w3c_trace_flags(TraceFlags::new(flags)), + expected, + "flags {flags:#04x}" + ); + } + } +} + #[cfg(feature = "gen-tonic-messages")] pub mod tonic { use crate::proto::tonic::common::v1::{ diff --git a/opentelemetry-proto/src/transform/logs.rs b/opentelemetry-proto/src/transform/logs.rs index 1832eb56b9..99f39d9a75 100644 --- a/opentelemetry-proto/src/transform/logs.rs +++ b/opentelemetry-proto/src/transform/logs.rs @@ -9,7 +9,7 @@ pub mod tonic { logs::v1::{LogRecord, ResourceLogs, ScopeLogs, SeverityNumber}, resource::v1::Resource, }, - transform::common::{to_nanos, tonic::ResourceAttributesWithSchema}, + transform::common::{to_nanos, tonic::ResourceAttributesWithSchema, w3c_trace_flags}, }; use opentelemetry::logs::{AnyValue as LogsAnyValue, Severity}; use opentelemetry_sdk::logs::LogBatch; @@ -112,11 +112,8 @@ pub mod tonic { body: log_record.body().cloned().map(Into::into), dropped_attributes_count: 0, flags: trace_context - .map(|ctx| { - ctx.trace_flags - .map(|flags| flags.to_u8() as u32) - .unwrap_or_default() - }) + .and_then(|ctx| ctx.trace_flags) + .map(w3c_trace_flags) .unwrap_or_default(), span_id: trace_context .map(|ctx| ctx.span_id.to_bytes().to_vec()) @@ -321,6 +318,28 @@ mod tests { assert_eq!(scope_logs_2.log_records.len(), 1); } + #[test] + fn log_record_flags_only_carry_w3c_trace_flags() { + use crate::tonic::logs::v1::LogRecord; + use opentelemetry::trace::{SpanId, TraceFlags, TraceId}; + + // 0x80 is B3's private "deferred" marker; OTLP must only see W3C bits. + for (flags, expected) in [ + (TraceFlags::new(0x80), 0x00), + (TraceFlags::new(0x80) | TraceFlags::RANDOM, 0x02), + (TraceFlags::new(0x80) | TraceFlags::SAMPLED, 0x01), + (TraceFlags::SAMPLED | TraceFlags::RANDOM, 0x03), + ] { + let (mut record, _scope) = create_test_log_data("test-lib", "Log"); + record.set_trace_context(TraceId::from(1), SpanId::from(2), Some(flags)); + + let otlp: LogRecord = (&record).into(); + assert_eq!(otlp.flags, expected, "flags {:#04x}", flags.to_u8()); + assert_eq!(otlp.trace_id, TraceId::from(1).to_bytes().to_vec()); + assert_eq!(otlp.span_id, SpanId::from(2).to_bytes().to_vec()); + } + } + #[test] fn scope_grouping_same_target_preserves_distinct_versions() { let (mut first, _) = create_test_log_data("bridge", "first"); diff --git a/opentelemetry-proto/src/transform/trace.rs b/opentelemetry-proto/src/transform/trace.rs index 2fe69c680a..73364c6c3d 100644 --- a/opentelemetry-proto/src/transform/trace.rs +++ b/opentelemetry-proto/src/transform/trace.rs @@ -18,6 +18,7 @@ pub mod tonic { use crate::transform::common::{ to_nanos, tonic::{Attributes, ResourceAttributesWithSchema}, + w3c_trace_flags, }; use opentelemetry::trace; use opentelemetry::trace::{Link, SpanId, SpanKind}; @@ -56,7 +57,7 @@ pub mod tonic { dropped_attributes_count: link.dropped_attributes_count, flags: super::build_span_flags( link.span_context.is_remote(), - link.span_context.trace_flags().to_u8() as u32, + w3c_trace_flags(link.span_context.trace_flags()), ), } } @@ -77,7 +78,7 @@ pub mod tonic { }, flags: super::build_span_flags( source_span.parent_span_is_remote, - source_span.span_context.trace_flags().to_u8() as u32, + w3c_trace_flags(source_span.span_context.trace_flags()), ), name: source_span.name.into_owned(), kind: span_kind as i32, @@ -139,7 +140,7 @@ pub mod tonic { }, flags: super::build_span_flags( source_span.parent_span_is_remote, - source_span.span_context.trace_flags().to_u8() as u32, + w3c_trace_flags(source_span.span_context.trace_flags()), ), name: source_span.name.into_owned(), kind: span_kind as i32, @@ -254,30 +255,65 @@ mod span_flags_tests { #[test] fn test_span_transformation_with_flags() { - let span_data = SpanData { - span_context: SpanContext::new( - TraceId::from(789), - SpanId::from(101112), - TraceFlags::default(), - false, - TraceState::default(), - ), - parent_span_id: SpanId::from(456), - parent_span_is_remote: false, - span_kind: opentelemetry::trace::SpanKind::Internal, - name: Cow::Borrowed("test_span"), - start_time: std::time::SystemTime::now(), - end_time: std::time::SystemTime::now(), - attributes: vec![], - dropped_attributes_count: 0, - events: opentelemetry_sdk::trace::SpanEvents::default(), - links: opentelemetry_sdk::trace::SpanLinks::default(), - status: opentelemetry::trace::Status::Unset, - instrumentation_scope: InstrumentationScope::builder("test").build(), - }; + use crate::proto::tonic::trace::v1::ResourceSpans; + + for (flags, expected) in [ + (TraceFlags::default(), 0x100), + (TraceFlags::new(0x80) | TraceFlags::RANDOM, 0x102), + ] { + let span_data = SpanData { + span_context: SpanContext::new( + TraceId::from(789), + SpanId::from(101112), + flags, + false, + TraceState::default(), + ), + parent_span_id: SpanId::from(456), + parent_span_is_remote: false, + span_kind: opentelemetry::trace::SpanKind::Internal, + name: Cow::Borrowed("test_span"), + start_time: std::time::SystemTime::now(), + end_time: std::time::SystemTime::now(), + attributes: vec![], + dropped_attributes_count: 0, + events: opentelemetry_sdk::trace::SpanEvents::default(), + links: opentelemetry_sdk::trace::SpanLinks::default(), + status: opentelemetry::trace::Status::Unset, + instrumentation_scope: InstrumentationScope::builder("test").build(), + }; + let otlp_span: Span = span_data.clone().into(); + assert_eq!(otlp_span.flags, expected); - let otlp_span: Span = span_data.into(); - assert_eq!(otlp_span.flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100 + let resource_spans = ResourceSpans::new(span_data, &Default::default()); + assert_eq!(resource_spans.scope_spans[0].spans[0].flags, expected); + } + } + + #[test] + fn test_link_flags_drop_propagator_private_bits() { + use crate::proto::tonic::trace::v1::span; + use opentelemetry::trace::Link; + + for (flags, remote, expected) in [ + (TraceFlags::new(0x80), false, 0x100), + (TraceFlags::new(0x80) | TraceFlags::SAMPLED, true, 0x301), + (TraceFlags::SAMPLED | TraceFlags::RANDOM, false, 0x103), + ] { + let link = Link::new( + SpanContext::new( + TraceId::from(1), + SpanId::from(2), + flags, + remote, + TraceState::default(), + ), + vec![], + 0, + ); + let otlp_link: span::Link = link.into(); + assert_eq!(otlp_link.flags, expected, "flags {:#04x}", flags.to_u8()); + } } #[test] From cae2a10e45ea69f426f1872684cba0939fc6ba4f Mon Sep 17 00:00:00 2001 From: ayush-that Date: Fri, 11 Sep 2026 03:03:03 +0530 Subject: [PATCH 10/19] Update proto changelog --- opentelemetry-proto/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index f265702472..89267b23a7 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -15,6 +15,10 @@ Released 2026-Sep-18 - **Bug fix**: Accept `null` fields in OTLP/JSON `AnyValue` objects as unset. - **Bug fix**: Accept OTLP/JSON partial-success responses when protobuf default fields are omitted. - **Performance**: `group_spans_by_resource_and_scope` no longer clones every `SpanData`. The batch is owned by the function, so spans are now moved into the proto conversion instead of being grouped by reference and cloned. This cuts the transform time of a 512-span batch roughly in half. ([#3652](https://github.com/open-telemetry/opentelemetry-rust/pull/3652)) +- **Bug fix**: OTLP span, link, and log record `flags` now carry only the W3C-defined + trace flags (`sampled`, `random-trace-id`); propagator-private bits such as the B3 + propagator's deferred marker are no longer exported + ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). ## 0.32.0 From 6f5155a0dfc19e27f126d91cfa198322e611cb11 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Fri, 11 Sep 2026 03:11:21 +0530 Subject: [PATCH 11/19] Start fresh for invalid parents --- opentelemetry-sdk/src/trace/tracer.rs | 39 +++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/src/trace/tracer.rs b/opentelemetry-sdk/src/trace/tracer.rs index c6370f7954..8b2a6ccc53 100644 --- a/opentelemetry-sdk/src/trace/tracer.rs +++ b/opentelemetry-sdk/src/trace/tracer.rs @@ -190,11 +190,8 @@ impl opentelemetry::trace::Tracer for SdkTracer { let trace_flags; let mut psc = &SpanContext::empty_context(); - let parent_span = if parent_cx.has_active_span() { - Some(parent_cx.span()) - } else { - None - }; + // Invalid parents must start a new trace. + let parent_span = Some(parent_cx.span()).filter(|span| span.span_context().is_valid()); // Inherit flags with the trace ID, or derive them from its generator. if let Some(sc) = parent_span.as_ref().map(|parent| parent.span_context()) { @@ -782,6 +779,38 @@ mod tests { ); } + #[test] + fn invalid_parent_starts_new_trace_with_generator_random_flag() { + // Neither the invalid trace ID nor the parent's flags should be inherited. + let invalid_parent = Context::current_with_span(TestSpan(SpanContext::new( + TraceId::INVALID, + SpanId::from(7), + TraceFlags::SAMPLED | TraceFlags::new(0x80), + true, + TraceState::default(), + ))); + + for random in [false, true] { + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_id_generator(TestIdGenerator { random }) + .build(); + let tracer = tracer_provider.tracer("test"); + let span = tracer.start_with_context("child", &invalid_parent); + let sc = span.span_context(); + assert!(sc.is_valid()); + assert_eq!(sc.trace_id(), TraceId::from(1)); + assert_eq!( + span.exported_data().unwrap().parent_span_id, + SpanId::INVALID + ); + assert_eq!( + sc.trace_flags(), + TraceFlags::SAMPLED.with_random(random), + "random = {random}" + ); + } + } + #[test] fn dropped_child_keeps_only_random_flag() { // Propagators may stash private bits (e.g. B3's deferred marker) in the From 26d99987825713c2ff2fbae3ede48d58a0596b99 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Fri, 18 Sep 2026 23:28:30 +0530 Subject: [PATCH 12/19] Keep unreleased changelog notes under vNext --- opentelemetry-proto/CHANGELOG.md | 9 +++++---- opentelemetry-sdk/CHANGELOG.md | 9 +++++---- opentelemetry-zipkin/CHANGELOG.md | 7 ++++--- opentelemetry/CHANGELOG.md | 7 ++++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 89267b23a7..b438bcf4b8 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,11 @@ ## vNext +- **Bug fix**: OTLP span, link, and log record `flags` now carry only the W3C-defined + trace flags (`sampled`, `random-trace-id`); propagator-private bits such as the B3 + propagator's deferred marker are no longer exported + ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). + ## 0.33.0 Released 2026-Sep-18 @@ -15,10 +20,6 @@ Released 2026-Sep-18 - **Bug fix**: Accept `null` fields in OTLP/JSON `AnyValue` objects as unset. - **Bug fix**: Accept OTLP/JSON partial-success responses when protobuf default fields are omitted. - **Performance**: `group_spans_by_resource_and_scope` no longer clones every `SpanData`. The batch is owned by the function, so spans are now moved into the proto conversion instead of being grouped by reference and cloned. This cuts the transform time of a 512-span batch roughly in half. ([#3652](https://github.com/open-telemetry/opentelemetry-rust/pull/3652)) -- **Bug fix**: OTLP span, link, and log record `flags` now carry only the W3C-defined - trace flags (`sampled`, `random-trace-id`); propagator-private bits such as the B3 - propagator's deferred marker are no longer exported - ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). ## 0.32.0 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 0cd65aaefd..a109e4d729 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,16 +2,17 @@ ## vNext -## 0.33.0 - -Released 2026-Sep-18 - - `IdGenerator` gained a defaulted `is_random()` method (default `false`); `RandomIdGenerator` returns `true`. Root spans now carry `TraceFlags::RANDOM` when the configured generator reports random trace IDs, child spans inherit it from their parent, and spans with a `Drop` sampling decision keep `RANDOM` while clearing all other trace flags. `TraceContextPropagator` now propagates the `random-trace-id` flag ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). + +## 0.33.0 + +Released 2026-Sep-18 + - Publicly export the `OTEL_*`/`OTEL_*_DEFAULT` environment variable name and default value constants for `BatchSpanProcessor` (`opentelemetry_sdk::trace`), `BatchLogProcessor` (`opentelemetry_sdk::logs`), and `PeriodicReader` diff --git a/opentelemetry-zipkin/CHANGELOG.md b/opentelemetry-zipkin/CHANGELOG.md index 641a73912c..bdf4657cee 100644 --- a/opentelemetry-zipkin/CHANGELOG.md +++ b/opentelemetry-zipkin/CHANGELOG.md @@ -2,14 +2,15 @@ ## vNext -## 0.33.0 - -Released 2026-Sep-18 - The B3 propagator's internal "deferred" marker no longer overlaps the W3C `random-trace-id` trace flag; span contexts carrying that flag now inject their sampling state correctly ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). +## 0.33.0 + +Released 2026-Sep-18 + ## 0.32.0 Released 2026-May-08 diff --git a/opentelemetry/CHANGELOG.md b/opentelemetry/CHANGELOG.md index 8ecd56f47d..e7f8f76e5c 100644 --- a/opentelemetry/CHANGELOG.md +++ b/opentelemetry/CHANGELOG.md @@ -2,13 +2,14 @@ ## vNext +- Added `TraceFlags::RANDOM`, `TraceFlags::is_random`, `TraceFlags::with_random`, and + `SpanContext::is_random` for the W3C Trace Context Level 2 `random-trace-id` flag + ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). + ## 0.33.0 Released 2026-Sep-18 -- Added `TraceFlags::RANDOM`, `TraceFlags::is_random`, `TraceFlags::with_random`, and - `SpanContext::is_random` for the W3C Trace Context Level 2 `random-trace-id` flag - ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). - Fix `TraceState` accepting more than the 32 list-members the W3C trace-context specification allows. `from_str`, `from_key_value` and `insert` now keep at most 32, dropping members from the end of the list as the specification prescribes, so From d8e110e5e23c9dd6cb6ba8fe89529be40cb18059 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Sat, 19 Sep 2026 12:41:06 +0530 Subject: [PATCH 13/19] Retrigger CI From f2d9e96b25e54011c65afe9bd48202f2d161c63e Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 23 Sep 2026 16:47:08 +0530 Subject: [PATCH 14/19] Make generators declare randomness --- .../src/trace/id_generator/mod.rs | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/opentelemetry-sdk/src/trace/id_generator/mod.rs b/opentelemetry-sdk/src/trace/id_generator/mod.rs index 09cefb3234..b324f737fa 100644 --- a/opentelemetry-sdk/src/trace/id_generator/mod.rs +++ b/opentelemetry-sdk/src/trace/id_generator/mod.rs @@ -17,14 +17,12 @@ pub trait IdGenerator: Send + Sync + fmt::Debug { /// [W3C Trace Context Level 2](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag). /// /// When this returns `true`, the SDK sets [`TraceFlags::RANDOM`] on the span - /// context of every root span. The flag is a positive assertion about the - /// trace ID, so this defaults to `false` and implementations must opt in explicitly. + /// context of every root span. Implementations must return `false` if they + /// cannot guarantee this randomness. /// /// [`new_trace_id`]: IdGenerator::new_trace_id /// [`TraceFlags::RANDOM`]: opentelemetry::trace::TraceFlags::RANDOM - fn is_random(&self) -> bool { - false - } + fn is_random(&self) -> bool; } /// Default [`IdGenerator`] implementation. @@ -53,24 +51,6 @@ impl IdGenerator for RandomIdGenerator { mod tests { use super::*; - #[derive(Debug)] - struct CounterIdGenerator; - - impl IdGenerator for CounterIdGenerator { - fn new_trace_id(&self) -> TraceId { - TraceId::from(1) - } - - fn new_span_id(&self) -> SpanId { - SpanId::from(1) - } - } - - #[test] - fn id_generator_is_random_defaults_to_false() { - assert!(!CounterIdGenerator.is_random()); - } - #[test] fn random_id_generator_is_random() { assert!(RandomIdGenerator::default().is_random()); From 754ac6d692ce5aa1794992394d2854388d10526b Mon Sep 17 00:00:00 2001 From: ayush-that Date: Wed, 23 Sep 2026 16:47:08 +0530 Subject: [PATCH 15/19] Update SDK changelog --- opentelemetry-sdk/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index a109e4d729..fecb171f42 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,7 +2,7 @@ ## vNext -- `IdGenerator` gained a defaulted `is_random()` method (default `false`); +- **Breaking:** `IdGenerator` now requires an `is_random()` implementation; `RandomIdGenerator` returns `true`. Root spans now carry `TraceFlags::RANDOM` when the configured generator reports random trace IDs, child spans inherit it from their parent, and spans with a `Drop` sampling decision keep `RANDOM` while clearing all From d8958f4033ee6ad909c601634d5c552859d2a493 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Sat, 26 Sep 2026 11:53:09 +0530 Subject: [PATCH 16/19] Show custom generators the new method --- opentelemetry-sdk/CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 4c90640afe..a62c60a754 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -3,6 +3,17 @@ ## vNext - **Breaking:** `IdGenerator` now requires an `is_random()` implementation; + custom generators need to add this method to their existing implementation: + + ```diff + impl IdGenerator for MyGenerator { + // Existing new_trace_id and new_span_id methods. + + fn is_random(&self) -> bool { false } + } + ``` + + Return `true` only if the right-most 7 bytes of every trace ID are generated + randomly or pseudo-randomly with uniform distribution. `RandomIdGenerator` returns `true`. Root spans now carry `TraceFlags::RANDOM` when the configured generator reports random trace IDs, child spans inherit it from their parent, and spans with a `Drop` sampling decision keep `RANDOM` while clearing all From fa2c521924595e01d59a65232053b0f7d66d720f Mon Sep 17 00:00:00 2001 From: ayush-that Date: Sat, 26 Sep 2026 16:59:24 +0530 Subject: [PATCH 17/19] Clear private flags after sampling --- opentelemetry-propagator-b3/src/lib.rs | 50 ++++++++++++++------------ opentelemetry-sdk/src/trace/tracer.rs | 32 ++++++++++++++++- opentelemetry/src/trace_context.rs | 24 +++++++++++++ 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/opentelemetry-propagator-b3/src/lib.rs b/opentelemetry-propagator-b3/src/lib.rs index e51404f403..79118d7e0d 100644 --- a/opentelemetry-propagator-b3/src/lib.rs +++ b/opentelemetry-propagator-b3/src/lib.rs @@ -713,7 +713,7 @@ mod tests { } #[test] - fn locally_dropped_child_of_deferred_parent_injects_sampled_zero() { + fn local_sampling_decision_clears_b3_deferred_marker() { use opentelemetry::trace::{Span, Tracer, TracerProvider}; use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider}; @@ -728,29 +728,33 @@ mod tests { TRACE_FLAG_DEFERRED ); - let provider = SdkTracerProvider::builder() - .with_sampler(Sampler::AlwaysOff) - .build(); - let child = provider - .tracer("test") - .start_with_context("child", &parent_cx); - assert!(!child.is_recording()); - let span_id = child.span_context().span_id().to_string(); - let child_cx = Context::current_with_span(child); - - // The SDK made a local decision, so B3 must send an explicit `0` - // rather than deferring again. - let mut single = HashMap::new(); - Propagator::with_encoding(B3Encoding::SingleHeader).inject_context(&child_cx, &mut single); - assert_eq!( - single.get(B3_SINGLE_HEADER), - Some(&format!("{TRACE_ID_STR}-{span_id}-0")) - ); + for (sampler, sampled) in [(Sampler::AlwaysOff, false), (Sampler::AlwaysOn, true)] { + let provider = SdkTracerProvider::builder().with_sampler(sampler).build(); + let child = provider + .tracer("test") + .start_with_context("child", &parent_cx); + assert_eq!(child.is_recording(), sampled); + let span_id = child.span_context().span_id().to_string(); + let child_cx = Context::current_with_span(child); + let sampled_flag = if sampled { "1" } else { "0" }; + + let mut single = HashMap::new(); + Propagator::with_encoding(B3Encoding::SingleHeader) + .inject_context(&child_cx, &mut single); + assert_eq!( + single.get(B3_SINGLE_HEADER), + Some(&format!("{TRACE_ID_STR}-{span_id}-{sampled_flag}")) + ); - let mut multi = HashMap::new(); - Propagator::with_encoding(B3Encoding::MultipleHeader).inject_context(&child_cx, &mut multi); - assert_eq!(multi.get(B3_SAMPLED_HEADER), Some(&"0".to_owned())); - assert_eq!(multi.get(B3_DEBUG_FLAG_HEADER), None); + let mut multi = HashMap::new(); + Propagator::with_encoding(B3Encoding::MultipleHeader) + .inject_context(&child_cx, &mut multi); + assert_eq!( + multi.get(B3_SAMPLED_HEADER).map(String::as_str), + Some(sampled_flag) + ); + assert_eq!(multi.get(B3_DEBUG_FLAG_HEADER), None); + } } #[test] diff --git a/opentelemetry-sdk/src/trace/tracer.rs b/opentelemetry-sdk/src/trace/tracer.rs index 8b2a6ccc53..07f3e1e11b 100644 --- a/opentelemetry-sdk/src/trace/tracer.rs +++ b/opentelemetry-sdk/src/trace/tracer.rs @@ -214,6 +214,7 @@ impl opentelemetry::trace::Tracer for SdkTracer { let trace_state = samplings_result.trace_state; let span_limits = config.span_limits; + let trace_flags = trace_flags.sanitized(); // Build optional inner context, `None` if not recording. let mut span = match samplings_result.decision { SamplingDecision::RecordAndSample => { @@ -249,7 +250,6 @@ impl opentelemetry::trace::Tracer for SdkTracer { ) } SamplingDecision::Drop => { - // Keep RANDOM, but clear private flags after a local drop. let span_context = SpanContext::new( trace_id, span_id, @@ -828,4 +828,34 @@ mod tests { let child = tracer.start_with_context("child", &remote_parent(private_bit)); assert_eq!(child.span_context().trace_flags(), TraceFlags::default()); } + + #[test] + fn recording_children_clear_private_parent_flags() { + for random in [false, true] { + let parent = remote_parent(TraceFlags::new(0xff).with_random(random)); + let sampled_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .build(); + let sampled = sampled_provider + .tracer("test") + .start_with_context("child", &parent); + assert!(sampled.is_recording()); + assert_eq!( + sampled.span_context().trace_flags(), + TraceFlags::SAMPLED.with_random(random) + ); + + let recording_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(RecordOnlySampler) + .build(); + let recording = recording_provider + .tracer("test") + .start_with_context("child", &parent); + assert!(recording.is_recording()); + assert_eq!( + recording.span_context().trace_flags(), + TraceFlags::NOT_SAMPLED.with_random(random) + ); + } + } } diff --git a/opentelemetry/src/trace_context.rs b/opentelemetry/src/trace_context.rs index 8e9645dc39..ec3dcbe280 100644 --- a/opentelemetry/src/trace_context.rs +++ b/opentelemetry/src/trace_context.rs @@ -41,6 +41,8 @@ impl TraceFlags { /// [W3C TraceContext specification]: https://www.w3.org/TR/trace-context-2/#random-trace-id-flag pub const RANDOM: TraceFlags = TraceFlags(0x02); + const VALID_FLAGS: Self = Self(Self::SAMPLED.0 | Self::RANDOM.0); + /// Construct new trace flags pub const fn new(flags: u8) -> Self { TraceFlags(flags) @@ -74,6 +76,12 @@ impl TraceFlags { } } + /// Returns a copy with only the recognized [`Self::SAMPLED`] and + /// [`Self::RANDOM`] flags preserved. + pub fn sanitized(self) -> Self { + self & Self::VALID_FLAGS + } + /// Returns the flags as a `u8` pub fn to_u8(self) -> u8 { self.0 @@ -326,4 +334,20 @@ mod tests { "03" ); } + + #[test] + fn trace_flags_sanitized_preserves_only_recognized_flags() { + for (bits, expected) in [ + (0x00, TraceFlags::NOT_SAMPLED), + (0x01, TraceFlags::SAMPLED), + (0x02, TraceFlags::RANDOM), + (0x03, TraceFlags::SAMPLED | TraceFlags::RANDOM), + (0xfc, TraceFlags::NOT_SAMPLED), + (0xfd, TraceFlags::SAMPLED), + (0xfe, TraceFlags::RANDOM), + (0xff, TraceFlags::SAMPLED | TraceFlags::RANDOM), + ] { + assert_eq!(TraceFlags::new(bits).sanitized(), expected); + } + } } From e53bae10801f361a608f37d9aefae5e5a91dca59 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Sat, 26 Sep 2026 16:59:29 +0530 Subject: [PATCH 18/19] Note the sampling flag cleanup --- opentelemetry-sdk/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index a62c60a754..879278bcaa 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -16,8 +16,8 @@ randomly or pseudo-randomly with uniform distribution. `RandomIdGenerator` returns `true`. Root spans now carry `TraceFlags::RANDOM` when the configured generator reports random trace IDs, child spans inherit it from their - parent, and spans with a `Drop` sampling decision keep `RANDOM` while clearing all - other trace flags. `TraceContextPropagator` now propagates the `random-trace-id` flag + parent, and local sampling decisions clear private propagator flags while retaining + `RANDOM`. `TraceContextPropagator` now propagates the `random-trace-id` flag ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). - Fixed `Histogram`, `Sum`, `LastValue`, and `PrecomputedSum` aggregators (and their bound-instrument handles) silently accepting NaN/Infinity From 6c8f1cb4f0deea4d9157d724aee86b122319f7e2 Mon Sep 17 00:00:00 2001 From: ayush-that Date: Sat, 26 Sep 2026 16:59:33 +0530 Subject: [PATCH 19/19] Note the trace flag helper --- opentelemetry/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/opentelemetry/CHANGELOG.md b/opentelemetry/CHANGELOG.md index 8e379efb6b..4893779015 100644 --- a/opentelemetry/CHANGELOG.md +++ b/opentelemetry/CHANGELOG.md @@ -5,6 +5,7 @@ - Added `TraceFlags::RANDOM`, `TraceFlags::is_random`, `TraceFlags::with_random`, and `SpanContext::is_random` for the W3C Trace Context Level 2 `random-trace-id` flag ([#3270](https://github.com/open-telemetry/opentelemetry-rust/issues/3270)). +- Added `TraceFlags::sanitized()` to retain only recognized trace flags. ## 0.33.0