Skip to content

Commit bf9c49b

Browse files
committed
feat: add support for 'random-trace-id' flags in W3C traceparent header trace flags
1 parent 102fec2 commit bf9c49b

File tree

6 files changed

+65
-27
lines changed

6 files changed

+65
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798))
2121
- Silence events API warnings for internal users
2222
([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847))
23+
- `opentelemetry-api`, `opentelemetry-sdk`: add support for 'random-trace-id' flags in W3C traceparent header trace flags
24+
([#4837](https://github.com/open-telemetry/opentelemetry-python/pull/4854)
2325

2426
## Version 1.39.0/0.60b0 (2025-12-03)
2527

opentelemetry-api/src/opentelemetry/trace/span.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,21 @@ def __exit__(
197197
class TraceFlags(int):
198198
"""A bitmask that represents options specific to the trace.
199199
200-
The only supported option is the "sampled" flag (``0x01``). If set, this
201-
flag indicates that the trace may have been sampled upstream.
200+
Supported flags:
201+
- "sampled" (``0x01``): Indicates the trace may have been sampled upstream.
202+
- "random-trace-id" (``0x02``): Indicates the trace ID was generated
203+
randomly, with at least the 7 rightmost bytes (56 bits) selected
204+
with uniform distribution.
202205
203206
See the `W3C Trace Context - Traceparent`_ spec for details.
204207
205208
.. _W3C Trace Context - Traceparent:
206-
https://www.w3.org/TR/trace-context/#trace-flags
209+
https://www.w3.org/TR/trace-context-2/#trace-flags
207210
"""
208211

209212
DEFAULT = 0x00
210213
SAMPLED = 0x01
214+
RANDOM_TRACE_ID = 0x02
211215

212216
@classmethod
213217
def get_default(cls) -> "TraceFlags":
@@ -217,6 +221,10 @@ def get_default(cls) -> "TraceFlags":
217221
def sampled(self) -> bool:
218222
return bool(self & TraceFlags.SAMPLED)
219223

224+
@property
225+
def random_trace_id(self) -> bool:
226+
return bool(self & TraceFlags.RANDOM_TRACE_ID)
227+
220228

221229
DEFAULT_TRACE_OPTIONS = TraceFlags.get_default()
222230

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,12 @@ def start_span( # pylint: disable=too-many-locals
11691169
if sampling_result.decision.is_sampled()
11701170
else trace_api.TraceFlags(trace_api.TraceFlags.DEFAULT)
11711171
)
1172+
1173+
if self.id_generator.is_trace_id_random():
1174+
trace_flags = trace_api.TraceFlags(
1175+
trace_flags | trace_api.TraceFlags.RANDOM_TRACE_ID
1176+
)
1177+
11721178
span_context = trace_api.SpanContext(
11731179
trace_id,
11741180
self.id_generator.generate_span_id(),

opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,34 @@ def generate_span_id(self) -> int:
3131
def generate_trace_id(self) -> int:
3232
"""Get a new trace ID.
3333
34-
Implementations should at least make the 64 least significant bits
34+
Implementations should at least make the 56 least significant bits
3535
uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on
3636
this randomness to make sampling decisions.
3737
38+
If the implementation does randomly generate the 56 least significant bits,
39+
it should also implement `is_trace_id_random` to return True.
40+
3841
See `the specification on TraceIdRatioBased <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#traceidratiobased>`_.
3942
4043
Returns:
4144
A 128-bit int for use as a trace ID
4245
"""
4346

47+
@abc.abstractmethod
48+
def is_trace_id_random(self) -> bool:
49+
"""Indicates whether generated trace IDs are random.
50+
51+
When True, the `trace-id` field will have the `random-trace-id` flag set
52+
in the W3C traceparent header. Per the W3C Trace Context specification,
53+
this indicates that at least the 7 rightmost bytes (56 bits) of the
54+
trace ID were generated randomly with uniform distribution.
55+
56+
See `the W3C Trace Context specification <https://www.w3.org/TR/trace-context-2/#considerations-for-trace-id-field-generation>`_.
57+
58+
Returns:
59+
True if this generator produces random IDs, False otherwise.
60+
"""
61+
4462

4563
class RandomIdGenerator(IdGenerator):
4664
"""The default ID generator for TracerProvider which randomly generates all
@@ -58,3 +76,6 @@ def generate_trace_id(self) -> int:
5876
while trace_id == trace.INVALID_TRACE_ID:
5977
trace_id = random.getrandbits(128)
6078
return trace_id
79+
80+
def is_trace_id_random(self) -> bool:
81+
return True

opentelemetry-sdk/tests/test_configurator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ def generate_span_id(self):
307307
def generate_trace_id(self):
308308
pass
309309

310+
def is_trace_id_random(self):
311+
return False
312+
310313

311314
class TestTraceInit(TestCase):
312315
def setUp(self):

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,7 @@ def test_default_sampler(self):
212212
child_span = tracer.start_span(name="child span", context=ctx)
213213
self.assertIsInstance(child_span, trace.Span)
214214
self.assertTrue(root_span.context.trace_flags.sampled)
215-
self.assertEqual(
216-
root_span.get_span_context().trace_flags,
217-
trace_api.TraceFlags.SAMPLED,
218-
)
219-
self.assertEqual(
220-
child_span.get_span_context().trace_flags,
221-
trace_api.TraceFlags.SAMPLED,
222-
)
215+
self.assertTrue(root_span.get_span_context().trace_flags.sampled)
223216

224217
def test_default_sampler_type(self):
225218
tracer_provider = trace.TracerProvider()
@@ -237,14 +230,8 @@ def test_sampler_no_sampling(self, _get_from_env_or_default):
237230
self.assertIsInstance(root_span, trace_api.NonRecordingSpan)
238231
child_span = tracer.start_span(name="child span", context=ctx)
239232
self.assertIsInstance(child_span, trace_api.NonRecordingSpan)
240-
self.assertEqual(
241-
root_span.get_span_context().trace_flags,
242-
trace_api.TraceFlags.DEFAULT,
243-
)
244-
self.assertEqual(
245-
child_span.get_span_context().trace_flags,
246-
trace_api.TraceFlags.DEFAULT,
247-
)
233+
self.assertFalse(root_span.get_span_context().trace_flags.sampled)
234+
self.assertFalse(child_span.get_span_context().trace_flags.sampled)
248235
self.assertFalse(_get_from_env_or_default.called)
249236

250237
@mock.patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "always_off"})
@@ -464,9 +451,8 @@ def test_start_span_explicit(self):
464451
other_parent.get_span_context().trace_state,
465452
child_context.trace_state,
466453
)
467-
self.assertEqual(
468-
other_parent.get_span_context().trace_flags,
469-
child_context.trace_flags,
454+
self.assertTrue(
455+
other_parent.get_span_context().trace_flags.sampled
470456
)
471457

472458
# Verify start_span() did not set the current span.
@@ -827,10 +813,7 @@ def test_sampling_attributes(self):
827813
self.assertEqual(len(root.attributes), 2)
828814
self.assertEqual(root.attributes["sampler-attr"], "sample-val")
829815
self.assertEqual(root.attributes["attr-in-both"], "decision-attr")
830-
self.assertEqual(
831-
root.get_span_context().trace_flags,
832-
trace_api.TraceFlags.SAMPLED,
833-
)
816+
self.assertTrue(root.get_span_context().trace_flags.sampled)
834817

835818
def test_events(self):
836819
self.assertEqual(trace_api.get_current_span(), trace_api.INVALID_SPAN)
@@ -2066,6 +2049,9 @@ def test_constant_default(self):
20662049
def test_constant_sampled(self):
20672050
self.assertEqual(trace_api.TraceFlags.SAMPLED, 1)
20682051

2052+
def test_constant_random_trace_id(self):
2053+
self.assertEqual(trace_api.TraceFlags.RANDOM_TRACE_ID, 2)
2054+
20692055
def test_get_default(self):
20702056
self.assertEqual(
20712057
trace_api.TraceFlags.get_default(), trace_api.TraceFlags.DEFAULT
@@ -2077,6 +2063,14 @@ def test_sampled_true(self):
20772063
def test_sampled_false(self):
20782064
self.assertFalse(trace_api.TraceFlags(0xF0).sampled)
20792065

2066+
def test_random_trace_id_true(self):
2067+
self.assertTrue(trace_api.TraceFlags(0xF2).random_trace_id)
2068+
self.assertTrue(trace_api.TraceFlags(0xF3).random_trace_id)
2069+
2070+
def test_random_trace_id_false(self):
2071+
self.assertFalse(trace_api.TraceFlags(0xF0).random_trace_id)
2072+
self.assertFalse(trace_api.TraceFlags(0xF1).random_trace_id)
2073+
20802074
def test_constant_default_trace_options(self):
20812075
self.assertEqual(
20822076
trace_api.DEFAULT_TRACE_OPTIONS, trace_api.TraceFlags.DEFAULT
@@ -2214,3 +2208,7 @@ def test_generate_trace_id_avoids_invalid(self, mock_getrandbits):
22142208
self.assertNotEqual(trace_id, trace_api.INVALID_TRACE_ID)
22152209
mock_getrandbits.assert_any_call(128)
22162210
self.assertEqual(mock_getrandbits.call_count, 2)
2211+
2212+
def test_is_trace_id_random_returns_true(self):
2213+
generator = RandomIdGenerator()
2214+
self.assertTrue(generator.is_trace_id_random())

0 commit comments

Comments
 (0)