diff --git a/CHANGELOG.md b/CHANGELOG.md index 90fcb56cb7e..b1961da8efb 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 ([#4755](https://github.com/open-telemetry/opentelemetry-python/pull/4755)) - logs: extend Logger.emit to accept separated keyword arguments ([#4737](https://github.com/open-telemetry/opentelemetry-python/pull/4737)) +- 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)) ## Version 1.37.0/0.58b0 (2025-09-11) 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..9c5cbfe3a2b 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,12 @@ 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 + flags = child_trace_flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK + # 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 +134,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), ) @@ -161,7 +165,7 @@ def _encode_links(links: Sequence[Link]) -> Sequence[PB2SPan.Link]: 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..7c145615675 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 @@ -24,6 +24,8 @@ from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( _SPAN_KIND_MAP, _encode_status, + _encode_span, + _encode_links, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( @@ -43,6 +45,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 Status as PB2Status +from opentelemetry.proto.trace.v1.trace_pb2 import SpanFlags as PB2SpanFlags from opentelemetry.sdk.trace import Event as SDKEvent from opentelemetry.sdk.trace import Resource as SDKResource from opentelemetry.sdk.trace import SpanContext as SDKSpanContext @@ -291,14 +294,14 @@ def get_exhaustive_test_spans( ), ), ], - flags=0x100, + flags=0x101, ) ], status=PB2Status( code=SDKStatusCode.ERROR.value, message="Example description", ), - flags=0x300, + flags=0x301, ) ], ), @@ -501,3 +504,51 @@ def test_encode_status_code_translations(self): code=SDKStatusCode.ERROR.value, ), ) + + +class TestSpanFlagsEncoding(unittest.TestCase): + def test_span_flags_root_unsampled(self): + sc = SDKSpanContext(0x1, 0x2, is_remote=False, trace_flags=0x00) + span = SDKSpan(name="root", context=sc, parent=None) + span.start(); span.end() + pb = _encode_span(span) + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x00 + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0 + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) == 0 + assert (pb.flags & ~0x3FF) == 0 + + def test_span_flags_root_sampled(self): + sc = SDKSpanContext(0x1, 0x2, is_remote=False, trace_flags=0x01) + span = SDKSpan(name="root", context=sc, parent=None) + span.start(); span.end() + pb = _encode_span(span) + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x01 + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0 + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) == 0 + assert (pb.flags & ~0x3FF) == 0 + + def test_span_flags_remote_parent_sampled(self): + parent = SDKSpanContext(0x1, 0x9, is_remote=True) + sc = SDKSpanContext(0x1, 0x2, is_remote=False, trace_flags=0x01) + span = SDKSpan(name="child", context=sc, parent=parent) + span.start(); span.end() + pb = _encode_span(span) + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x01 + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0 + assert (pb.flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0 + assert (pb.flags & ~0x3FF) == 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) == 0x01 + assert (pb_links[0].flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0 + assert (pb_links[0].flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) == 0 + assert (pb_links[0].flags & ~0x3FF) == 0 + assert (pb_links[1].flags & PB2SpanFlags.SPAN_FLAGS_TRACE_FLAGS_MASK) == 0x01 + assert (pb_links[1].flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0 + assert (pb_links[1].flags & PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0 + assert (pb_links[1].flags & ~0x3FF) == 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..bcf8404f171 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, @@ -570,10 +570,10 @@ def test_translate_spans_multi(self): ), ), ], - flags=0x300, + flags=0x301, ) ], - flags=0x300, + flags=0x301, ) ], ), @@ -603,7 +603,7 @@ def test_translate_spans_multi(self): OTLPSpan.SpanKind.SPAN_KIND_INTERNAL ), status=Status(code=0, message=""), - flags=0x300, + flags=0x301, ) ], ), @@ -645,7 +645,7 @@ def test_translate_spans_multi(self): OTLPSpan.SpanKind.SPAN_KIND_INTERNAL ), status=Status(code=0, message=""), - flags=0x300, + flags=0x301, ) ], )