diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index a8a2a64a01..b997fce6a2 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -897,7 +897,7 @@ mod tests { ); assert_eq!( log.record.trace_context().unwrap().trace_flags.unwrap(), - TraceFlags::SAMPLED + TraceFlags::SAMPLED | TraceFlags::RANDOM ); // validate attributes. @@ -1125,7 +1125,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-propagator-b3/CHANGELOG.md b/opentelemetry-propagator-b3/CHANGELOG.md index f80a7a17dd..72176cbd4c 100644 --- a/opentelemetry-propagator-b3/CHANGELOG.md +++ b/opentelemetry-propagator-b3/CHANGELOG.md @@ -2,6 +2,10 @@ ## vNext +- 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.1 Released 2026-Oct-08 diff --git a/opentelemetry-propagator-b3/Cargo.toml b/opentelemetry-propagator-b3/Cargo.toml index c1795e6f28..327cae3e9a 100644 --- a/opentelemetry-propagator-b3/Cargo.toml +++ b/opentelemetry-propagator-b3/Cargo.toml @@ -21,6 +21,7 @@ opentelemetry = { workspace = true, features = ["trace"] } [dev-dependencies] opentelemetry = { workspace = true, features = ["testing"] } +opentelemetry_sdk = { workspace = true, features = ["trace"] } [lints] workspace = true diff --git a/opentelemetry-propagator-b3/src/lib.rs b/opentelemetry-propagator-b3/src/lib.rs index 2ae6a9cdf9..79118d7e0d 100644 --- a/opentelemetry-propagator-b3/src/lib.rs +++ b/opentelemetry-propagator-b3/src/lib.rs @@ -36,7 +36,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()]); @@ -312,6 +312,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"; @@ -403,9 +404,11 @@ mod tests { fn single_header_inject_data() -> Vec<(&'static str, SpanContext)> { vec![ ("4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1", SpanContext::new(TraceId::from(TRACE_ID_HEX), SpanId::from(SPAN_ID_HEX), TraceFlags::SAMPLED, 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-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-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())), ] @@ -417,9 +420,11 @@ mod tests { // TraceId, SpanId, isSampled, isDebug vec![ (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, 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), 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("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())) ] @@ -685,4 +690,107 @@ 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 local_sampling_decision_clears_b3_deferred_marker() { + 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 + ); + + 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).map(String::as_str), + Some(sampled_flag) + ); + 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()) + ); + } } diff --git a/opentelemetry-proto/CHANGELOG.md b/opentelemetry-proto/CHANGELOG.md index 6cde74ab79..fc96b6abfa 100644 --- a/opentelemetry-proto/CHANGELOG.md +++ b/opentelemetry-proto/CHANGELOG.md @@ -2,6 +2,10 @@ ## 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.1 Released 2026-Oct-08 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 6dc417116a..588cc88fb3 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] diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index e180e0adb1..6f014d1e26 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,23 @@ ## 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 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 `BatchSpanProcessor` and `BatchLogProcessor` shutdown reporting success when exporting the final queued telemetry failed. ([#3793](https://github.com/open-telemetry/opentelemetry-rust/issues/3793)) diff --git a/opentelemetry-sdk/src/propagation/trace_context.rs b/opentelemetry-sdk/src/propagation/trace_context.rs index c67eac5219..8ca123a755 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); // Ensure trace id and span id are valid before looking at tracestate, which // must not be parsed when traceparent is invalid. @@ -138,7 +142,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); @@ -176,7 +180,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())), @@ -213,7 +220,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()), ] } @@ -428,4 +439,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-sdk/src/trace/id_generator/mod.rs b/opentelemetry-sdk/src/trace/id_generator/mod.rs index c753de152c..b324f737fa 100644 --- a/opentelemetry-sdk/src/trace/id_generator/mod.rs +++ b/opentelemetry-sdk/src/trace/id_generator/mod.rs @@ -10,6 +10,19 @@ 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. 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; } /// Default [`IdGenerator`] implementation. @@ -28,6 +41,20 @@ 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::*; + + #[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 bca9675002..8c34efb49e 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 0fae22f437..12e02f48cc 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(); // Inheriting its all-zero trace id would produce a span with an invalid trace id @@ -194,12 +195,14 @@ impl opentelemetry::trace::Tracer for SdkTracer { let parent_span = Some(parent_cx.span()) .filter(|span| span.span_context().trace_id() != TraceId::INVALID); - // 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( @@ -211,9 +214,9 @@ 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; + let trace_flags = trace_flags.sanitized(); // Build optional inner context, `None` if not recording. let mut span = match samplings_result.decision { SamplingDecision::RecordAndSample => { @@ -249,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 & TraceFlags::RANDOM, + false, + trace_state, + ); Span::new(span_context, None, self.clone(), span_limits) } }; @@ -269,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,6 +628,239 @@ mod tests { 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()) + ); + } + + #[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 + // 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()); + } + + #[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) + ); + } + } + #[test] fn parent_with_invalid_span_id_but_valid_trace_id_inherits_trace_id() { use crate::trace::{InMemorySpanExporter, SimpleSpanProcessor}; diff --git a/opentelemetry/CHANGELOG.md b/opentelemetry/CHANGELOG.md index b60508a0f2..8e69667c81 100644 --- a/opentelemetry/CHANGELOG.md +++ b/opentelemetry/CHANGELOG.md @@ -2,9 +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)). +- Added `TraceFlags::sanitized()` to retain only recognized trace flags. - Fix panics when the context stack is full and a rejected value's destructor or a tracing subscriber calls `Context::current()`. ([#3753](https://github.com/open-telemetry/opentelemetry-rust/issues/3753)) + ## 0.33.1 Released 2026-Oct-08 diff --git a/opentelemetry/src/trace/span_context.rs b/opentelemetry/src/trace/span_context.rs index 71a479b07d..93d5cc3d31 100644 --- a/opentelemetry/src/trace/span_context.rs +++ b/opentelemetry/src/trace/span_context.rs @@ -64,8 +64,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 } @@ -88,6 +89,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 @@ -121,4 +129,19 @@ mod tests { }" ); } + + #[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 38da52ac4c..2decffe6a2 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,17 @@ 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); + + const VALID_FLAGS: Self = Self(Self::SAMPLED.0 | Self::RANDOM.0); + /// Construct new trace flags pub const fn new(flags: u8) -> Self { TraceFlags(flags) @@ -51,6 +62,26 @@ 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 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 @@ -260,4 +291,69 @@ 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" + ); + } + + #[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); + } + } }