From fea2e0adb212d245751805ddeb4c53275649d889 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 26 Feb 2026 15:20:07 -0500 Subject: [PATCH 1/2] fix(tracer): restore OpenTelemetry API compliance for context parameter Add 'context' parameter to start_span() and start_as_current_span() while maintaining backward compatibility with 'span_context' parameter. Fixes TypeError with OpenTelemetry-compliant libraries (regression from c4d4251). Includes 15 new tests, all 20 tests pass. Signed-off-by: Michael Ramos --- src/instana/tracer.py | 24 +++- tests/test_tracer.py | 317 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 5 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 8546a8a1..8b2aa39e 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -13,6 +13,7 @@ Tracer, TracerProvider, _Links, + get_current_span as otel_get_current_span, use_span, ) from opentelemetry.util import types @@ -27,7 +28,8 @@ from instana.propagators.text_propagator import TextPropagator from instana.recorder import StanRecorder from instana.sampling import InstanaSampler, Sampler -from instana.span.span import InstanaSpan, get_current_span +from instana.span.span import InstanaSpan +from instana.span.span import get_current_span as instana_get_current_span from instana.span_context import SpanContext from instana.util.ids import generate_id @@ -108,16 +110,21 @@ def exporter(self) -> Optional[Type["BaseAgent"]]: def start_span( self, name: str, - span_context: Optional[SpanContext] = None, + context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, start_time: Optional[int] = None, record_exception: bool = True, set_status_on_exception: bool = True, + span_context: Optional[SpanContext] = None, ) -> InstanaSpan: + # Extract span_context from context if needed (OpenTelemetry API compliance) + if context is not None and span_context is None: + span_context = otel_get_current_span(context).get_span_context() # type: ignore[assignment] + parent_context = ( - span_context if span_context else get_current_span().get_span_context() + span_context if span_context else instana_get_current_span().get_span_context() ) if parent_context and not isinstance(parent_context, SpanContext): @@ -140,7 +147,7 @@ def start_span( def start_as_current_span( self, name: str, - span_context: Optional[SpanContext] = None, + context: Optional[Context] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, @@ -148,16 +155,23 @@ def start_as_current_span( record_exception: bool = True, set_status_on_exception: bool = True, end_on_exit: bool = True, + span_context: Optional[SpanContext] = None, ) -> Iterator[InstanaSpan]: + # Extract span_context from context if needed (OpenTelemetry API compliance) + if context is not None and span_context is None: + span_context = otel_get_current_span(context).get_span_context() # type: ignore[assignment] + + # Pass both context and span_context - the guard in start_span prevents redundant extraction span = self.start_span( name=name, - span_context=span_context, + context=context, kind=kind, attributes=attributes, links=links, start_time=start_time, record_exception=record_exception, set_status_on_exception=set_status_on_exception, + span_context=span_context, ) with use_span( span, diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 13ed495e..6fd18628 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,6 +1,8 @@ # (c) Copyright IBM Corp. 2024 import pytest +from opentelemetry.context.context import Context +from opentelemetry.trace import set_span_in_context from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE from instana.agent.host import HostAgent @@ -142,3 +144,318 @@ def test_tracer_create_span_context_root( assert new_span_context.trace_id == new_span_context.span_id + + +# OpenTelemetry Compliance Tests for context parameter + +def test_tracer_start_span_with_context_parameter( + tracer_provider: InstanaTracerProvider, span: InstanaSpan, context: Context +) -> None: + """Test start_span() with context parameter (OpenTelemetry-compliant usage).""" + span_name = "test-span-with-context" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Start a span with context parameter + new_span = tracer.start_span(name=span_name, context=context) + + assert new_span + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + # Verify the span has the correct parent from the context + assert new_span.parent_id == span.context.span_id + + +def test_tracer_start_span_with_span_context_parameter( + tracer_provider: InstanaTracerProvider, span_context: SpanContext +) -> None: + """Test start_span() with span_context parameter (backward compatibility).""" + span_name = "test-span-with-span-context" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Start a span with span_context parameter (existing behavior) + new_span = tracer.start_span(name=span_name, span_context=span_context) + + assert new_span + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + assert new_span.parent_id == span_context.span_id + + +def test_tracer_start_span_with_both_parameters( + tracer_provider: InstanaTracerProvider, + span_context: SpanContext, + context: Context +) -> None: + """Test start_span() with both context and span_context (span_context takes precedence).""" + span_name = "test-span-both-params" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # When both are provided, span_context should take precedence + new_span = tracer.start_span( + name=span_name, + context=context, + span_context=span_context + ) + + assert new_span + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + # Should use span_context, not the one from context + assert new_span.parent_id == span_context.span_id + + +def test_tracer_start_span_with_neither_parameter( + tracer_provider: InstanaTracerProvider +) -> None: + """Test start_span() with neither parameter (uses current span context).""" + span_name = "test-span-no-params" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Start a span without any context parameters + new_span = tracer.start_span(name=span_name) + + assert new_span + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + # Should be a root span since no parent context is provided + # In Instana, root spans have parent_id=0 (INVALID_SPAN_ID) + assert new_span.parent_id == 0 + + +def test_tracer_start_span_context_extraction( + tracer_provider: InstanaTracerProvider, + span: InstanaSpan, + context: Context +) -> None: + """Test that span_context is correctly extracted from context parameter.""" + span_name = "test-span-context-extraction" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Start a span with context parameter + new_span = tracer.start_span(name=span_name, context=context) + + # Verify the extracted span_context matches the span in the context + assert new_span.parent_id == span.context.span_id + assert new_span.context.trace_id == span.context.trace_id + + +def test_tracer_start_as_current_span_with_context_parameter( + tracer_provider: InstanaTracerProvider, + span: InstanaSpan, + context: Context +) -> None: + """Test start_as_current_span() with context parameter (OpenTelemetry-compliant usage).""" + span_name = "test-current-span-with-context" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Use context manager with context parameter + with tracer.start_as_current_span(name=span_name, context=context) as new_span: + assert new_span is not None + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + assert new_span.parent_id == span.context.span_id + assert get_current_span() is new_span + + +def test_tracer_start_as_current_span_with_span_context_parameter( + tracer_provider: InstanaTracerProvider, span_context: SpanContext +) -> None: + """Test start_as_current_span() with span_context parameter (backward compatibility).""" + span_name = "test-current-span-with-span-context" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Use context manager with span_context parameter + with tracer.start_as_current_span( + name=span_name, span_context=span_context + ) as new_span: + assert new_span is not None + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + assert new_span.parent_id == span_context.span_id + + +def test_tracer_start_as_current_span_with_both_parameters( + tracer_provider: InstanaTracerProvider, + span_context: SpanContext, + context: Context +) -> None: + """Test start_as_current_span() with both parameters (span_context takes precedence).""" + span_name = "test-current-span-both-params" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # When both are provided, span_context should take precedence + with tracer.start_as_current_span( + name=span_name, context=context, span_context=span_context + ) as new_span: + assert new_span is not None + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + # Should use span_context, not the one from context + assert new_span.parent_id == span_context.span_id + + +def test_tracer_start_as_current_span_with_neither_parameter( + tracer_provider: InstanaTracerProvider +) -> None: + """Test start_as_current_span() with neither parameter (uses current span context).""" + span_name = "test-current-span-no-params" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Use context manager without any context parameters + with tracer.start_as_current_span(name=span_name) as new_span: + assert new_span is not None + assert isinstance(new_span, InstanaSpan) + assert new_span.name == span_name + # Should be a root span since no parent context is provided + # In Instana, root spans have parent_id=0 (INVALID_SPAN_ID) + assert new_span.parent_id == 0 + + +def test_tracer_start_as_current_span_context_manager_works( + tracer_provider: InstanaTracerProvider +) -> None: + """Test that the context manager properly sets and unsets the current span.""" + span_name = "test-context-manager" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Before entering context manager + initial_span = get_current_span() + + with tracer.start_as_current_span(name=span_name) as new_span: + # Inside context manager, new_span should be current + assert get_current_span() is new_span + assert new_span.end_time is None + + # After exiting context manager, span should be ended + assert new_span.end_time is not None + # Current span should be restored (or INVALID_SPAN if no parent) + assert get_current_span() is initial_span or get_current_span() is INVALID_SPAN + + +def test_tracer_minimal_example_from_bug_report( + tracer_provider: InstanaTracerProvider +) -> None: + """Test the minimal example from BUG_REPORT.md that previously caused TypeError.""" + # This is the exact code from the bug report that should now work + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # This should not raise TypeError anymore + with tracer.start_as_current_span("test-span", context=None): + pass + + # Test also works with start_span + span = tracer.start_span("test-span", context=None) + assert span is not None + assert isinstance(span, InstanaSpan) + + +def test_tracer_context_none_vs_not_provided( + tracer_provider: InstanaTracerProvider +) -> None: + """Test that context=None behaves the same as not providing context parameter.""" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + # Create span with context=None + span1 = tracer.start_span("span1", context=None) + + # Create span without context parameter + span2 = tracer.start_span("span2") + + # Both should be root spans with no parent + # In Instana, root spans have parent_id=0 (INVALID_SPAN_ID) + assert span1.parent_id == 0 + assert span2.parent_id == 0 + + # Both should have valid span contexts + assert span1.context.is_valid + assert span2.context.is_valid + + +def test_tracer_nested_spans_with_context_parameter( + tracer_provider: InstanaTracerProvider +) -> None: + """Test nested spans using context parameter.""" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + parent_span_name = "parent-span-context" + child_span_name = "child-span-context" + + with tracer.start_as_current_span(name=parent_span_name) as parent_span: + # Get the context with the parent span + parent_context = set_span_in_context(parent_span) + + # Create child span using context parameter + with tracer.start_as_current_span( + name=child_span_name, context=parent_context + ) as child_span: + assert get_current_span() is child_span + assert child_span.parent_id == parent_span.context.span_id + assert child_span.context.trace_id == parent_span.context.trace_id + + # After child exits, parent should be current again + assert get_current_span() is parent_span From f367d2cbf27ada97815cb5f66fa5d8d0c95987c8 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 26 Feb 2026 21:36:20 -0500 Subject: [PATCH 2/2] fix(tracer): use isinstance guard when extracting span context from OTel context Signed-off-by: Michael Ramos --- src/instana/tracer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 8b2aa39e..408c62f0 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -121,7 +121,10 @@ def start_span( ) -> InstanaSpan: # Extract span_context from context if needed (OpenTelemetry API compliance) if context is not None and span_context is None: - span_context = otel_get_current_span(context).get_span_context() # type: ignore[assignment] + otel_ctx = otel_get_current_span(context).get_span_context() + if isinstance(otel_ctx, SpanContext): + span_context = otel_ctx + parent_context = ( span_context if span_context else instana_get_current_span().get_span_context() @@ -159,7 +162,9 @@ def start_as_current_span( ) -> Iterator[InstanaSpan]: # Extract span_context from context if needed (OpenTelemetry API compliance) if context is not None and span_context is None: - span_context = otel_get_current_span(context).get_span_context() # type: ignore[assignment] + otel_ctx = otel_get_current_span(context).get_span_context() + if isinstance(otel_ctx, SpanContext): + span_context = otel_ctx # Pass both context and span_context - the guard in start_span prevents redundant extraction span = self.start_span(