From e0bd60f1387e28d2335feac82bcdeac8a8345eda Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Thu, 17 Sep 2026 10:35:36 +0530 Subject: [PATCH 1/4] fix(tracer): support converting OpenTelemetry parent span context Allow OpenTelemetry SpanContext instances in start_span by converting valid non-Instana parent contexts into Instana SpanContext objects instead of raising a TypeError. If the context is invalid, fall back to None. Include debug logs for parent context conversion and invalid context resets. Signed-off-by: Arjun Rajappa --- src/instana/tracer.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index b5b9e2df..01a839f0 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -121,7 +121,19 @@ def start_span( parent_context = get_current_span(context).get_span_context() if parent_context and not isinstance(parent_context, SpanContext): - raise TypeError("parent_context must be an Instana SpanContext or None.") + if parent_context.is_valid: + logger.debug("Converting non-Instana parent context to Instana SpanContext") + parent_context = SpanContext( + trace_id=parent_context.trace_id, + span_id=parent_context.span_id, + is_remote=parent_context.is_remote, + trace_flags=parent_context.trace_flags, + trace_state=parent_context.trace_state, + trace_parent=True, + ) + else: + logger.debug("Non-Instana parent context is invalid, resetting to None") + parent_context = None span_context = self._create_span_context(parent_context) span = InstanaSpan( From 93cd60d0b7aa496ce193118708c297298ac0c5e1 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Thu, 17 Sep 2026 15:09:47 +0530 Subject: [PATCH 2/4] fix(tracer): add tests cases to convertion of OTEL COntext to instana Context Signed-off-by: Arjun Rajappa --- tests/test_tracer.py | 170 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 2 deletions(-) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 4e18a7d4..c2e5c922 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -2,8 +2,15 @@ import pytest from opentelemetry.context.context import Context -from opentelemetry.trace import SpanKind -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE +from opentelemetry.trace import SpanKind, TraceFlags +from opentelemetry.trace import SpanContext as OtelSpanContext +from opentelemetry.trace import set_span_in_context +from opentelemetry.trace.span import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + NonRecordingSpan, + _SPAN_ID_MAX_VALUE, +) from instana.agent.host import HostAgent from instana.recorder import StanRecorder @@ -256,3 +263,162 @@ def test_tracer_kind_propagation_to_readable_span( readable_span = span._readable_span() assert readable_span.kind == SpanKind.PRODUCER + + +# --------------------------------------------------------------------------- +# Parametrized tests: start_span with different context / parent-span types +# --------------------------------------------------------------------------- + +# A valid OTel-native trace_id and span_id (not Instana SpanContext subclass) +_OTEL_TRACE_ID = 0x000000000000000018BED7B8D2E72F6B +_OTEL_SPAN_ID = 0x5FB3484FB90A5BAD + + +def _make_otel_context(trace_id: int, span_id: int, is_valid: bool = True) -> Context: + """Return an OTel Context carrying a NonRecordingSpan with a plain OtelSpanContext.""" + if is_valid: + sc = OtelSpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=True, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, + ) + else: + # OTel INVALID_SPAN_CONTEXT has trace_id=0 and span_id=0 → is_valid == False + sc = OtelSpanContext( + trace_id=0, + span_id=0, + is_remote=False, + ) + return set_span_in_context(NonRecordingSpan(sc)) + + +def _make_instana_context(trace_id: int, span_id: int) -> Context: + """Return a Context carrying a NonRecordingSpan with an Instana SpanContext.""" + sc = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + ) + return set_span_in_context(NonRecordingSpan(sc)) + + +@pytest.mark.parametrize( + "context_factory, expected_trace_id, is_root, converts_to_instana, has_trace_parent", + [ + # --- Instana SpanContext parent (valid) --- + # The tracer must inherit the trace_id and create a new child span_id. + pytest.param( + lambda: _make_instana_context(_OTEL_TRACE_ID, _OTEL_SPAN_ID), + _OTEL_TRACE_ID, + False, # NOT a root span – parent_id must be set + False, # Already an InstanaSpanContext, no conversion + None, # trace_parent field is None (plain Instana parent) + id="instana_span_context_parent", + ), + # --- OTel SpanContext parent (valid) --- + # The tracer must convert it to an Instana SpanContext and use its trace_id. + pytest.param( + lambda: _make_otel_context(_OTEL_TRACE_ID, _OTEL_SPAN_ID, is_valid=True), + _OTEL_TRACE_ID, + False, # NOT a root span – parent_id must be set + True, # Conversion happens → child SpanContext.trace_parent == True + True, # trace_parent flag must be True after conversion + id="otel_span_context_valid_parent", + ), + # --- OTel SpanContext parent (invalid, i.e. trace_id=0, span_id=0) --- + # The tracer must ignore it and create a new root span. + pytest.param( + lambda: _make_otel_context(0, 0, is_valid=False), + None, # trace_id is generated fresh – we only check it is > 0 + True, # IS a root span – no parent + False, # No conversion (invalid context is discarded) + None, # no trace_parent on a fresh root + id="otel_span_context_invalid_parent", + ), + # --- No context (None) --- + # The tracer must create a standalone root span. + pytest.param( + lambda: None, + None, # trace_id is generated fresh – we only check it is > 0 + True, # IS a root span – no parent + False, # No conversion needed + None, # no trace_parent on a fresh root + id="no_context_root_span", + ), + ], +) +def test_tracer_start_span_with_context_types( + tracer_provider: InstanaTracerProvider, + context_factory, + expected_trace_id, + is_root: bool, + converts_to_instana: bool, + has_trace_parent, +) -> None: + """Test start_span behaviour for all relevant parent-context scenarios. + + Covers: + * Instana SpanContext as parent → trace_id inherited, new span_id, no conversion + * Valid OTel SpanContext parent → converted to InstanaSpanContext, trace_parent=True + * Invalid OTel SpanContext → discarded, new root span created + * No context (None) → new root span created + """ + span_name = "test-span-context-type" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + ctx = context_factory() + span = tracer.start_span(name=span_name, context=ctx) + + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + sc = span.context + assert isinstance(sc, SpanContext), "InstanaTracer must always produce an Instana SpanContext" + + # --- trace_id checks --- + if expected_trace_id is not None: + assert sc.trace_id == expected_trace_id, ( + f"Expected trace_id {expected_trace_id:#x}, got {sc.trace_id:#x}" + ) + else: + # Fresh root span: trace_id must be a newly generated valid value + assert sc.trace_id > INVALID_SPAN_ID + assert sc.trace_id <= _SPAN_ID_MAX_VALUE + + # --- span_id must always be a fresh, valid value --- + assert sc.span_id > INVALID_SPAN_ID + assert sc.span_id <= _SPAN_ID_MAX_VALUE + + # --- parent / root relationship --- + if is_root: + # When no valid parent is present, parent_id is either None or the + # INVALID_SPAN_ID (0) – both indicate "no real parent". + assert not span.parent_id, ( + f"Root span must have no real parent_id, got {span.parent_id!r}" + ) + # For genuine root spans trace_id == span_id (Instana convention) + if expected_trace_id is None: + assert sc.trace_id == sc.span_id, ( + "Root span must have trace_id == span_id" + ) + else: + assert span.parent_id == _OTEL_SPAN_ID, ( + "Child span must carry the parent's span_id as parent_id" + ) + assert sc.span_id != _OTEL_SPAN_ID, "Child must get a new span_id" + + # --- OTel-to-Instana conversion flag --- + if converts_to_instana: + assert sc.trace_parent is True, ( + "SpanContext created from OTel parent must have trace_parent=True" + ) + elif has_trace_parent is None: + assert sc.trace_parent is None, ( + "Non-converted SpanContext must not have trace_parent set" + ) From a8b41322bab111baec7b372f31a4c3d3e416426e Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 13 Oct 2026 13:25:40 +0200 Subject: [PATCH 3/4] chore(style): Remove unused import from test_tracer.py Signed-off-by: Paulo Vital --- tests/test_tracer.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index c2e5c922..93ddc901 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -2,14 +2,13 @@ import pytest from opentelemetry.context.context import Context -from opentelemetry.trace import SpanKind, TraceFlags from opentelemetry.trace import SpanContext as OtelSpanContext -from opentelemetry.trace import set_span_in_context +from opentelemetry.trace import SpanKind, set_span_in_context from opentelemetry.trace.span import ( + _SPAN_ID_MAX_VALUE, DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE, NonRecordingSpan, - _SPAN_ID_MAX_VALUE, ) from instana.agent.host import HostAgent @@ -312,9 +311,9 @@ def _make_instana_context(trace_id: int, span_id: int) -> Context: pytest.param( lambda: _make_instana_context(_OTEL_TRACE_ID, _OTEL_SPAN_ID), _OTEL_TRACE_ID, - False, # NOT a root span – parent_id must be set - False, # Already an InstanaSpanContext, no conversion - None, # trace_parent field is None (plain Instana parent) + False, # NOT a root span – parent_id must be set + False, # Already an InstanaSpanContext, no conversion + None, # trace_parent field is None (plain Instana parent) id="instana_span_context_parent", ), # --- OTel SpanContext parent (valid) --- @@ -322,29 +321,29 @@ def _make_instana_context(trace_id: int, span_id: int) -> Context: pytest.param( lambda: _make_otel_context(_OTEL_TRACE_ID, _OTEL_SPAN_ID, is_valid=True), _OTEL_TRACE_ID, - False, # NOT a root span – parent_id must be set - True, # Conversion happens → child SpanContext.trace_parent == True - True, # trace_parent flag must be True after conversion + False, # NOT a root span – parent_id must be set + True, # Conversion happens → child SpanContext.trace_parent == True + True, # trace_parent flag must be True after conversion id="otel_span_context_valid_parent", ), # --- OTel SpanContext parent (invalid, i.e. trace_id=0, span_id=0) --- # The tracer must ignore it and create a new root span. pytest.param( lambda: _make_otel_context(0, 0, is_valid=False), - None, # trace_id is generated fresh – we only check it is > 0 - True, # IS a root span – no parent - False, # No conversion (invalid context is discarded) - None, # no trace_parent on a fresh root + None, # trace_id is generated fresh – we only check it is > 0 + True, # IS a root span – no parent + False, # No conversion (invalid context is discarded) + None, # no trace_parent on a fresh root id="otel_span_context_invalid_parent", ), # --- No context (None) --- # The tracer must create a standalone root span. pytest.param( lambda: None, - None, # trace_id is generated fresh – we only check it is > 0 - True, # IS a root span – no parent - False, # No conversion needed - None, # no trace_parent on a fresh root + None, # trace_id is generated fresh – we only check it is > 0 + True, # IS a root span – no parent + False, # No conversion needed + None, # no trace_parent on a fresh root id="no_context_root_span", ), ], @@ -379,7 +378,9 @@ def test_tracer_start_span_with_context_types( assert span.name == span_name sc = span.context - assert isinstance(sc, SpanContext), "InstanaTracer must always produce an Instana SpanContext" + assert isinstance(sc, SpanContext), ( + "InstanaTracer must always produce an Instana SpanContext" + ) # --- trace_id checks --- if expected_trace_id is not None: @@ -404,9 +405,7 @@ def test_tracer_start_span_with_context_types( ) # For genuine root spans trace_id == span_id (Instana convention) if expected_trace_id is None: - assert sc.trace_id == sc.span_id, ( - "Root span must have trace_id == span_id" - ) + assert sc.trace_id == sc.span_id, "Root span must have trace_id == span_id" else: assert span.parent_id == _OTEL_SPAN_ID, ( "Child span must carry the parent's span_id as parent_id" From b7f1773de3a55d13424a20df5d3276fad7963749 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 13 Oct 2026 17:44:12 +0530 Subject: [PATCH 4/4] fix(tracer): handle 128-bit OpenTelemetry trace ID conversion Convert 128-bit OpenTelemetry parent trace IDs to 64-bit integers while preserving the full 32-character hex ID in long_trace_id, and default trace_parent to None. Update parametrized test cases to cover both 128-bit and 64-bit parent contexts. Signed-off-by: Arjun Rajappa --- src/instana/tracer.py | 8 +++-- tests/test_tracer.py | 68 +++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 01a839f0..266f2fd7 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -29,7 +29,7 @@ from instana.sampling import InstanaSampler, Sampler from instana.span.span import InstanaSpan, get_current_span from instana.span_context import SpanContext -from instana.util.ids import generate_id +from instana.util.ids import generate_id, hex_id, hex_id_limited if TYPE_CHECKING: from opentelemetry.trace import Span @@ -123,13 +123,15 @@ def start_span( if parent_context and not isinstance(parent_context, SpanContext): if parent_context.is_valid: logger.debug("Converting non-Instana parent context to Instana SpanContext") + hex_tid = hex_id(parent_context.trace_id) parent_context = SpanContext( - trace_id=parent_context.trace_id, + trace_id=int(hex_id_limited(parent_context.trace_id), 16) if len(hex_tid) > 16 else parent_context.trace_id, span_id=parent_context.span_id, is_remote=parent_context.is_remote, trace_flags=parent_context.trace_flags, trace_state=parent_context.trace_state, - trace_parent=True, + trace_parent=None, + long_trace_id=hex_tid if len(hex_tid) > 16 else None, ) else: logger.debug("Non-Instana parent context is invalid, resetting to None") diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 93ddc901..3c91e42b 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -268,8 +268,9 @@ def test_tracer_kind_propagation_to_readable_span( # Parametrized tests: start_span with different context / parent-span types # --------------------------------------------------------------------------- -# A valid OTel-native trace_id and span_id (not Instana SpanContext subclass) -_OTEL_TRACE_ID = 0x000000000000000018BED7B8D2E72F6B +# A valid 128-bit and 64-bit OTel trace_id and span_id +_OTEL_128BIT_TRACE_ID = 0x1234567890ABCDEF18BED7B8D2E72F6B +_OTEL_64BIT_TRACE_ID = 0x18BED7B8D2E72F6B _OTEL_SPAN_ID = 0x5FB3484FB90A5BAD @@ -304,36 +305,43 @@ def _make_instana_context(trace_id: int, span_id: int) -> Context: @pytest.mark.parametrize( - "context_factory, expected_trace_id, is_root, converts_to_instana, has_trace_parent", + "context_factory, expected_trace_id, expected_long_trace_id, is_root", [ # --- Instana SpanContext parent (valid) --- # The tracer must inherit the trace_id and create a new child span_id. pytest.param( - lambda: _make_instana_context(_OTEL_TRACE_ID, _OTEL_SPAN_ID), - _OTEL_TRACE_ID, + lambda: _make_instana_context(_OTEL_64BIT_TRACE_ID, _OTEL_SPAN_ID), + _OTEL_64BIT_TRACE_ID, + None, False, # NOT a root span – parent_id must be set - False, # Already an InstanaSpanContext, no conversion - None, # trace_parent field is None (plain Instana parent) id="instana_span_context_parent", ), - # --- OTel SpanContext parent (valid) --- - # The tracer must convert it to an Instana SpanContext and use its trace_id. + # --- OTel SpanContext parent with 128-bit trace ID (valid) --- + # The tracer must convert it to an Instana SpanContext: + # trace_id set to lower 64 bits, long_trace_id set to full 32-char hex string. pytest.param( - lambda: _make_otel_context(_OTEL_TRACE_ID, _OTEL_SPAN_ID, is_valid=True), - _OTEL_TRACE_ID, + lambda: _make_otel_context(_OTEL_128BIT_TRACE_ID, _OTEL_SPAN_ID, is_valid=True), + _OTEL_64BIT_TRACE_ID, + "1234567890abcdef18bed7b8d2e72f6b", False, # NOT a root span – parent_id must be set - True, # Conversion happens → child SpanContext.trace_parent == True - True, # trace_parent flag must be True after conversion - id="otel_span_context_valid_parent", + id="otel_span_context_128bit_parent", + ), + # --- OTel SpanContext parent with 64-bit trace ID (valid) --- + # The tracer must convert it to an Instana SpanContext and use its trace_id directly. + pytest.param( + lambda: _make_otel_context(_OTEL_64BIT_TRACE_ID, _OTEL_SPAN_ID, is_valid=True), + _OTEL_64BIT_TRACE_ID, + None, + False, # NOT a root span – parent_id must be set + id="otel_span_context_64bit_parent", ), # --- OTel SpanContext parent (invalid, i.e. trace_id=0, span_id=0) --- # The tracer must ignore it and create a new root span. pytest.param( lambda: _make_otel_context(0, 0, is_valid=False), None, # trace_id is generated fresh – we only check it is > 0 + None, True, # IS a root span – no parent - False, # No conversion (invalid context is discarded) - None, # no trace_parent on a fresh root id="otel_span_context_invalid_parent", ), # --- No context (None) --- @@ -341,9 +349,8 @@ def _make_instana_context(trace_id: int, span_id: int) -> Context: pytest.param( lambda: None, None, # trace_id is generated fresh – we only check it is > 0 + None, True, # IS a root span – no parent - False, # No conversion needed - None, # no trace_parent on a fresh root id="no_context_root_span", ), ], @@ -352,17 +359,17 @@ def test_tracer_start_span_with_context_types( tracer_provider: InstanaTracerProvider, context_factory, expected_trace_id, + expected_long_trace_id, is_root: bool, - converts_to_instana: bool, - has_trace_parent, ) -> None: """Test start_span behaviour for all relevant parent-context scenarios. Covers: - * Instana SpanContext as parent → trace_id inherited, new span_id, no conversion - * Valid OTel SpanContext parent → converted to InstanaSpanContext, trace_parent=True - * Invalid OTel SpanContext → discarded, new root span created - * No context (None) → new root span created + * Instana SpanContext as parent → trace_id inherited, new span_id + * Valid 128-bit OTel SpanContext parent → trace_id has lower 64 bits, long_trace_id has full hex + * Valid 64-bit OTel SpanContext parent → trace_id inherited, long_trace_id is None + * Invalid OTel SpanContext → discarded, new root span created + * No context (None) → new root span created """ span_name = "test-span-context-type" tracer = InstanaTracer( @@ -392,6 +399,9 @@ def test_tracer_start_span_with_context_types( assert sc.trace_id > INVALID_SPAN_ID assert sc.trace_id <= _SPAN_ID_MAX_VALUE + # --- long_trace_id checks --- + assert sc.long_trace_id == expected_long_trace_id + # --- span_id must always be a fresh, valid value --- assert sc.span_id > INVALID_SPAN_ID assert sc.span_id <= _SPAN_ID_MAX_VALUE @@ -411,13 +421,3 @@ def test_tracer_start_span_with_context_types( "Child span must carry the parent's span_id as parent_id" ) assert sc.span_id != _OTEL_SPAN_ID, "Child must get a new span_id" - - # --- OTel-to-Instana conversion flag --- - if converts_to_instana: - assert sc.trace_parent is True, ( - "SpanContext created from OTel parent must have trace_parent=True" - ) - elif has_trace_parent is None: - assert sc.trace_parent is None, ( - "Non-converted SpanContext must not have trace_parent set" - )