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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862))
- `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters
([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709))
- Implement experimental TracerConfigurator
([#4861](https://github.com/open-telemetry/opentelemetry-python/pull/4861))

## Version 1.39.0/0.60b0 (2025-12-03)

Expand Down
34 changes: 32 additions & 2 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,21 @@ def start_span(
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
return INVALID_SPAN
current_span = get_current_span(context)
if isinstance(current_span, NonRecordingSpan):
return current_span
parent_span_context = current_span.get_span_context()
if parent_span_context is not None and not isinstance(
parent_span_context, SpanContext
):
logger.warning(
"Invalid span context for %s: %s",
current_span,
parent_span_context,
)
return INVALID_SPAN

return NonRecordingSpan(context=parent_span_context)

@_agnosticcontextmanager
def start_as_current_span(
Expand All @@ -486,7 +500,23 @@ def start_as_current_span(
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
yield INVALID_SPAN
span = self.start_span(
name=name,
context=context,
kind=kind,
attributes=attributes,
links=links,
start_time=start_time,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
)
with use_span(
span,
end_on_exit=end_on_exit,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
) as span:
yield span


@deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.")
Expand Down
100 changes: 92 additions & 8 deletions opentelemetry-api/tests/test_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,63 @@
from opentelemetry import trace


class RecordingSpan(trace.Span):
def __init__(self, context: trace.SpanContext) -> None:
self._context = context

def get_span_context(self) -> trace.SpanContext:
return self._context

def is_recording(self) -> bool:
return True

def end(self, end_time=None) -> None:
pass

def set_attributes(self, attributes) -> None:
pass

def set_attribute(self, key, value) -> None:
pass

def add_event(
self,
name: str,
attributes=None,
timestamp=None,
) -> None:
pass

def add_link(
self,
context,
attributes=None,
) -> None:
pass

def update_name(self, name) -> None:
pass

def set_status(
self,
status,
description=None,
) -> None:
pass

def record_exception(
self,
exception,
attributes=None,
timestamp=None,
escaped=False,
) -> None:
pass

def __repr__(self) -> str:
return f"RecordingSpan({self._context!r})"


class TestAPIOnlyImplementation(unittest.TestCase):
"""
This test is in place to ensure the API is returning values that
Expand All @@ -36,18 +93,45 @@ def test_default_tracer(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
with tracer.start_span("test") as span:
self.assertEqual(
span.get_span_context(), trace.INVALID_SPAN_CONTEXT
)
self.assertEqual(span, trace.INVALID_SPAN)
self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span.is_recording(), False)
with tracer.start_span("test2") as span2:
self.assertEqual(
span2.get_span_context(), trace.INVALID_SPAN_CONTEXT
)
self.assertEqual(span2, trace.INVALID_SPAN)
self.assertFalse(span2.get_span_context().is_valid)
self.assertIs(span2.is_recording(), False)

def test_default_tracer_context_propagation_recording_span(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
span_context = trace.SpanContext(
2604504634922341076776623263868986797,
5213367945872657620,
False,
trace.TraceFlags(0x01),
)
ctx = trace.set_span_in_context(RecordingSpan(context=span_context))
with tracer.start_span("test", context=ctx) as span:
self.assertTrue(span.get_span_context().is_valid)
self.assertEqual(span.get_span_context(), span_context)
self.assertIs(span.is_recording(), False)

def test_default_tracer_context_propagation_non_recording_span(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
ctx = trace.set_span_in_context(trace.INVALID_SPAN)
with tracer.start_span("test", context=ctx) as span:
self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span, trace.INVALID_SPAN)

def test_default_tracer_context_propagation_with_invalid_context(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
ctx = trace.set_span_in_context(
RecordingSpan(context="invalid_context") # type: ignore[reportArgumentType]
)
with tracer.start_span("test", context=ctx) as span:
self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span, trace.INVALID_SPAN)

def test_span(self):
with self.assertRaises(TypeError):
# pylint: disable=abstract-class-instantiated
Expand Down
4 changes: 2 additions & 2 deletions opentelemetry-api/tests/trace/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def my_function() -> Span:
return trace.get_current_span()

# call function before configuring tracing provider, should
# return INVALID_SPAN from the NoOpTracer
self.assertEqual(my_function(), trace.INVALID_SPAN)
# return NonRecordingSpan from the NoOpTracer
self.assertFalse(my_function().is_recording())

# configure tracing provider
trace.set_tracer_provider(TestProvider())
Expand Down
2 changes: 1 addition & 1 deletion opentelemetry-api/tests/trace/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,5 @@ async def function_async(data: str) -> int:
def test_get_current_span(self):
with self.tracer.start_as_current_span("test") as span:
get_current_span().set_attribute("test", "test")
self.assertEqual(span, INVALID_SPAN)
self.assertFalse(span.is_recording())
self.assertFalse(hasattr("span", "attributes"))
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTEL_PYTHON_TRACER_CONFIGURATOR,
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG,
)
Expand All @@ -58,7 +59,7 @@
PeriodicExportingMetricReader,
)
from opentelemetry.sdk.resources import Attributes, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace import TracerProvider, _TracerConfiguratorT
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
from opentelemetry.sdk.trace.id_generator import IdGenerator
from opentelemetry.sdk.trace.sampling import Sampler
Expand Down Expand Up @@ -146,6 +147,10 @@ def _get_id_generator() -> str:
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)


def _get_tracer_configurator() -> str | None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the configuration via env var to match what we are doing with the others TraceProvider parameters, since this is in development I can drop it. For my use case I can just use _OTelSDKConfigurator._configure.

return environ.get(OTEL_PYTHON_TRACER_CONFIGURATOR, None)


def _get_exporter_entry_point(
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
):
Expand Down Expand Up @@ -210,11 +215,13 @@ def _init_tracing(
sampler: Sampler | None = None,
resource: Resource | None = None,
exporter_args_map: ExporterArgsMap | None = None,
tracer_configurator: _TracerConfiguratorT | None = None,
):
provider = TracerProvider(
id_generator=id_generator,
sampler=sampler,
resource=resource,
_tracer_configurator=tracer_configurator,
)
set_tracer_provider(provider)

Expand Down Expand Up @@ -315,6 +322,27 @@ def overwritten_config_fn(*args, **kwargs):
logging.basicConfig = wrapper(logging.basicConfig)


def _import_tracer_configurator(
tracer_configurator_name: str | None,
) -> _TracerConfiguratorT | None:
if not tracer_configurator_name:
return None

try:
_, tracer_configurator_impl = _import_config_components(
[tracer_configurator_name.strip()],
"_opentelemetry_tracer_configurator",
)[0]
except Exception as exc: # pylint: disable=broad-exception-caught
_logger.warning(
"Using default tracer configurator. Failed to load tracer configurator, %s: %s",
tracer_configurator_name,
exc,
)
return None
return tracer_configurator_impl


def _import_exporters(
trace_exporter_names: Sequence[str],
metric_exporter_names: Sequence[str],
Expand Down Expand Up @@ -429,7 +457,9 @@ def _initialize_components(
id_generator: IdGenerator | None = None,
setup_logging_handler: bool | None = None,
exporter_args_map: ExporterArgsMap | None = None,
tracer_configurator: _TracerConfiguratorT | None = None,
):
# pylint: disable=too-many-locals
if trace_exporter_names is None:
trace_exporter_names = []
if metric_exporter_names is None:
Expand All @@ -454,6 +484,12 @@ def _initialize_components(
resource_attributes[ResourceAttributes.TELEMETRY_AUTO_VERSION] = ( # type: ignore[reportIndexIssue]
auto_instrumentation_version
)
if tracer_configurator is None:
tracer_configurator_name = _get_tracer_configurator()
tracer_configurator = _import_tracer_configurator(
tracer_configurator_name
)

# if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name
# from the env variable else defaults to "unknown_service"
resource = Resource.create(resource_attributes)
Expand All @@ -464,6 +500,7 @@ def _initialize_components(
sampler=sampler,
resource=resource,
exporter_args_map=exporter_args_map,
tracer_configurator=tracer_configurator,
)
_init_metrics(
metric_exporters, resource, exporter_args_map=exporter_args_map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,15 @@ def channel_credential_provider() -> grpc.ChannelCredentials:
This is an experimental environment variable and the name of this variable and its behavior can
change in a non-backwards compatible way.
"""

OTEL_PYTHON_TRACER_CONFIGURATOR = "OTEL_PYTHON_TRACER_CONFIGURATOR"
"""
.. envvar:: OTEL_PYTHON_TRACER_CONFIGURATOR

The :envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR` environment variable allows users to set a
custom Tracer Configurator function.
Default: opentelemetry.sdk.trace._default_tracer_configurator

This is an experimental environment variable and the name of this variable and its behavior can
change in a non-backwards compatible way.
"""
Loading
Loading