Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/instana/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
170 changes: 168 additions & 2 deletions tests/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
)
Loading