diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b13589e3d..200854de313 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 ([#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)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py index 388d229bab6..644a0ed0d55 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py @@ -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 + 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 @@ -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), ) @@ -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 diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py index bf78526d7e4..43362f3ae6f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py @@ -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 @@ -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 @@ -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): @@ -298,7 +308,7 @@ def get_exhaustive_test_spans( code=SDKStatusCode.ERROR.value, message="Example description", ), - flags=0x300, + flags=0x301, ) ], ), @@ -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 + 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 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index cbe6298df77..4a50938e28c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -466,7 +466,7 @@ def test_translate_spans(self): ), ), ], - flags=0x300, + flags=0x300, # updated below in more focused tests ) ], flags=0x300,