Skip to content
Merged
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
18 changes: 16 additions & 2 deletions src/instana/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,7 +121,21 @@ 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")
hex_tid = hex_id(parent_context.trace_id)
parent_context = SpanContext(
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=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")
parent_context = None

span_context = self._create_span_context(parent_context)
span = InstanaSpan(
Expand Down
169 changes: 167 additions & 2 deletions tests/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

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 SpanContext as OtelSpanContext
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,
)

from instana.agent.host import HostAgent
from instana.recorder import StanRecorder
Expand Down Expand Up @@ -256,3 +262,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 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


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, 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_64BIT_TRACE_ID, _OTEL_SPAN_ID),
_OTEL_64BIT_TRACE_ID,
None,
False, # NOT a root span – parent_id must be set
id="instana_span_context_parent",
),
# --- 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_128BIT_TRACE_ID, _OTEL_SPAN_ID, is_valid=True),
_OTEL_64BIT_TRACE_ID,
"1234567890abcdef18bed7b8d2e72f6b",
False, # NOT a root span – parent_id must be set
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
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
None,
True, # IS a root span – no parent
id="no_context_root_span",
),
],
)
def test_tracer_start_span_with_context_types(
tracer_provider: InstanaTracerProvider,
context_factory,
expected_trace_id,
expected_long_trace_id,
is_root: bool,
) -> None:
"""Test start_span behaviour for all relevant parent-context scenarios.

Covers:
* 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(
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

# --- 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

# --- 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"
Loading