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( 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" + )