From b3575174151129deedebec23c86100d24bd07e26 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 11 Dec 2025 23:51:41 -0500 Subject: [PATCH 1/4] feat: add support for 'random-trace-id' flags in W3C traceparent header trace flags --- CHANGELOG.md | 2 + .../src/opentelemetry/trace/span.py | 14 ++++-- .../src/opentelemetry/sdk/trace/__init__.py | 6 +++ .../opentelemetry/sdk/trace/id_generator.py | 23 +++++++++- opentelemetry-sdk/tests/test_configurator.py | 3 ++ opentelemetry-sdk/tests/trace/test_trace.py | 44 +++++++++---------- 6 files changed, 65 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6003d1a7947..78d68cf9d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4958](https://github.com/open-telemetry/opentelemetry-python/pull/4958)) - `opentelemetry-sdk`: fix type annotations on `MetricReader` and related types ([#4938](https://github.com/open-telemetry/opentelemetry-python/pull/4938/)) +- `opentelemetry-api`, `opentelemetry-sdk`: add support for 'random-trace-id' flags in W3C traceparent header trace flags. Implementations of `IdGenerator` that do randomly generate the 56 least significant bits, should also implement a `is_trace_id_random` methods that returns `True`. + ([#4854](https://github.com/open-telemetry/opentelemetry-python/pull/4854)) ## Version 1.40.0/0.61b0 (2026-03-04) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index b0cda475e2f..6d4fb726b1b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -197,17 +197,21 @@ def __exit__( class TraceFlags(int): """A bitmask that represents options specific to the trace. - The only supported option is the "sampled" flag (``0x01``). If set, this - flag indicates that the trace may have been sampled upstream. + Supported flags: + - "sampled" (``0x01``): Indicates the trace may have been sampled upstream. + - "random-trace-id" (``0x02``): Indicates the trace ID was generated + randomly, with at least the 7 rightmost bytes (56 bits) selected + with uniform distribution. See the `W3C Trace Context - Traceparent`_ spec for details. .. _W3C Trace Context - Traceparent: - https://www.w3.org/TR/trace-context/#trace-flags + https://www.w3.org/TR/trace-context-2/#trace-flags """ DEFAULT = 0x00 SAMPLED = 0x01 + RANDOM_TRACE_ID = 0x02 @classmethod def get_default(cls) -> "TraceFlags": @@ -217,6 +221,10 @@ def get_default(cls) -> "TraceFlags": def sampled(self) -> bool: return bool(self & TraceFlags.SAMPLED) + @property + def random_trace_id(self) -> bool: + return bool(self & TraceFlags.RANDOM_TRACE_ID) + DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 37e4db77ec8..c1cad794691 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1219,6 +1219,12 @@ def start_span( # pylint: disable=too-many-locals if sampling_result.decision.is_sampled() else trace_api.TraceFlags(trace_api.TraceFlags.DEFAULT) ) + + if self.id_generator.is_trace_id_random(): + trace_flags = trace_api.TraceFlags( + trace_flags | trace_api.TraceFlags.RANDOM_TRACE_ID + ) + span_context = trace_api.SpanContext( trace_id, self.id_generator.generate_span_id(), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py index cd1f89bcde2..cd1144b2556 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py @@ -31,16 +31,34 @@ def generate_span_id(self) -> int: def generate_trace_id(self) -> int: """Get a new trace ID. - Implementations should at least make the 64 least significant bits + Implementations should at least make the 56 least significant bits uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on this randomness to make sampling decisions. + If the implementation does randomly generate the 56 least significant bits, + it should also implement `is_trace_id_random` to return True. + See `the specification on TraceIdRatioBased `_. Returns: A 128-bit int for use as a trace ID """ + @abc.abstractmethod + def is_trace_id_random(self) -> bool: + """Indicates whether generated trace IDs are random. + + When True, the `trace-id` field will have the `random-trace-id` flag set + in the W3C traceparent header. Per the W3C Trace Context specification, + this indicates that at least the 7 rightmost bytes (56 bits) of the + trace ID were generated randomly with uniform distribution. + + See `the W3C Trace Context specification `_. + + Returns: + True if this generator produces random IDs, False otherwise. + """ + class RandomIdGenerator(IdGenerator): """The default ID generator for TracerProvider which randomly generates all @@ -58,3 +76,6 @@ def generate_trace_id(self) -> int: while trace_id == trace.INVALID_TRACE_ID: trace_id = random.getrandbits(128) return trace_id + + def is_trace_id_random(self) -> bool: + return True diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 333494df746..a32b7f80cb8 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -326,6 +326,9 @@ def generate_span_id(self): def generate_trace_id(self): pass + def is_trace_id_random(self): + return False + class TestTraceInit(TestCase): def setUp(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e9a59c6cde9..08505301a42 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -238,14 +238,7 @@ def test_default_sampler(self): child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace.Span) self.assertTrue(root_span.context.trace_flags.sampled) - self.assertEqual( - root_span.get_span_context().trace_flags, - trace_api.TraceFlags.SAMPLED, - ) - self.assertEqual( - child_span.get_span_context().trace_flags, - trace_api.TraceFlags.SAMPLED, - ) + self.assertTrue(root_span.get_span_context().trace_flags.sampled) def test_default_sampler_type(self): tracer_provider = trace.TracerProvider() @@ -263,14 +256,8 @@ def test_sampler_no_sampling(self, _get_from_env_or_default): self.assertIsInstance(root_span, trace_api.NonRecordingSpan) child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace_api.NonRecordingSpan) - self.assertEqual( - root_span.get_span_context().trace_flags, - trace_api.TraceFlags.DEFAULT, - ) - self.assertEqual( - child_span.get_span_context().trace_flags, - trace_api.TraceFlags.DEFAULT, - ) + self.assertFalse(root_span.get_span_context().trace_flags.sampled) + self.assertFalse(child_span.get_span_context().trace_flags.sampled) self.assertFalse(_get_from_env_or_default.called) @mock.patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "always_off"}) @@ -490,9 +477,8 @@ def test_start_span_explicit(self): other_parent.get_span_context().trace_state, child_context.trace_state, ) - self.assertEqual( - other_parent.get_span_context().trace_flags, - child_context.trace_flags, + self.assertTrue( + other_parent.get_span_context().trace_flags.sampled ) # Verify start_span() did not set the current span. @@ -853,10 +839,7 @@ def test_sampling_attributes(self): self.assertEqual(len(root.attributes), 2) self.assertEqual(root.attributes["sampler-attr"], "sample-val") self.assertEqual(root.attributes["attr-in-both"], "decision-attr") - self.assertEqual( - root.get_span_context().trace_flags, - trace_api.TraceFlags.SAMPLED, - ) + self.assertTrue(root.get_span_context().trace_flags.sampled) def test_events(self): self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN) @@ -2092,6 +2075,9 @@ def test_constant_default(self): def test_constant_sampled(self): self.assertEqual(trace_api.TraceFlags.SAMPLED, 1) + def test_constant_random_trace_id(self): + self.assertEqual(trace_api.TraceFlags.RANDOM_TRACE_ID, 2) + def test_get_default(self): self.assertEqual( trace_api.TraceFlags.get_default(), trace_api.TraceFlags.DEFAULT @@ -2103,6 +2089,14 @@ def test_sampled_true(self): def test_sampled_false(self): self.assertFalse(trace_api.TraceFlags(0xF0).sampled) + def test_random_trace_id_true(self): + self.assertTrue(trace_api.TraceFlags(0xF2).random_trace_id) + self.assertTrue(trace_api.TraceFlags(0xF3).random_trace_id) + + def test_random_trace_id_false(self): + self.assertFalse(trace_api.TraceFlags(0xF0).random_trace_id) + self.assertFalse(trace_api.TraceFlags(0xF1).random_trace_id) + def test_constant_default_trace_options(self): self.assertEqual( trace_api.DEFAULT_TRACE_OPTIONS, trace_api.TraceFlags.DEFAULT @@ -2349,3 +2343,7 @@ def test_generate_trace_id_avoids_invalid(self, mock_getrandbits): self.assertNotEqual(trace_id, trace_api.INVALID_TRACE_ID) mock_getrandbits.assert_any_call(128) self.assertEqual(mock_getrandbits.call_count, 2) + + def test_is_trace_id_random_returns_true(self): + generator = RandomIdGenerator() + self.assertTrue(generator.is_trace_id_random()) From a848d73b90724f38f4ca9a08dc3fe3566fbe8411 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 12 Dec 2025 11:54:27 -0500 Subject: [PATCH 2/4] update IdGenerator to provide default implementation for 'is_trace_id_random' for backwards compatability --- opentelemetry-api/src/opentelemetry/trace/span.py | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 6d4fb726b1b..7ccc38d6205 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -200,8 +200,8 @@ class TraceFlags(int): Supported flags: - "sampled" (``0x01``): Indicates the trace may have been sampled upstream. - "random-trace-id" (``0x02``): Indicates the trace ID was generated - randomly, with at least the 7 rightmost bytes (56 bits) selected - with uniform distribution. + randomly, with at least the 7 rightmost bytes (56 bits) selected with + uniform distribution. See the `W3C Trace Context - Traceparent`_ spec for details. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py index cd1144b2556..d451e111816 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py @@ -44,7 +44,7 @@ def generate_trace_id(self) -> int: A 128-bit int for use as a trace ID """ - @abc.abstractmethod + # pylint: disable=no-self-use def is_trace_id_random(self) -> bool: """Indicates whether generated trace IDs are random. @@ -58,6 +58,8 @@ def is_trace_id_random(self) -> bool: Returns: True if this generator produces random IDs, False otherwise. """ + # By default, return False for backwards compatibility. + return False class RandomIdGenerator(IdGenerator): From 51801c369340ea23649b7794269384276485e06e Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 12 Dec 2025 14:11:19 -0500 Subject: [PATCH 3/4] update WsgiTestBase --- .../src/opentelemetry/test/wsgitestutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py index 908b1d41847..9e501b38489 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py @@ -48,7 +48,8 @@ def assertTraceResponseHeaderMatchesSpan(self, headers, span): # pylint: disabl trace_id = trace.format_trace_id(span.get_span_context().trace_id) span_id = trace.format_span_id(span.get_span_context().span_id) + trace_flags = span.get_span_context().trace_flags self.assertEqual( - f"00-{trace_id}-{span_id}-01", + f"00-{trace_id}-{span_id}-{trace_flags:02x}", headers["traceresponse"], ) From 703b7fd7a5e79b019a945873e08772e44f0ab7eb Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 5 Feb 2026 13:09:19 -0500 Subject: [PATCH 4/4] fix typo in sampler tests --- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 08505301a42..9c1f148272b 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -237,8 +237,8 @@ def test_default_sampler(self): self.assertIsInstance(root_span, trace.Span) child_span = tracer.start_span(name="child span", context=ctx) self.assertIsInstance(child_span, trace.Span) - self.assertTrue(root_span.context.trace_flags.sampled) self.assertTrue(root_span.get_span_context().trace_flags.sampled) + self.assertTrue(child_span.get_span_context().trace_flags.sampled) def test_default_sampler_type(self): tracer_provider = trace.TracerProvider()