Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798))
- Silence events API warnings for internal users
([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847))
- `opentelemetry-api`, `opentelemetry-sdk`: add support for 'random-trace-id' flags in W3C traceparent header trace flags
([#4837](https://github.com/open-telemetry/opentelemetry-python/pull/4854)

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

Expand Down
14 changes: 11 additions & 3 deletions opentelemetry-api/src/opentelemetry/trace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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()

Expand Down
6 changes: 6 additions & 0 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,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(),
Expand Down
25 changes: 24 additions & 1 deletion opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,36 @@ 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 <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#traceidratiobased>`_.

Returns:
A 128-bit int for use as a trace ID
"""

# pylint: disable=no-self-use
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 <https://www.w3.org/TR/trace-context-2/#considerations-for-trace-id-field-generation>`_.

Returns:
True if this generator produces random IDs, False otherwise.
"""
# By default, return False for backwards compatibility.
return False


class RandomIdGenerator(IdGenerator):
"""The default ID generator for TracerProvider which randomly generates all
Expand All @@ -58,3 +78,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
3 changes: 3 additions & 0 deletions opentelemetry-sdk/tests/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,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):
Expand Down
44 changes: 21 additions & 23 deletions opentelemetry-sdk/tests/trace/test_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,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()
Expand All @@ -237,14 +230,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"})
Expand Down Expand Up @@ -464,9 +451,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.
Expand Down Expand Up @@ -827,10 +813,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)
Expand Down Expand Up @@ -2066,6 +2049,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
Expand All @@ -2077,6 +2063,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
Expand Down Expand Up @@ -2214,3 +2208,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())
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
Loading