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 @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4734](https://github.com/open-telemetry/opentelemetry-python/pull/4734))
- build: bump ruff to 0.14.1
([#4782](https://github.com/open-telemetry/opentelemetry-python/pull/4782))
- otlp exporters (trace): include W3C TraceFlags (bits 0–7) in OTLP `Span.flags` alongside parent isRemote bits (8–9)
([#4761](https://github.com/open-telemetry/opentelemetry-python/pull/4761))
- semantic-conventions: Bump to 1.38.0
([#4791](https://github.com/open-telemetry/opentelemetry-python/pull/4791))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,19 @@ def _encode_resource_spans(
return pb2_resource_spans


def _span_flags(parent_span_context: Optional[SpanContext]) -> int:
flags = PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
def _span_flags(
child_trace_flags: int, parent_span_context: Optional[SpanContext]
) -> int:
# Lower 8 bits: W3C TraceFlags
# TraceFlags is an int subclass, but we handle Mock objects in tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again the code should not be aware of the mocks in tests, what tests are failing? Can we update the mocks to be speced against the class we need?

Copy link
Member

@emdneto emdneto Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for otlp-http-exporter the fix is small, need to add trace_flags to the mocked span context, here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for otlp-grpc-exporter will have to fix the same, but in more places here

try:
flags = child_trace_flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK
except TypeError:
# If bitwise operation fails (e.g., Mock object in tests), default to 0
flags = 0
# Always indicate whether parent remote information is known
flags |= PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
# Set remote bit when applicable
if parent_span_context and parent_span_context.is_remote:
flags |= PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
return flags
Expand All @@ -130,7 +141,7 @@ def _encode_span(sdk_span: ReadableSpan) -> PB2SPan:
dropped_attributes_count=sdk_span.dropped_attributes,
dropped_events_count=sdk_span.dropped_events,
dropped_links_count=sdk_span.dropped_links,
flags=_span_flags(sdk_span.parent),
flags=_span_flags(span_context.trace_flags, sdk_span.parent),
)


Expand All @@ -156,12 +167,14 @@ def _encode_links(links: Sequence[Link]) -> Sequence[PB2SPan.Link]:
if links:
pb2_links = []
for link in links:
# For links, we encode the link's own context (not treating it as parent-child)
# The link context's is_remote indicates if the linked span is from a remote process
encoded_link = PB2SPan.Link(
trace_id=_encode_trace_id(link.context.trace_id),
span_id=_encode_span_id(link.context.span_id),
attributes=_encode_attributes(link.attributes),
dropped_attributes_count=link.dropped_attributes,
flags=_span_flags(link.context),
flags=_span_flags(link.context.trace_flags, link.context),
)
pb2_links.append(encoded_link)
return pb2_links
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
)
from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import (
_SPAN_KIND_MAP,
_encode_links,
_encode_span,
_encode_status,
)
from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans
Expand All @@ -42,6 +44,7 @@
)
from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans
from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan
from opentelemetry.proto.trace.v1.trace_pb2 import SpanFlags as PB2SpanFlags
from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status
from opentelemetry.sdk.trace import Event as SDKEvent
from opentelemetry.sdk.trace import Resource as SDKResource
Expand All @@ -56,6 +59,13 @@
from opentelemetry.trace.status import Status as SDKStatus
from opentelemetry.trace.status import StatusCode as SDKStatusCode

# Mask for all currently-defined span flag bits (0-9): lower 8 trace flags + has/is remote bits
ALL_SPAN_FLAGS_MASK = ( # pylint: disable=no-member
PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK
| PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
| PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
)


class TestOTLPTraceEncoder(unittest.TestCase):
def test_encode_spans(self):
Expand Down Expand Up @@ -298,7 +308,7 @@ def get_exhaustive_test_spans(
code=SDKStatusCode.ERROR.value,
message="Example description",
),
flags=0x300,
flags=0x301,
)
],
),
Expand Down Expand Up @@ -501,3 +511,78 @@ def test_encode_status_code_translations(self):
code=SDKStatusCode.ERROR.value,
),
)


class TestSpanFlagsEncoding(unittest.TestCase):
def test_span_flags_root_unsampled(self):
span_context = SDKSpanContext(
0x1, 0x2, is_remote=False, trace_flags=0x00
)
span = SDKSpan(name="root", context=span_context, parent=None)
pb = _encode_span(span)
assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x00 # pylint: disable=no-member
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this per testcase instead so we can have this only once, i.e. put this comment in first line of this class?

assert (
pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK # pylint: disable=no-member
) != 0
assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) == 0 # pylint: disable=no-member
assert (pb.flags & ~ALL_SPAN_FLAGS_MASK) == 0

def test_span_flags_root_sampled(self):
span_context = SDKSpanContext(
0x1, 0x2, is_remote=False, trace_flags=0x01
)
span = SDKSpan(name="root", context=span_context, parent=None)
pb = _encode_span(span)
assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x01 # pylint: disable=no-member
assert (
pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK # pylint: disable=no-member
) != 0
assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) == 0 # pylint: disable=no-member
assert (pb.flags & ~ALL_SPAN_FLAGS_MASK) == 0

def test_span_flags_remote_parent_sampled(self):
parent = SDKSpanContext(0x1, 0x9, is_remote=True)
span_context = SDKSpanContext(
0x1, 0x2, is_remote=False, trace_flags=0x01
)
span = SDKSpan(name="child", context=span_context, parent=parent)
pb = _encode_span(span)
assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x01 # pylint: disable=no-member
assert (
pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK # pylint: disable=no-member
) != 0
assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0 # pylint: disable=no-member
assert (pb.flags & ~ALL_SPAN_FLAGS_MASK) == 0

def test_link_flags_local_and_remote(self):
# local sampled link
l1 = SDKLink(
SDKSpanContext(0x1, 0x2, is_remote=False, trace_flags=0x01)
)
# remote sampled link
l2 = SDKLink(
SDKSpanContext(0x1, 0x3, is_remote=True, trace_flags=0x01)
)
pb_links = _encode_links([l1, l2])
assert (
pb_links[0].flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK # pylint: disable=no-member
) == 0x01
assert (
pb_links[0].flags
& PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK # pylint: disable=no-member
) != 0
assert (
pb_links[0].flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK # pylint: disable=no-member
) == 0
assert (pb_links[0].flags & ~ALL_SPAN_FLAGS_MASK) == 0
assert (
pb_links[1].flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK # pylint: disable=no-member
) == 0x01
assert (
pb_links[1].flags
& PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK # pylint: disable=no-member
) != 0
assert (
pb_links[1].flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK # pylint: disable=no-member
) != 0
assert (pb_links[1].flags & ~ALL_SPAN_FLAGS_MASK) == 0
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ def test_translate_spans(self):
),
),
],
flags=0x300,
flags=0x300, # updated below in more focused tests
)
],
flags=0x300,
Expand Down