Skip to content
Draft
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
48 changes: 48 additions & 0 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@

from opentelemetry import context as context_api
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.context import get_current
from opentelemetry.context.context import Context
from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER
from opentelemetry.trace.propagation import (
Expand Down Expand Up @@ -402,6 +403,31 @@ def function():
The newly-created span.
"""

@_agnosticcontextmanager
@abstractmethod
def apply_egress_continuation(
self,
kind: SpanKind,
context: Context | None = None,
attributes: types.Attributes = None,
) -> Iterator[Context]:
"""Context manager for getting a Context informed with continuation hints
for egress propagation based on span kind and attributes.

Example::

with tracer.apply_egress_continuation(kind=Span.CLIENT, attributes=attributes) as injection_context:
propagate.inject(carrier, context=injection_context, setter=setter)

Args:
context: An optional Context. Defaults to the global context.
kind: The span's kind.
attributes: The span's attributes.

Yields:
A context that contains egress continuation hints for propagators
"""


class ProxyTracer(Tracer):
# pylint: disable=W0222,signature-differs
Expand Down Expand Up @@ -442,6 +468,18 @@ def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]:
with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore
yield span

@_agnosticcontextmanager
def apply_egress_continuation(
self,
kind: SpanKind,
context: Context | None = None,
attributes: types.Attributes = None,
) -> Iterator[Context]:
with self._tracer.apply_egress_continuation(
kind, context, attributes
) as injection_context:
yield injection_context


class NoOpTracer(Tracer):
"""The default Tracer, used when no Tracer implementation is available.
Expand Down Expand Up @@ -507,6 +545,16 @@ def start_as_current_span(
) as span:
yield span

@_agnosticcontextmanager
def apply_egress_continuation(
self,
kind: SpanKind,
context: Context | None = None,
attributes: types.Attributes = None,
) -> Iterator[Context]:
injected_context = context if context else get_current()
yield injected_context


@deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.")
class _DefaultTracer(NoOpTracer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,44 @@
#
import re

from opentelemetry import context as context_api
from opentelemetry import trace
from opentelemetry.context.context import Context
from opentelemetry.propagators import textmap
from opentelemetry.trace import format_span_id, format_trace_id
from opentelemetry.trace.span import TraceState

_SUPPRESS_TRACE_CONTEXT_INJECTION_KEY = context_api.create_key(
"suppress-trace-context-injection"
)


def suppress_trace_context_injection(
context: Context | None = None,
) -> Context:
"""Returns a context that suppresses W3C Trace Context injection."""
return context_api.set_value(
_SUPPRESS_TRACE_CONTEXT_INJECTION_KEY, True, context
)


def enable_trace_context_injection(
context: Context | None = None,
) -> Context:
"""Returns a context that allows W3C Trace Context injection."""
return context_api.set_value(
_SUPPRESS_TRACE_CONTEXT_INJECTION_KEY, False, context
)


def is_trace_context_injection_suppressed(
context: Context | None = None,
) -> bool:
"""Returns whether W3C Trace Context injection is suppressed."""
return bool(
context_api.get_value(_SUPPRESS_TRACE_CONTEXT_INJECTION_KEY, context)
)


class TraceContextTextMapPropagator(textmap.TextMapPropagator):
"""Extracts and injects using w3c TraceContext's headers."""
Expand Down Expand Up @@ -84,6 +116,9 @@ def inject(

See `opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
if is_trace_context_injection_suppressed(context):
return

span = trace.get_current_span(context)
span_context = span.get_span_context()
if span_context == trace.INVALID_SPAN_CONTEXT:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,34 @@ def test_propagate_invalid_context(self):
FORMAT.inject(output, context=ctx)
self.assertFalse("traceparent" in output)

def test_suppress_trace_context_injection(self):
"""Do not propagate trace context when injection is suppressed."""
output: dict[str, str] = {}
span = trace.NonRecordingSpan(
trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False)
)
ctx = trace.set_span_in_context(span)
ctx = tracecontext.suppress_trace_context_injection(ctx)

FORMAT.inject(output, context=ctx)

self.assertFalse("traceparent" in output)
self.assertFalse("tracestate" in output)

def test_enable_trace_context_injection(self):
"""Propagate trace context after inherited suppression is cleared."""
output: dict[str, str] = {}
span = trace.NonRecordingSpan(
trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False)
)
ctx = trace.set_span_in_context(span)
ctx = tracecontext.suppress_trace_context_injection(ctx)
ctx = tracecontext.enable_trace_context_injection(ctx)

FORMAT.inject(output, context=ctx)

self.assertTrue("traceparent" in output)

def test_tracestate_empty_header(self):
"""Test tracestate with an additional empty header (should be ignored)"""
span = trace.get_current_span(
Expand Down
49 changes: 49 additions & 0 deletions opentelemetry-api/tests/trace/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@
import asyncio
from unittest import TestCase

from opentelemetry.context import get_current
from opentelemetry.trace import (
INVALID_SPAN,
NonRecordingSpan,
NoOpTracer,
Span,
SpanContext,
SpanKind,
Tracer,
_agnosticcontextmanager,
get_current_span,
set_span_in_context,
)
from opentelemetry.trace.propagation import tracecontext


class TestTracer(TestCase):
Expand All @@ -27,6 +33,45 @@ def test_start_as_current_span_context_manager(self):
with self.tracer.start_as_current_span("") as span:
self.assertIsInstance(span, Span)

def test_apply_egress_continuation_noop_tracer_preserves_context(self):
propagator = tracecontext.TraceContextTextMapPropagator()
span_context = SpanContext(
trace_id=0x000000000000000000000000DEADBEEF,
span_id=0x00000000DEADBEF0,
is_remote=False,
)
context = set_span_in_context(NonRecordingSpan(span_context))

with self.tracer.apply_egress_continuation(
kind=SpanKind.CLIENT,
context=context,
attributes={"server.address": "api.example.com"},
) as injection_context:
carrier: dict[str, str] = {}
propagator.inject(carrier, context=injection_context)

self.assertIn("traceparent", carrier)

def test_apply_egress_continuation_noop_tracer_preserves_suppression(self):
propagator = tracecontext.TraceContextTextMapPropagator()
span_context = SpanContext(
trace_id=0x000000000000000000000000DEADBEEF,
span_id=0x00000000DEADBEF0,
is_remote=False,
)
context = set_span_in_context(NonRecordingSpan(span_context))
context = tracecontext.suppress_trace_context_injection(context)

with self.tracer.apply_egress_continuation(
kind=SpanKind.CLIENT,
context=context,
attributes={"server.address": "api.example.com"},
) as injection_context:
carrier: dict[str, str] = {}
propagator.inject(carrier, context=injection_context)

self.assertNotIn("traceparent", carrier)

def test_start_as_current_span_decorator(self):
# using a list to track the mock call order
calls = []
Expand All @@ -41,6 +86,10 @@ def start_as_current_span(self, *args, **kwargs): # type: ignore
yield INVALID_SPAN
calls.append(9)

@_agnosticcontextmanager # pylint: disable=protected-access
def apply_egress_continuation(self, *args, **kwargs): # type: ignore
yield get_current()

mock_tracer = MockTracer()

# test 1 : sync function
Expand Down
Loading
Loading